问题描述
解决方案
这是一个基于 QMT Python API 编写的选股策略脚本。
策略逻辑说明
- 选股范围:沪深300成分股(可根据需要修改为全A股或其他板块)。
- 因子选择:
- 市盈率 (PE):数值越低得分越高(剔除负值)。
- 市净率 (PB):数值越低得分越高(剔除负值)。
- 月度涨幅 (Momentum):最近20个交易日涨幅,涨幅越高得分越高。
- 打分机制:采用排名打分法。对每个因子进行排名,将三个因子的排名相加,总排名数值越小(即综合排名越靠前),代表股票越好。
- 交易逻辑:
- 选出综合得分最高的 10 只股票。
- 卖出不在前 10 名的持仓股票。
- 买入前 10 名的股票,等权重分配资金。
- 调仓频率:脚本设定为每日运行检测(实际回测中通常建议按月调仓,这里为了演示方便设为逐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}")
使用说明与注意事项
-
数据下载:
- 运行此脚本前,必须在 QMT 客户端的【数据管理】中下载历史行情数据(日线)和财务/因子数据(特别是“Valuation_and_Market_Cap”表)。如果没有本地数据,
get_factor_data将无法返回结果。
- 运行此脚本前,必须在 QMT 客户端的【数据管理】中下载历史行情数据(日线)和财务/因子数据(特别是“Valuation_and_Market_Cap”表)。如果没有本地数据,
-
账号设置:
- 代码中的
ContextInfo.accid = '6000000000'需要修改为您实际的资金账号。
- 代码中的
-
交易函数:
- 代码中为了演示逻辑,回测部分使用了
order_target_percent。 - 如果是实盘,建议使用
passorder函数。我在代码注释中提供了一个实盘买入的示例(按金额买入),您需要根据账户实际可用资金来计算具体的买入数量。
- 代码中为了演示逻辑,回测部分使用了
-
因子方向:
- 本策略假设是价值+动量风格:低PE(便宜)、低PB(便宜)、高涨幅(强势)。如果您想做反转策略(买跌得多的),可以将
rank_mom的ascending改为True。
- 本策略假设是价值+动量风格:低PE(便宜)、低PB(便宜)、高涨幅(强势)。如果您想做反转策略(买跌得多的),可以将
-
运行频率:
- 当前脚本在
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 数值和综合得分。