问题描述
解决方案
这是一个结合了**估值因子(PE)和动量因子(20日涨跌幅)**的多因子选股策略。
策略逻辑说明
- 选股范围:沪深300成分股。
- 因子选择:
- 估值因子:市盈率(PE),使用 QMT 内置因子
Valuation_and_Market_Cap.PE。逻辑是低估值优先(PE越小越好)。 - 动量因子:20日价格动量,使用 QMT 内置因子
Trend.REVS20。逻辑是动量效应(过去20日涨幅越高越好)。
- 估值因子:市盈率(PE),使用 QMT 内置因子
- 打分机制:
- 对股票池内的股票按 PE 从小到大排名。
- 对股票池内的股票按 动量 从大到小排名。
- 将两个排名相加得到总分,总分越低代表综合表现越好。
- 调仓周期:按月调仓(每月月初运行一次)。
- 交易执行:选取总分排名前 20 的股票,等权重买入。不在名单中的持仓股票卖出。
QMT 策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
策略初始化函数
"""
# 1. 策略参数设置
ContextInfo.target_num = 20 # 持仓股票数量
ContextInfo.benchmark = '000300.SH' # 股票池基准(沪深300)
ContextInfo.account_id = '600000248' # 请修改为您的资金账号
# 2. 设置账号
ContextInfo.set_account(ContextInfo.account_id)
# 3. 设置股票池
# 获取沪深300成分股
stock_list = ContextInfo.get_sector(ContextInfo.benchmark)
ContextInfo.set_universe(stock_list)
# 4. 调仓控制变量
ContextInfo.last_month = -1 # 用于记录上一次调仓的月份
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 1. 获取当前时间
# get_bar_timetag 返回的是毫秒时间戳
timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
# 转换为日期字符串 YYYYMMDD
current_date_str = timetag_to_datetime(timetag, '%Y%m%d')
# 获取当前月份
current_month = int(current_date_str[4:6])
# 2. 判断是否换月(简单的月度调仓逻辑)
if current_month == ContextInfo.last_month:
return
# 更新月份记录
ContextInfo.last_month = current_month
print(f'触发调仓,当前日期: {current_date_str}')
# 3. 获取因子数据
# 使用 QMT 内置的多因子库
# Valuation_and_Market_Cap.PE : 市盈率
# Trend.REVS20 : 20日价格动量
factor_fields = ['Valuation_and_Market_Cap.PE', 'Trend.REVS20']
stock_list = ContextInfo.get_universe()
# 获取当天的因子数据
# 注意:get_factor_data 返回的数据格式取决于参数。
# 传入股票列表和单日时间,返回 DataFrame (Index=股票代码, Columns=因子名)
df_factors = ContextInfo.get_factor_data(
factor_fields,
stock_list,
current_date_str,
current_date_str
)
if df_factors is None or df_factors.empty:
print("未获取到因子数据,跳过本次调仓")
return
# 4. 数据清洗与打分
# 去除空值
df_factors.dropna(inplace=True)
# 过滤掉 PE 为负(亏损)的股票,通常估值策略只看正PE
df_factors = df_factors[df_factors['Valuation_and_Market_Cap.PE'] > 0]
if df_factors.empty:
return
# 计算排名 (Rank)
# PE: 升序排名 (值越小排名越靠前,rank值越小)
df_factors['rank_pe'] = df_factors['Valuation_and_Market_Cap.PE'].rank(ascending=True)
# 动量: 降序排名 (值越大排名越靠前,rank值越小)
df_factors['rank_mom'] = df_factors['Trend.REVS20'].rank(ascending=False)
# 计算综合得分 (等权重相加)
df_factors['score'] = df_factors['rank_pe'] + df_factors['rank_mom']
# 按得分升序排列 (分数越低越好)
df_factors.sort_values(by='score', ascending=True, inplace=True)
# 选取前 N 只股票
target_list = df_factors.head(ContextInfo.target_num).index.tolist()
print(f"选股结果: {target_list}")
# 5. 交易执行
# 计算单只股票的目标权重 (等权重)
if len(target_list) > 0:
target_weight = 1.0 / len(target_list)
else:
target_weight = 0.0
# 获取当前持仓
positions = get_trade_detail_data(ContextInfo.account_id, 'stock', 'position')
holding_list = [obj.m_strInstrumentID for obj in positions]
# 卖出逻辑:不在目标列表中的股票,权重设为0(清仓)
for code in holding_list:
if code not in target_list:
order_target_percent(code, 0, ContextInfo, ContextInfo.account_id)
# 买入/调仓逻辑:在目标列表中的股票,调整至目标权重
for code in target_list:
order_target_percent(code, target_weight, ContextInfo, ContextInfo.account_id)
代码关键点解析
-
get_factor_data接口:- 这是 QMT 获取基本面和技术面因子的核心接口。
Valuation_and_Market_Cap.PE对应市盈率。Trend.REVS20对应20日收益率(动量)。- 该接口直接返回 pandas DataFrame,非常适合进行向量化计算(如
rank排序)。
-
打分逻辑:
- 使用
rank()函数将绝对数值转换为相对排名。这消除了不同因子量纲(PE是几十倍,动量是百分比)不一致的问题。 - PE 越低越好,所以
ascending=True。 - 动量越高越好,所以
ascending=False。
- 使用
-
交易执行:
- 使用
order_target_percent函数。这是一个非常方便的高级交易函数,它会自动计算当前持仓与目标仓位的差额,并发出相应的买入或卖出指令。例如,如果目标权重是 5%,当前持有 2%,它会自动买入 3%。
- 使用
-
数据清洗:
- 代码中增加了
df_factors.dropna()和PE > 0的过滤,这是量化实盘中防止脏数据导致报错或买入亏损股的必要步骤。
- 代码中增加了
常见问题 (Q&A)
Q: 如何查看 QMT 支持的所有因子名称?
A: 在 QMT 软件界面的“帮助”文档中,或者在策略编辑器中查看“多因子库”部分。文档附录通常有详细的因子表名和字段名对照表(如 Valuation_and_Market_Cap 表下的 PE)。
Q: 为什么使用 order_target_percent 而不是 passorder?
A: passorder 是底层的下单接口,需要自己计算买卖数量。对于选股策略,我们通常关注的是“仓位占比”,order_target_percent 封装了计算逻辑,能自动处理买卖方向和数量,代码更简洁且不易出错。
Q: 这个策略可以在回测和实盘中直接运行吗?
A: 可以。但请注意:
- 账号ID:代码中的
ContextInfo.account_id需要修改为你实际的资金账号。 - 数据下载:运行前请确保在 QMT 的“数据管理”中下载了对应的历史行情数据和多因子数据。如果没有下载多因子数据,
get_factor_data将无法返回结果。