🚀 AI 一键生成 qmt 策略代码

如何构建一个结合基本面筛选和技术面择时的量化选股策略?

qmt | 发布时间: 2025/12/2
以下内容由 EasyQuant 生成。

问题描述

请帮我编写一个选股策略,先找出市盈率低、净资产收益率高的公司,然后在这些公司中根据均线金叉信号进行买入。

解决方案

这是一个非常经典的“基本面选股 + 技术面择时”的双重策略。

策略逻辑分析

  1. 基本面筛选(选股)

    • 低市盈率 (Low PE):寻找估值较低的股票,具有安全边际。
    • 高净资产收益率 (High ROE):寻找盈利能力强的优质公司。
    • 我们将在策略中设定阈值,例如:PE < 30 且 ROE > 15%。
  2. 技术面择时(买卖点)

    • 均线金叉:当短期均线(如5日线)上穿长期均线(如20日线)时,视为上涨趋势确立,执行买入。
    • 卖出逻辑:为了策略完整性,我增加了一个简单的卖出逻辑(死叉卖出),即短期均线下穿长期均线时平仓。

QMT 策略代码实现

以下是完整的 Python 策略代码。代码中包含了详细的注释,直接复制到 QMT 的 Python 编辑器中即可使用。

# -*- coding: gbk -*-
import pandas as pd
import numpy as np

def init(ContextInfo):
    """
    策略初始化函数
    """
    # 1. 设置账号(请替换为您自己的资金账号)
    # ContextInfo.set_account('您的资金账号') 
    
    # 2. 设置策略参数
    ContextInfo.pe_threshold = 30       # 市盈率阈值(小于此值)
    ContextInfo.roe_threshold = 15      # ROE阈值(大于此值)
    ContextInfo.ma_short_period = 5     # 短期均线周期
    ContextInfo.ma_long_period = 20     # 长期均线周期
    ContextInfo.target_pos = 0.1        # 每只股票买入仓位占比(例如10%)
    
    # 3. 设置股票池(这里以沪深300成分股为例)
    # 注意:实际运行时建议在“数据管理”中补充好相关品种的日线和财务数据
    ContextInfo.trade_code_list = ContextInfo.get_stock_list_in_sector('沪深300')
    ContextInfo.set_universe(ContextInfo.trade_code_list)
    
    # 4. 设置回测费率等(仅回测有效)
    ContextInfo.set_commission(0, [0.001, 0.001, 0.0003, 0.0003, 0.0003, 5])

def handlebar(ContextInfo):
    """
    K线周期运行函数
    """
    # 获取当前K线位置
    index = ContextInfo.barpos
    # 获取当前时间戳并转换为日期格式 YYYYMMDD
    timetag = ContextInfo.get_bar_timetag(index)
    current_date = timetag_to_datetime(timetag, '%Y%m%d')
    
    # 跳过历史数据不足以计算均线的时期
    if index < ContextInfo.ma_long_period:
        return

    # --- 第一步:基本面筛选 ---
    
    # 定义需要获取的因子字段
    # PE: Valuation_and_Market_Cap.PE (市盈率)
    # ROE: Profitability_and_Earning Quality.ROE (权益回报率)
    factor_fields = [
        'Valuation_and_Market_Cap.PE', 
        'Profitability_and_Earning Quality.ROE'
    ]
    
    # 获取当天的因子数据
    # 注意:get_factor_data 在回测中能取到历史对应时刻的数据
    factor_data = ContextInfo.get_factor_data(
        factor_fields, 
        ContextInfo.trade_code_list, 
        current_date, 
        current_date
    )
    
    # 筛选出的优质股票列表
    fundamental_select_list = []
    
    if not factor_data.empty:
        # 遍历股票池进行筛选
        for stock in ContextInfo.trade_code_list:
            if stock in factor_data.index:
                try:
                    pe_val = factor_data.loc[stock, 'Valuation_and_Market_Cap.PE']
                    roe_val = factor_data.loc[stock, 'Profitability_and_Earning Quality.ROE']
                    
                    # 过滤无效数据
                    if np.isnan(pe_val) or np.isnan(roe_val):
                        continue
                        
                    # 判断条件:低PE 且 高ROE
                    if 0 < pe_val < ContextInfo.pe_threshold and roe_val > ContextInfo.roe_threshold:
                        fundamental_select_list.append(stock)
                except KeyError:
                    continue
    
    # --- 第二步:技术面择时与交易 ---
    
    # 获取持仓信息
    positions = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'position')
    holding_stocks = [pos.m_strInstrumentID + '.' + pos.m_strExchangeID for pos in positions]

    # 遍历筛选出的股票,检查均线金叉
    for stock in fundamental_select_list:
        # 获取历史行情数据 (多取一点数据以确保计算均线足够)
        # 注意:get_market_data_ex 返回的是 dict {code: dataframe}
        market_data = ContextInfo.get_market_data_ex(
            ['close'], 
            [stock], 
            period='1d', 
            end_time=current_date, 
            count=ContextInfo.ma_long_period + 2,
            dividend_type='front' # 前复权
        )
        
        if stock not in market_data or market_data[stock].empty:
            continue
            
        df_price = market_data[stock]
        
        # 如果数据长度不足,跳过
        if len(df_price) < ContextInfo.ma_long_period + 1:
            continue
            
        # 计算均线
        close_prices = df_price['close'].values
        ma_short = pd.Series(close_prices).rolling(window=ContextInfo.ma_short_period).mean().values
        ma_long = pd.Series(close_prices).rolling(window=ContextInfo.ma_long_period).mean().values
        
        # 获取最近两个点的均线值(用于判断交叉)
        # -1 是当前bar,-2 是上一根bar
        curr_short = ma_short[-1]
        curr_long = ma_long[-1]
        prev_short = ma_short[-2]
        prev_long = ma_long[-2]
        
        # 检查是否有效
        if np.isnan(curr_short) or np.isnan(curr_long):
            continue

        # 信号判断:金叉 (短期均线上穿长期均线)
        # 逻辑:当前短线 > 长线 且 上一根短线 <= 长线
        golden_cross = (curr_short > curr_long) and (prev_short <= prev_long)
        
        # 执行买入
        if golden_cross and stock not in holding_stocks:
            # 使用目标比例下单,控制仓位
            # 注意:实盘中需要确保已设置账号
            print(f"日期: {current_date}, 股票: {stock} 触发金叉买入 (PE:{pe_val:.2f}, ROE:{roe_val:.2f})")
            order_target_percent(stock, ContextInfo.target_pos, ContextInfo, ContextInfo.accid)

    # --- 第三步:卖出逻辑 (死叉平仓) ---
    # 遍历当前持仓,如果发生死叉则卖出
    for stock in holding_stocks:
        market_data = ContextInfo.get_market_data_ex(
            ['close'], 
            [stock], 
            period='1d', 
            end_time=current_date, 
            count=ContextInfo.ma_long_period + 2,
            dividend_type='front'
        )
        
        if stock not in market_data or market_data[stock].empty:
            continue
            
        df_price = market_data[stock]
        if len(df_price) < ContextInfo.ma_long_period + 1:
            continue
            
        close_prices = df_price['close'].values
        ma_short = pd.Series(close_prices).rolling(window=ContextInfo.ma_short_period).mean().values
        ma_long = pd.Series(close_prices).rolling(window=ContextInfo.ma_long_period).mean().values
        
        curr_short = ma_short[-1]
        curr_long = ma_long[-1]
        prev_short = ma_short[-2]
        prev_long = ma_long[-2]
        
        # 信号判断:死叉 (短期均线下穿长期均线)
        dead_cross = (curr_short < curr_long) and (prev_short >= prev_long)
        
        if dead_cross:
            print(f"日期: {current_date}, 股票: {stock} 触发死叉卖出")
            order_target_percent(stock, 0, ContextInfo, ContextInfo.accid)

代码关键点解析

  1. 数据获取 (get_factor_data)

    • 使用了 QMT 内置的多因子库。Valuation_and_Market_Cap.PE 代表市盈率,Profitability_and_Earning Quality.ROE 代表净资产收益率。
    • 这个接口非常强大,在回测时会自动获取历史时刻的财务数据,避免了“未来函数”的问题。
  2. 行情获取 (get_market_data_ex)

    • 我们使用 get_market_data_ex 获取历史收盘价来计算均线。
    • 设置了 dividend_type='front'(前复权),这对于技术指标计算(如均线)非常重要,可以防止分红除权导致的价格跳空影响信号。
  3. 交易执行 (order_target_percent)

    • 使用 order_target_percent 可以方便地管理仓位。例如设置为 0.1 表示将该股票买入至总资产的 10%。
    • 卖出时设置为 0 即可清仓。
  4. 运行频率

    • 该策略设计为日线级别 (period='1d')。在 QMT 界面运行或回测时,请确保主图周期选择为“日线”。

使用前准备

  1. 补充数据:在 QMT 客户端顶部菜单,点击【数据管理】->【补充数据】,下载“沪深300”成分股的日线数据财务数据/多因子数据
  2. 设置账号:在代码 init 函数中,取消注释 ContextInfo.set_account 并填入您的真实或模拟资金账号,或者在策略编辑器的右侧参数栏设置交易账号。

Q&A

Q: 如何修改选股范围,比如改成中证500?
A: 在 init 函数中,将 ContextInfo.get_stock_list_in_sector('沪深300') 修改为 ContextInfo.get_stock_list_in_sector('中证500') 即可。

Q: 为什么回测时没有交易?
A: 常见原因有:1. 没有下载多因子数据或日线数据;2. 筛选条件过严(例如PE设置得太低);3. 回测时间段内没有触发金叉信号。建议先放宽 PE/ROE 阈值测试一下。

Q: 这个策略可以实盘自动交易吗?
A: 可以。在实盘模式下,QMT 会在每日收盘前或盘中(取决于您的设置)运行 handlebar,当数据满足条件时会自动发出委托。请务必先在模拟盘充分测试。