问题描述
解决方案
这是一个基于QMT平台的动量策略实现。
策略逻辑分析
- 选股范围:默认为沪深300成分股(可在
init中修改)。 - 动量计算:计算过去60个交易日(约3个月)的收益率。
- 筛选标准:选取收益率最高的10%的股票。
- 调仓周期:按月调仓。当检测到当前K线所在的月份与上一次交易月份不同时,触发调仓。
- 交易执行:
- 清仓不在目标列表中的持仓。
- 对目标列表中的股票进行等权重买入。
策略代码
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
"""
初始化函数,设定股票池、账号、全局变量
"""
# 1. 设定基准和股票池(这里以沪深300为例)
ContextInfo.index_code = '000300.SH'
ContextInfo.set_universe(ContextInfo.get_stock_list_in_sector('沪深300'))
# 2. 设定资金账号 (请替换为您自己的资金账号)
ContextInfo.accID = 'YOUR_ACCOUNT_ID'
ContextInfo.set_account(ContextInfo.accID)
# 3. 初始化全局变量
ContextInfo.last_month = None # 用于记录上一次调仓的月份
ContextInfo.holdings = [] # 记录当前持仓
print("策略初始化完成")
def get_momentum_stocks(ContextInfo):
"""
计算动量并筛选前10%的股票
"""
stock_list = ContextInfo.get_universe()
# 获取过去约3个月(60个交易日)的历史收盘价数据
# count=61 是为了保证能计算出60天的涨幅 (今天 / 60天前 - 1)
data_map = ContextInfo.get_market_data_ex(
['close'],
stock_list,
period='1d',
count=61,
dividend_type='front' # 前复权
)
momentum_dict = {}
for stock in stock_list:
if stock in data_map:
df = data_map[stock]
# 确保数据长度足够计算
if len(df) >= 60:
# 获取最新收盘价和60天前的收盘价
price_now = df.iloc[-1]['close']
price_prev = df.iloc[0]['close'] # 取获取到的数据的第一条作为基准
if price_prev > 0:
# 计算收益率
ret = (price_now - price_prev) / price_prev
momentum_dict[stock] = ret
# 按收益率从高到低排序
sorted_stocks = sorted(momentum_dict.items(), key=lambda x: x[1], reverse=True)
# 选取前10%
select_count = int(len(sorted_stocks) * 0.1)
if select_count < 1:
select_count = 1
target_stocks = [x[0] for x in sorted_stocks[:select_count]]
return target_stocks
def rebalance(ContextInfo, target_stocks):
"""
调仓执行函数
"""
print(f"开始调仓,目标持仓数量: {len(target_stocks)}")
# 1. 获取当前持仓
# 注意:回测模式下可以直接读取 ContextInfo.holdings (如果自己维护)
# 或者使用 get_trade_detail_data 获取真实/模拟账户持仓
positions = get_trade_detail_data(ContextInfo.accID, 'stock', 'position')
current_holdings = [obj.m_strInstrumentID + '.' + obj.m_strExchangeID for obj in positions]
# 2. 卖出不在目标池中的股票
for stock in current_holdings:
if stock not in target_stocks:
# 目标仓位为0,即清仓
order_target_percent(stock, 0, 'fix', -1, ContextInfo, ContextInfo.accID)
print(f"卖出平仓: {stock}")
# 3. 买入目标池中的股票
if len(target_stocks) > 0:
# 等权重分配,每个股票分配 1/N 的资金
weight = 1.0 / len(target_stocks)
for stock in target_stocks:
# 目标仓位调整为 weight (例如 0.05 代表 5%)
# 注意:order_target_percent 需要在回测或实盘支持该接口的环境下运行
# 如果是纯实盘简易模式,可能需要改用 order_target_value
order_target_percent(stock, weight, 'fix', -1, ContextInfo, ContextInfo.accID)
print(f"买入/调整: {stock}, 目标权重: {weight:.2%}")
def handlebar(ContextInfo):
"""
K线周期运行函数
"""
# 获取当前K线的时间
index = ContextInfo.barpos
timetag = ContextInfo.get_bar_timetag(index)
current_date_str = timetag_to_datetime(timetag, '%Y%m%d')
current_month = current_date_str[0:6] # 提取 'YYYYMM'
# 判断是否是新的一月
if ContextInfo.last_month != current_month:
print(f"检测到新月份: {current_month},触发调仓逻辑...")
# 1. 选股
target_stocks = get_momentum_stocks(ContextInfo)
# 2. 执行调仓
rebalance(ContextInfo, target_stocks)
# 3. 更新月份记录
ContextInfo.last_month = current_month
代码关键点说明
-
ContextInfo.get_market_data_ex:- 这是QMT获取历史行情的核心接口。
count=61:我们需要计算过去60个交易日的涨幅,所以取61根K线,用(第61根收盘价 - 第1根收盘价) / 第1根收盘价来计算。dividend_type='front':使用前复权数据,这对于计算长周期收益率至关重要,可以避免分红除权导致的价格跳水误判。
-
调仓时间控制:
- 策略通过比较
current_month和ContextInfo.last_month来判断是否换月。 - 在回测中,这通常意味着每个月第一个交易日的开盘阶段(或收盘阶段,取决于回测机制设置)会触发信号。
- 策略通过比较
-
交易函数
order_target_percent:- 该函数用于将某只股票的仓位调整到总资产的指定比例。
weight = 1.0 / len(target_stocks)实现了等权重买入。- 如果某只股票已经在持仓中且在目标列表中,该函数会自动加仓或减仓使其达到目标权重。
- 如果某只股票不在目标列表中,我们显式调用
order_target_percent(stock, 0, ...)进行平仓。
-
账号设置:
- 代码中
ContextInfo.accID = 'YOUR_ACCOUNT_ID'处需要您填入实际的资金账号(回测时通常不需要改,实盘/模拟盘必须填对)。
- 代码中
使用前注意事项
- 数据下载:请务必在QMT客户端的【数据管理】中,下载补充好所需的历史日线数据(包括沪深300成分股的日线数据),否则
get_market_data_ex可能返回空值。 - 回测设置:在策略编辑器的回测参数中,设置好初始资金、费率等参数。
- 实盘风险:此代码主要用于回测和学习。实盘使用时,建议增加风控逻辑(如停牌股处理、涨跌停无法成交处理、滑点控制等)。
Q&A
Q: 如何修改回测的起止时间?
A: 在QMT策略编辑器的右侧“回测参数”面板中直接设置“开始时间”和“结束时间”,或者在代码 init 函数中添加 ContextInfo.start = '2020-01-01' 和 ContextInfo.end = '2021-01-01'。
Q: 为什么回测时没有交易?
A: 1. 检查是否下载了对应的历史数据。2. 检查 ContextInfo.accID 是否设置正确(回测模式下通常会自动分配虚拟账号,但显式设置为空字符串 '' 有时更安全)。3. 检查 get_stock_list_in_sector 获取的板块名称是否正确。
Q: 如何将选股范围改为全市场?
A: 将 init 中的 ContextInfo.set_universe(...) 替换为 ContextInfo.set_universe(ContextInfo.get_stock_list_in_sector('沪深A股'))。注意全市场选股计算量较大,回测速度会变慢。