问题描述
解决方案
这是一个非常经典的动量策略(Momentum Strategy)。
策略逻辑分析
- 选股范围:股票池(例如沪深300或自定义板块)。
- 调仓周期:按月调仓(通常在每个月的第一个交易日或最后一个交易日)。
- 因子计算:计算过去20个交易日的涨跌幅。
- 公式:$(当前收盘价 - 20日前收盘价) / 20日前收盘价$
- 排序与筛选:
- 按涨幅从高到低排序。
- 买入:排名前 20% 的股票。
- 卖出:为了保持仓位纯粹性,通常的做法是清仓所有不在“排名前20%”名单中的股票(这自然包含了排名后20%的股票以及中间的股票),并将资金等权买入新的前20%股票。
代码实现
以下是基于 QMT Python API 的完整策略代码。
# -*- coding: gbk -*-
import pandas as pd
import time
def init(ContextInfo):
# 1. 设置股票池,这里示例使用沪深300,您可以修改为 '我的自选' 等
# 注意:实盘或回测前请确保已下载相关板块的历史数据
ContextInfo.index_code = '000300.SH'
ContextInfo.stock_list = ContextInfo.get_stock_list_in_sector('沪深300')
# 2. 设置策略参数
ContextInfo.lookback_days = 20 # 回溯天数:20天
ContextInfo.select_ratio = 0.2 # 筛选比例:前20%
ContextInfo.hold_percent = 0.98 # 目标总仓位(留一点现金防止手续费不够)
# 3. 记录上一次调仓的月份,用于判断是否换月
ContextInfo.last_month = None
# 4. 设置资金账号 (请修改为您自己的资金账号)
# ContextInfo.set_account('您的资金账号')
def handlebar(ContextInfo):
# 获取当前K线的时间
current_timetag = ContextInfo.get_bar_timetag(ContextInfo.barpos)
current_date_str = timetag_to_datetime(current_timetag, '%Y%m%d')
current_month = current_date_str[0:6] # 提取年份和月份,如 '202305'
# 判断是否换月:如果当前月份与上一次记录的月份不同,则触发调仓
# 注意:这里是在每个月出现的第一个K线(即月初)进行调仓
if current_month != ContextInfo.last_month:
print(f'检测到月份变化,开始执行调仓逻辑。当前日期: {current_date_str}')
rebalance(ContextInfo)
ContextInfo.last_month = current_month
def rebalance(ContextInfo):
# 1. 获取历史行情数据
# 我们需要获取过去21天的数据(包含今天),以便计算 (今天收盘 - 20天前收盘) / 20天前收盘
# dividend_type='front' 使用前复权数据,保证收益率计算准确
count = ContextInfo.lookback_days + 1
market_data = ContextInfo.get_market_data_ex(
['close'],
ContextInfo.stock_list,
period='1d',
count=count,
dividend_type='front'
)
# 2. 计算涨幅
momentum_data = []
for stock in ContextInfo.stock_list:
if stock in market_data:
df = market_data[stock]
# 确保数据长度足够
if len(df) >= count:
price_now = df.iloc[-1]['close'] # 最新收盘价
price_prev = df.iloc[0]['close'] # 20天前的收盘价
# 避免除以0或停牌导致的数据异常
if price_prev > 0 and price_now > 0:
pct_change = (price_now - price_prev) / price_prev
momentum_data.append({
'code': stock,
'pct_change': pct_change
})
# 如果没有有效数据,直接返回
if not momentum_data:
print("未获取到有效行情数据,跳过本次调仓")
return
# 3. 排序与筛选
# 按涨幅从大到小排序
df_momentum = pd.DataFrame(momentum_data)
df_momentum = df_momentum.sort_values(by='pct_change', ascending=False)
# 计算买入数量
target_count = int(len(df_momentum) * ContextInfo.select_ratio)
if target_count == 0:
target_count = 1
# 获取目标买入列表(前20%)
target_list = df_momentum.iloc[:target_count]['code'].tolist()
print(f"本次选股数量: {len(target_list)}, 排名首位: {target_list[0]}")
# 4. 执行交易
# 获取当前持仓
positions = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'position')
current_holding_codes = [pos.m_strInstrumentID + '.' + pos.m_strExchangeID for pos in positions]
# 4.1 卖出逻辑:不在目标列表中的股票全部卖出
# 这涵盖了“卖出排名后20%”以及“卖出中间排名”的股票,只保留最强的
for stock in current_holding_codes:
if stock not in target_list:
# 目标价值设为0,即清仓
order_target_value(stock, 0, ContextInfo, ContextInfo.accid)
print(f"卖出(不在前20%): {stock}")
# 4.2 买入逻辑:对目标列表中的股票进行等权买入
# 获取当前账户总资产
account_info = ContextInfo.get_trade_detail_data(ContextInfo.accid, 'stock', 'account')
if len(account_info) > 0:
total_asset = account_info[0].m_dBalance
# 计算每只股票的目标持仓市值
# 留出少量资金防止手续费不足导致废单
target_value_per_stock = (total_asset * ContextInfo.hold_percent) / len(target_list)
for stock in target_list:
order_target_value(stock, target_value_per_stock, ContextInfo, ContextInfo.accid)
print(f"买入/调仓: {stock}, 目标市值: {target_value_per_stock:.2f}")
关键点说明
-
get_market_data_ex:- 这是 QMT 获取数据最高效的接口。
count=21:因为要计算过去20天的涨幅,我们需要第0天(20天前)和第20天(今天)的数据,所以总共需要取21根K线。dividend_type='front':非常重要。计算收益率必须使用前复权数据,否则分红派息会导致价格跳空,从而计算出错误的暴跌。
-
调仓逻辑 (
rebalance):- 策略的核心在于“持有最强的,卖出不够强的”。
- 代码中的逻辑是:只要股票不在“前20%”的名单里,就卖出。这比单纯“卖出后20%”更符合动量策略的逻辑,因为它能确保你的资金始终集中在表现最好的股票上。
-
交易函数 (
order_target_value):- 使用
order_target_value(目标价值下单)比指定股数下单更方便。它会自动计算需要买入或卖出多少股才能达到设定的金额。 - 如果当前已经持有该股票且市值刚好,它不会重复下单;如果市值不够,它会补仓;如果市值多了,它会减仓。
- 使用
-
运行频率:
- 在 QMT 界面运行或回测时,请将策略的运行周期设置为 “日线”。
- 代码中的
if current_month != ContextInfo.last_month:逻辑保证了虽然每天运行,但只在每个月的第一天触发交易。
使用前准备
- 补充数据:在 QMT 客户端顶部菜单 -> 【数据管理】 -> 【历史行情数据】,下载“沪深300”或您所选板块的日线数据。
- 设置账号:在代码
init函数中,取消ContextInfo.set_account的注释,并填入您的真实资金账号。 - 设置股票池:代码默认使用沪深300成分股,您可以在
init中修改ContextInfo.stock_list为自定义的代码列表。
Q&A
Q: 为什么不直接用 order_target_percent?
A: order_target_percent 在回测中很好用,但在实盘中,如果账户中有非策略持仓(比如你自己手动买的股票),直接用百分比可能会干扰其他持仓。使用 order_target_value 基于总资产计算具体金额通常更稳健。
Q: 如何改成每周调仓?
A: 可以将 handlebar 中的换月判断逻辑修改为判断周。例如使用 timetag_to_datetime 获取当前是周几,如果是周一且上一次交易不是周一,则触发调仓。
Q: 为什么卖出逻辑是“不在目标列表中就卖出”?
A: 题目要求是“卖掉排名后20%,买入排名前20%”。如果只执行这个操作,那么中间60%的股票会一直滞留在账户里,导致资金被占用,无法最大化利用动量效应。标准的动量策略通常是轮动,即始终持有最强的那部分。我的代码实现的是标准的轮动逻辑。