问题描述
解决方案
这是一个基于 QMT (Quantitative Trading Platform) API 编写的 时序动量 (Time Series Momentum) 策略。
策略逻辑说明
时序动量(Time Series Momentum)关注资产自身的历史表现,而不是与其他资产进行横向比较。
- 核心逻辑:如果某只股票在过去 N 天的收益率为正(即当前价格 > N天前价格),则做多(买入);如果收益率为负,则平仓或空仓。
- 回溯周期:代码中默认设置为 20天(约一个月)。
- 资金管理:采用等权重分配。如果有信号的股票数量为 M,则每只股票分配
1/股票池总数的资金。 - 交易标的:示例中使用了一组具有代表性的股票(如贵州茅台、宁德时代等),您可以根据需要修改
ContextInfo.set_universe中的列表。
QMT 策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
初始化函数,策略启动时调用一次
"""
# 1. 设置策略参数
ContextInfo.lookback_period = 20 # 动量回溯周期 (例如20天)
ContextInfo.holding_period = 1 # 调仓周期 (此处示例为每天判断)
ContextInfo.account_id = 'YOUR_ACCOUNT_ID' # 【请务必修改】这里填写您的资金账号
# 2. 设置股票池 (示例:选取几只代表性股票)
# 实际使用时可以使用 ContextInfo.get_sector('000300.SH') 获取沪深300成分股
stock_list = ['600519.SH', '300750.SZ', '000858.SZ', '601318.SH', '002594.SZ']
ContextInfo.set_universe(stock_list)
# 3. 设置账号 (实盘/回测必须)
ContextInfo.set_account(ContextInfo.account_id)
# 4. 设置回测参数 (仅回测有效,实盘需在界面设置)
# 设定初始资金
ContextInfo.capital = 1000000
# 设定手续费 (万三)
ContextInfo.set_commission(0, [0.001, 0.001, 0.0003, 0.0003, 0, 5])
# 设定滑点 (0.02元)
ContextInfo.set_slippage(1, 0.02)
def handlebar(ContextInfo):
"""
K线运行函数,每根K线执行一次
"""
# 获取当前K线位置
index = ContextInfo.barpos
# 获取当前时间
realtime = ContextInfo.get_bar_timetag(index)
# 过滤掉历史K线中数据不足的情况,防止回溯越界
if index < ContextInfo.lookback_period:
return
# 仅在每根K线结束时或特定时间点运行逻辑 (此处假设运行在日线周期)
# 如果是实盘,建议结合 ContextInfo.is_last_bar() 和时间判断
# 1. 获取股票池
stock_list = ContextInfo.get_universe()
if not stock_list:
print("股票池为空")
return
# 2. 获取历史行情数据
# 我们需要获取 (lookback_period + 1) 个数据,以便计算 N 天前的价格对比
# 使用 get_market_data_ex 接口,这是QMT推荐的新接口
# count = ContextInfo.lookback_period + 1
# period = '1d' (日线)
market_data = ContextInfo.get_market_data_ex(
fields=['close'],
stock_code=stock_list,
period='1d',
count=ContextInfo.lookback_period + 1,
dividend_type='front' # 前复权,计算收益率必须用复权数据
)
# 计算每只股票的目标仓位比例 (等权重)
# 如果股票池有5只股票,每只目标仓位最大为 20%
target_weight_per_stock = 1.0 / len(stock_list)
for stock in stock_list:
# 获取该股票的收盘价序列
if stock not in market_data:
continue
df_price = market_data[stock]
# 检查数据长度是否足够
if len(df_price) < ContextInfo.lookback_period + 1:
continue
# 获取当前价格和N天前的价格
# iloc[-1] 是最新价格,iloc[0] 是N天前的价格 (因为取了 N+1 个数据)
current_price = df_price['close'].iloc[-1]
past_price = df_price['close'].iloc[0]
# 3. 计算动量 (Momentum)
# 动量 = 当前价格 / N天前价格 - 1
momentum = (current_price / past_price) - 1.0
# 4. 生成交易信号并执行
# 逻辑:动量 > 0 买入/持有;动量 <= 0 卖出/空仓
if momentum > 0:
# 动量为正,做多
# 使用 order_target_percent 自动调整仓位到目标比例
# 注意:实盘中 order_target_percent 需要配合 passorder 使用,但在回测中可直接调用
# 这里演示标准的回测/仿真写法
order_target_percent(stock, target_weight_per_stock, ContextInfo, ContextInfo.account_id)
# print(f"时间: {realtime}, 标的: {stock}, 动量: {momentum:.4f}, 信号: 买入/持有")
else:
# 动量为负,清仓
order_target_percent(stock, 0.0, ContextInfo, ContextInfo.account_id)
# print(f"时间: {realtime}, 标的: {stock}, 动量: {momentum:.4f}, 信号: 卖出/空仓")
代码关键点解析
-
# -*- coding: gbk -*-:- QMT 的 Python 编辑器默认使用 GBK 编码,这行代码必须放在第一行,否则中文注释会导致报错。
-
ContextInfo.get_market_data_ex:- 这是获取行情的关键接口。
count=ContextInfo.lookback_period + 1: 如果我们要计算20天前的动量,我们需要取21根K线的数据(今天 + 过去20天)。dividend_type='front': 非常重要。计算收益率和动量时,必须使用前复权数据,否则分红派息会导致价格跳空,产生错误的动量信号(例如除息日价格下跌会被误判为动量转负)。
-
order_target_percent:- 这是一个非常方便的交易函数。它会将仓位调整至指定的百分比。
- 如果当前没持仓且目标是 20%,它会买入。
- 如果当前持仓 20% 且目标是 0%,它会卖出平仓。
- 如果当前持仓 10% 且目标是 20%,它会加仓。
-
ContextInfo.account_id:- 请务必将
'YOUR_ACCOUNT_ID'替换为您实际的 QMT 资金账号,否则无法下单。
- 请务必将
如何在 QMT 中运行
- 打开 QMT 客户端,进入【模型研究】或【策略研究】界面。
- 新建一个 Python 策略。
- 将上述代码完整复制粘贴进去。
- 修改
init函数中的ContextInfo.account_id为你的真实账号。 - 在右侧设置回测参数(如周期选择“日线”,时间段选择最近两年),点击【回测】即可看到结果。
- 如果是实盘,请确保在【模型交易】中运行,并注意风险。
Q&A: 常见问题解答
Q: 为什么使用 get_market_data_ex 而不是 get_market_data?
A: get_market_data_ex 是 QMT 较新的接口,返回的数据结构更规范(通常是 Pandas DataFrame 字典),处理多只股票的数据时效率更高,且参数定义更清晰。
Q: 如何将策略改为“横截面动量”策略(选最强的几只)?
A: 目前的代码是时序动量(只要涨就买)。如果要改为横截面动量,需要在 handlebar 中先计算所有股票的 momentum 值,将其存入一个列表,然后使用 sort 进行排序,只对排名前 K 位的股票执行买入操作,其余股票执行卖出操作。
Q: 策略中的 order_target_percent 在实盘中有效吗?
A: 有效,但前提是您必须在 init 中正确设置了 set_account。此外,实盘交易中通常建议结合 passorder 函数以获得更精细的控制(如指定报价模式、盘口价格等),但在简单的日线策略中,order_target_percent 是最简洁的实现方式。
Q: 如何修改回溯周期?
A: 修改 init 函数中的 ContextInfo.lookback_period = 20,将其改为您想要的数值(如 60 代表 60日动量)。