🚀 AI 一键生成 qmt 策略代码

如何结合多个因子(如PE、ROE、动量)对股票进行综合评分和排名?

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

问题描述

请帮我写一个选股脚本,根据市盈率、市净率和最近一个月涨幅给股票打分,然后选出综合得分最高的10只股票。

解决方案

这是一个基于 QMT Python API 编写的选股策略脚本。

策略逻辑说明

  1. 选股范围:沪深300成分股(可根据需要修改为全A股或其他板块)。
  2. 因子选择
    • 市盈率 (PE):数值越低得分越高(剔除负值)。
    • 市净率 (PB):数值越低得分越高(剔除负值)。
    • 月度涨幅 (Momentum):最近20个交易日涨幅,涨幅越高得分越高。
  3. 打分机制:采用排名打分法。对每个因子进行排名,将三个因子的排名相加,总排名数值越小(即综合排名越靠前),代表股票越好。
  4. 交易逻辑
    • 选出综合得分最高的 10 只股票。
    • 卖出不在前 10 名的持仓股票。
    • 买入前 10 名的股票,等权重分配资金。
  5. 调仓频率:脚本设定为每日运行检测(实际回测中通常建议按月调仓,这里为了演示方便设为逐K线运行)。

QMT 策略代码

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

def init(ContextInfo):
    # 设置账号(请替换为您自己的资金账号)
    ContextInfo.accid = '6000000000' 
    ContextInfo.set_account(ContextInfo.accid)
    
    # 策略参数设置
    ContextInfo.target_num = 10  # 持仓只数
    ContextInfo.index_code = '000300.SH' # 股票池:沪深300
    
    # 设定回测参数(如果是回测模式)
    ContextInfo.set_universe([ContextInfo.index_code])
    
    print("策略初始化完成")

def handlebar(ContextInfo):
    # 跳过历史K线,只在最新K线或回测的每一根K线结束时运行
    if not ContextInfo.is_last_bar():
        return

    # 获取当前时间
    bar_index = ContextInfo.barpos
    current_date = ContextInfo.get_bar_timetag(bar_index)
    date_str = timetag_to_datetime(current_date, '%Y%m%d')
    print(f'当前运行日期: {date_str}')

    # 1. 获取股票池 (沪深300成分股)
    stock_list = ContextInfo.get_stock_list_in_sector(ContextInfo.index_code)
    if not stock_list:
        print("未获取到成分股,请检查数据下载情况")
        return

    # 2. 获取因子数据 (PE, PB)
    # 注意:QMT因子库字段名需准确,这里使用 Valuation_and_Market_Cap 表
    factor_fields = [
        'Valuation_and_Market_Cap.PE', # 市盈率
        'Valuation_and_Market_Cap.PB'  # 市净率
    ]
    
    # 获取因子数据,返回的是字典或DataFrame
    factor_data = ContextInfo.get_factor_data(
        factor_fields, 
        stock_list, 
        date_str, 
        date_str
    )
    
    # 数据清洗与整理
    df_factors = pd.DataFrame(index=stock_list)
    
    # 解析 get_factor_data 返回的数据结构
    # 如果返回的是字典结构 (code -> df),需要转换
    # 如果是一维时间点,通常返回 DataFrame (index=code, columns=fields)
    if isinstance(factor_data, dict):
        # 处理可能的字典返回格式
        pe_list = []
        pb_list = []
        valid_stocks = []
        for stock in stock_list:
            if stock in factor_data:
                # 假设取最新的一条数据
                try:
                    # 这里视具体返回结构而定,通常单日查询直接取值
                    df_temp = factor_data[stock]
                    if not df_temp.empty:
                        pe_list.append(df_temp['Valuation_and_Market_Cap.PE'].iloc[-1])
                        pb_list.append(df_temp['Valuation_and_Market_Cap.PB'].iloc[-1])
                        valid_stocks.append(stock)
                except:
                    pass
        df_factors = pd.DataFrame({
            'PE': pe_list,
            'PB': pb_list
        }, index=valid_stocks)
    else:
        # 如果直接返回DataFrame
        df_factors = factor_data
        df_factors.columns = ['PE', 'PB']

    # 3. 获取行情数据计算涨幅 (最近1个月,约20个交易日)
    # 获取收盘价,count=21 表示取过去21根K线,计算 (今天-20天前)/20天前
    market_data = ContextInfo.get_market_data_ex(
        ['close'], 
        stock_list, 
        period='1d', 
        count=21, 
        dividend_type='front' # 前复权
    )
    
    mom_dict = {}
    for stock in stock_list:
        if stock in market_data:
            df_price = market_data[stock]
            if len(df_price) >= 21:
                price_now = df_price['close'].iloc[-1]
                price_prev = df_price['close'].iloc[0]
                # 计算涨幅
                if price_prev > 0:
                    pct_chg = (price_now - price_prev) / price_prev
                    mom_dict[stock] = pct_chg
    
    df_mom = pd.DataFrame.from_dict(mom_dict, orient='index', columns=['MOM'])
    
    # 4. 合并数据
    df_total = pd.concat([df_factors, df_mom], axis=1)
    df_total.dropna(inplace=True) # 去除缺失值
    
    # 剔除 PE 或 PB 为负的股票 (亏损股或资不抵债)
    df_total = df_total[(df_total['PE'] > 0) & (df_total['PB'] > 0)]
    
    if df_total.empty:
        print("没有符合条件的股票")
        return

    # 5. 打分逻辑 (排名法)
    # PE: 升序排名 (越小越好)
    df_total['rank_pe'] = df_total['PE'].rank(ascending=True)
    # PB: 升序排名 (越小越好)
    df_total['rank_pb'] = df_total['PB'].rank(ascending=True)
    # MOM: 降序排名 (涨幅越大越好,rank值越小代表排名越靠前)
    df_total['rank_mom'] = df_total['MOM'].rank(ascending=False)
    
    # 综合得分 = 各项排名之和 (总分越低,综合排名越靠前)
    df_total['score'] = df_total['rank_pe'] + df_total['rank_pb'] + df_total['rank_mom']
    
    # 按得分升序排列,取前10
    df_total.sort_values(by='score', ascending=True, inplace=True)
    target_list = df_total.head(ContextInfo.target_num).index.tolist()
    
    print(f"今日选股结果: {target_list}")
    
    # 6. 交易执行
    # 获取当前持仓
    positions = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'position')
    current_holdings = [obj.m_strInstrumentID + '.' + obj.m_strExchangeID for obj in positions]
    
    # 卖出不在目标列表中的股票
    for stock in current_holdings:
        if stock not in target_list:
            passorder(24, 1101, ContextInfo.accid, stock, 5, -1, 0, ContextInfo) # 0代表全卖
            print(f"卖出: {stock}")
    
    # 买入目标股票
    # 简单资金分配:可用资金平均分配 (这里仅做演示,实际需考虑资金管理)
    # 注意:实盘中需获取可用资金 ContextInfo.get_trade_detail_data(..., 'account')
    
    # 这里使用 order_target_percent 模拟调仓到目标比例 (需在回测设置中支持,实盘建议用 passorder)
    # 为了兼容性,这里演示使用 target_percent 逻辑 (每个股票占总资产的 1/10)
    weight = 1.0 / ContextInfo.target_num
    
    for stock in target_list:
        # 目标市值下单 (需要配合回测引擎或自行计算股数)
        # 这里演示使用 order_target_percent (仅回测有效,实盘请用 passorder 计算 volume)
        if ContextInfo.do_back_test:
            order_target_percent(stock, weight, ContextInfo, ContextInfo.accid)
        else:
            # 实盘简单示例:按固定金额买入,例如每只买1万元 (请根据实际资金修改)
            # passorder(23, 1102, ContextInfo.accid, stock, 5, -1, 10000, ContextInfo)
            print(f"信号触发: 买入 {stock}")

使用说明与注意事项

  1. 数据下载

    • 运行此脚本前,必须在 QMT 客户端的【数据管理】中下载历史行情数据(日线)和财务/因子数据(特别是“Valuation_and_Market_Cap”表)。如果没有本地数据,get_factor_data 将无法返回结果。
  2. 账号设置

    • 代码中的 ContextInfo.accid = '6000000000' 需要修改为您实际的资金账号。
  3. 交易函数

    • 代码中为了演示逻辑,回测部分使用了 order_target_percent
    • 如果是实盘,建议使用 passorder 函数。我在代码注释中提供了一个实盘买入的示例(按金额买入),您需要根据账户实际可用资金来计算具体的买入数量。
  4. 因子方向

    • 本策略假设是价值+动量风格:低PE(便宜)、低PB(便宜)、高涨幅(强势)。如果您想做反转策略(买跌得多的),可以将 rank_momascending 改为 True
  5. 运行频率

    • 当前脚本在 handlebar 中运行,意味着每根 K 线(如果是日线运行,就是每天)都会检测并调仓。如果您希望按月调仓,可以在代码中加入日期判断逻辑(例如判断是否是月初)。

常见问题 Q&A

Q: 为什么运行后提示“未获取到成分股”?
A: 请检查 ContextInfo.index_code 设置的指数代码是否正确,以及是否在数据管理中下载了该指数的板块数据。

Q: 为什么 get_factor_data 返回空值?
A: QMT 的因子数据依赖本地库。请务必在菜单栏“系统”->“数据管理”->“数据下载”中,勾选“财务数据”或“多因子数据”进行补充下载。

Q: 如何查看选出的股票具体得分?
A: 您可以在代码 print(f"今日选股结果: {target_list}") 之前,添加 print(df_total.head(10)) 来查看前10名的具体 PE、PB、MOM 数值和综合得分。