为了表示歉意,还是要继续分享一些干货回馈大家,感谢大家的理解不离不弃。
不少同学对ETF动量轮动策略感兴趣, 而网上一搜索要么是 聚宽策略, 要么是必须加入XX知识星球里才能下载源代码学习,多多少少有些套路。
对于新手来说, 不管是对于聚宽策略,改成qmt代码有一定难度。 知识星球虽然我也有,但我不强推, 不想因为一些基础入门的知识成为大家学习量化的拦路虎 。 希望分享的内容真正能够帮助到大家, 如果大家觉得有所收获,点点赞给个关注分享什么的, 就是对我最大的鼓励。
回到正题:
之前文章写过 tushare实现ETF动量趋势分析 , 这个代码其实对于非windows电脑想观察某些指数(包括宽基ETF、行业ETF)趋势强度有所帮助, 但真正要进行量化交易还是有一些问题, 里面设置的ETF有点多,且一些ETF存在强关联性。 比如科创- 芯片, 创业-电池等有非常强的相关性,毕竟成分股有大量重叠。
一般来说,我们配置ETF轮动,最好保持低关联性,这样轮动才有真正的价值。 比如代码中选择 黄金抗通胀、 美股、 A股成长股、A股价值股, 相关性比较小。
ETF动量轮动策略,怎么选择低关联性的ETF代码呢, 我们可以使用皮尔逊相关系数来衡量不同ETF之间的相关性,从而构建动量轮动策略。 根据相关系数的大小,识别出短期内表现相似或呈负相关的ETF,然后选出对应的ETF。
皮尔逊相关系数怎么计算, 之前我写过一篇文章 怎么分析2只股票的相关度 , 感兴趣可以看看,思路大差不差。
另外, 我们做量化交易, 一定要客观实际,尽量抛开一些过拟合的想法。比如说 我们知道最近1、2年人工智能涨得好,那我重心配置 通信、芯片、科创等ETF。 这其实带了上帝视角,虽然23年以来人工智能涨得好,这种配置其实带有前瞻性, 如果有这样的前瞻意识,代码中进行对应的ETF代码也行,收益会更好。文中的ETF代码可以调整。
这里说明下,我提供的例子借鉴了网上聚宽上的代码想法, QMT实际例子网上公开确实有点少。就当简单做做科普
代码细节我这里就不多做解释了, 我让AI辅助帮我写的, AI写QMT问题还是存在一些的,我纠正了下方法就成文了。 然后放入QMT内置编辑器做回测。 回测了下,收益率并不算太高。
#encoding:gbk
'''
迅投QMT ETF动量轮动策略
作者:子晓聊技术--AI辅助编程
时间:2025-10-24
策略原理:每日计算ETF池中各标的的动量得分,全仓持有得分最高的ETF
动量得分 = 年化收益趋势 × 趋势确定性(R平方)
参考:聚宽年化30%+的ETF轮动策略逻辑
'''
import pandas as pd
import numpy as np
def init(ContextInfo):
# 初始化配置
ContextInfo.account = '' # 账户号,实盘时填写
ContextInfo.account_type = 'STOCK' # 账户类型
ContextInfo.capital = 100000 # 初始资金
ContextInfo.start='20250101 00:00:00'
#结束时间
ContextInfo.end='20251023 00:00:00'
# ETF投资池配置
ContextInfo.etf_pool = [
'518880.SH', # 黄金ETF(大宗商品抗通胀)
'513100.SH', # 纳指ETF(海外科技股代表)
'159915.SZ', # 创业板ETF(A股成长股)
'510180.SH' # 上证180ETF(A股价值股)
]
ContextInfo.buy_ratio=1
ContextInfo.sell_ratio=0
# 动量计算参数
ContextInfo.m_days = 25 # 动量计算周期(25日)
# 订阅行情数据
ContextInfo.set_universe(ContextInfo.etf_pool)
print('ETF动量轮动策略初始化完成')
print('ETF池:', ContextInfo.etf_pool)
print('动量计算周期:', ContextInfo.m_days, '天')
def handlebar(ContextInfo):
'''每根K线触发的主逻辑'''
d=ContextInfo.barpos
#获取当前K线日期
#精确到分钟小周期,不然有未来函数
today_1=timetag_to_datetime(ContextInfo.get_bar_timetag(d),'%Y%m%d%H%M%S')
print(today_1,"当前时间*****")
# 获取当前持仓
hold_stock = get_position(ContextInfo, ContextInfo.account, ContextInfo.account_type)
if hold_stock.shape[0] > 0:
current_hold = hold_stock['证券代码'].tolist()[0] # 全仓只持有一个ETF
else:
current_hold = None
# 计算各ETF动量得分并排序
rank_list = get_rank(ContextInfo, today_1)
if not rank_list: # 检查排名列表是否为空
print("动量得分计算异常,跳过本次调仓")
return
# 获取动量得分最高的ETF
target_etf = rank_list[0]
print(f"动量排名结果: {rank_list}")
print(f"目标ETF: {target_etf}, 当前持仓: {current_hold}")
# 交易逻辑:如果目标ETF与当前持仓不同,则调仓
if current_hold != target_etf:
# 先清空现有持仓
if current_hold:
order_target_percent(current_hold, 0, ContextInfo, ContextInfo.account)
print(f'卖出 {current_hold}')
# 买入目标ETF
order_target_percent(target_etf, 1.0, ContextInfo, ContextInfo.account) # 全仓买入
print(f'买入 {target_etf}')
else:
print(f'继续持有 {current_hold}')
def get_rank(ContextInfo, today_1):
'''
计算ETF动量得分并排序
动量得分 = 年化收益趋势 × 趋势确定性(R平方)
返回:按得分从高到低排序的ETF列表
'''
import math
score_list = []
for etf in ContextInfo.etf_pool:
try:
start = str(ContextInfo.start)[:8]
#print(start)
# 获取历史数据
hist_data = ContextInfo.get_market_data_ex(
fields=['close'],
stock_code=[etf],
period='1d',
start_time= start,
end_time=today_1,
fill_data=True,
subscribe=False
)
#print(hist_data)
if etf not in hist_data or hist_data[etf] is None:
print(f"{etf} 数据获取失败")
continue
df = hist_data[etf]
#print(df)
if df is None or len(df) < ContextInfo.m_days:
print(f"{etf} 数据量不足")
continue
# 使用收盘价计算动量
closes = df['close'].values
if len(closes) < ContextInfo.m_days:
continue
# 取最近m_days个数据
closes = closes[-ContextInfo.m_days:]
# 计算对数价格序列
log_prices = np.log(closes)
x = np.arange(len(log_prices))
# 线性回归计算斜率(收益趋势)
slope, intercept = np.polyfit(x, log_prices, 1)
# 计算年化收益率
annualized_returns = math.pow(math.exp(slope), 250) - 1
# 计算R平方(趋势确定性)
y_pred = slope * x + intercept
y_mean = np.mean(log_prices)
ss_tot = np.sum((log_prices - y_mean)**2)
ss_res = np.sum((log_prices - y_pred)**2)
if ss_tot == 0:
r_squared = 0
else:
r_squared = 1 - (ss_res / ss_tot)
# 动量得分 = 年化收益 × R平方
score = annualized_returns * r_squared
score_list.append((etf, score))
print(f"{etf}: 年化收益{annualized_returns:.3f}, R平方{r_squared:.3f}, 得分{score:.3f}")
except Exception as e:
print(f"计算{etf}动量得分出错: {str(e)}")
continue
if not score_list:
return []
# 按得分从高到低排序
score_list.sort(key=lambda x: x[1], reverse=True)
return [item[0] for item in score_list]
def get_position(ContextInfo, accountid, datatype):
'''获取当前持仓信息'''
positions = get_trade_detail_data(accountid, datatype, 'position')
data = pd.DataFrame()
if len(positions) > 0:
df = pd.DataFrame()
for dt in positions:
df_temp = pd.DataFrame({
'证券代码': [dt.m_strInstrumentID + '.' + dt.m_strExchangeID],
'证券名称': [dt.m_strInstrumentName],
'持仓量': [dt.m_nVolume],
'可用数量': [dt.m_nCanUseVolume],
'成本价': [dt.m_dOpenPrice],
'市值': [dt.m_dInstrumentValue]
})
data = pd.concat([data, df_temp], ignore_index=True)
return data
备注说明下,这个只是QMT回测代码, 这个收益率实盘还需要改进。如果需要实盘,还需要修改下单代码,以及部分逻辑调整。