12_行业轮动 · 回测实录
10只行业ETF按5日涨幅排序取最强2只,每周第一个交易日调仓。
策略类型 :ETF轮动 / 周频
回测批次 :2026-10-04(本地 QuantStudio 回测引擎)
数据来源 :QuantStudio 本地行情库(etf_daily / stock_daily)
一、策略思路
相比01号策略,这版做了两处调整:一是把回看窗口从20日缩短到5日,反应更快,更贴近"行业风口"的短周期特征;二是调仓频率从每5个交易日改成每周一次,用固定的日历节奏换手,而不是机械地数交易日。行业ETF之间相关性低、轮动特征明显,是动量策略最经典的试验场。
关键参数
| 参数 | 取值 |
|---|---|
LOOKBACK |
5 |
TOP_N |
2 |
EXPOSURE |
0.98 |
二、回测设置
| 项目 | 设置 |
|---|---|
| 策略文件 | 12_行业轮动.py |
| 回测区间 | 2024-01-01 ~ 2026-09-02 |
| 初始资金 | 100000 元 |
| 佣金费率 | 0.00035(最低 5.0 元) |
| 印花税率 | 0.001 |
| 过户费率 | 1e-05 |
| 滑点 | 0.0 |
| 撮合价格 | close |
| 基准 | 000300 |
| 交易日数 | 647 |
三、回测结果

本图有两条策略线:灰色虚线是回测引擎的原始记录,红色实线是剔除数据污染后的校正净值。请只看红线------灰线里那条垂直往上跳的台阶,不是策略赚的,是数据库里一只房地产ETF的价格断层凭空"送"的。
⚠️ 重要:本策略的原始回测净值受数据断层污染,不可直接引用。
数据库中 512200(房地产ETF)在 2024-08-12 存在唯一一处价格断层:当日
preClose与前一日收盘的比值约为 2.79 ,即该标的的价格在这一天整体跳到了约 2.79 倍。策略恰好在这一天卖出该标的,成交价被相应抬高,等于向账户凭空注入了一大笔现金。其结果是:此后账户净值被整体放大约 1.86 倍 。由于这笔注入是一次性的,策略的日收益率序列不变,因此正确的还原方式是把断层日之后的净值整体除以 1.86。
下表中「记录值」为引擎原始输出,「校正值」为剔除该污染后的结果。请以校正值为准。
绩效指标
| 指标 | 记录值 | 校正值 |
|---|---|---|
| 累计收益 | 113.44% | 14.95% |
| 年化收益 | 34.04% | 5.53% |
| 最大回撤 | 35.03% | 35.03% |
| 年化波动率 | 62.17% | 31.98% |
| 夏普比率 | 0.519 | 0.328 |
| 索提诺比率 | 1.438 | 0.493 |
| 卡玛比率 | 0.972 | 0.158 |
| 回合胜率 | 49.30% | --- |
| 日胜率 | 48.22% | --- |
| 盈亏比 | 137.7% | --- |
| 盈利回合数 | 106 | --- |
| 亏损回合数 | 109 | --- |
| 平均持仓天数 | 230.2 | --- |
| 成交笔数 | 704 | --- |
「盈亏比」= 平均盈利 ÷ 平均亏损 > 100%,表示平均每笔赚的比亏的多。
「回合」指一次完整的买入→卖出(
round_trips),「成交笔数」是买卖双方分开计数的原始成交记录条数。
⚠️ 本窗口基准数据不完整 :引擎的基准序列尾部为占位填充值,据此算出的沪深300收益为 0.00%,该数字不可信;净值图中的基准曲线已截断至有效区间(至 2026-07-31)。因此本策略的 α、β、超额收益、信息比率等相对指标一律不可引用,只能用绝对收益与回撤来判断。
分年度收益(校正后)
| 年份 | 区间收益 | 交易日 |
|---|---|---|
| 2024 | 2.70% | 242 |
| 2025 | 5.60% | 243 |
| 2026 | 6.63% | 162 |
四、交易明细
⚠️ 本节所有金额为引擎记录口径 ,已被上述数据断层整体放大约 1.86 倍,绝对金额不可直接引用(尤其盈利榜首那几笔)。盈亏的方向与回合之间的相对排序仍然有效。
- 完整交易回合数:215 个(盈利 106 / 亏损 109)
- 单笔平均盈亏:+544.84 元 ;最大单笔盈利 +57605.44 元 ;最大单笔亏损 -23721.49 元
- 持仓天数:平均 230.2 天 ,最长 657 天 ,最短 6 天
盈利最多的 5 个回合
| 买入日 | 卖出日 | 标的 | 盈亏(元) | 持仓天数 |
|---|---|---|---|---|
| 2024-05-06 | 2024-08-19 | 512200.SH |
+57605.44 | 105 |
| 2024-05-06 | 2024-08-12 | 512200.SH |
+28684.89 | 98 |
| 2025-02-24 | 2026-04-27 | 515880.SH |
+28161.04 | 427 |
| 2024-07-08 | 2024-10-14 | 512200.SH |
+22913.07 | 98 |
| 2025-02-10 | 2026-06-01 | 512480.SH |
+20449.75 | 476 |
亏损最多的 5 个回合
| 买入日 | 卖出日 | 标的 | 盈亏(元) | 持仓天数 |
|---|---|---|---|---|
| 2025-02-24 | 2026-07-20 | 512480.SH |
-23721.49 | 511 |
| 2025-02-24 | 2026-08-03 | 512480.SH |
-19930.86 | 525 |
| 2024-06-03 | 2024-11-18 | 512660.SH |
-13533.28 | 168 |
| 2024-04-29 | 2024-10-14 | 512690.SH |
-13109.93 | 168 |
| 2025-07-28 | 2026-06-15 | 515220.SH |
-10513.28 | 322 |
五、完整回测代码
文件名:
12_行业轮动.py(共 111 行)引擎:QuantStudio 本地回测;全部为公开 API,无平台专有调用。
python
# -*- coding: utf-8 -*-
"""
12_行业轮动策略 (QuantStudio 本地回测版)
========================================
核心逻辑:10只行业ETF按5日涨幅排序,取最强2只,每周第一个交易日调仓。
周频行业轮动,降低换手率,捕捉行业中期趋势。
"""
import numpy as np
# 10只行业ETF
SECTOR_ETF_POOL = [
'512880.SS', # 证券
'512800.SS', # 银行
'512690.SS', # 酒(消费)
'512170.SS', # 医疗
'512480.SS', # 半导体
'515880.SS', # 通信
'512660.SS', # 军工
'516160.SS', # 新能源
'512200.SS', # 房地产
'515220.SS', # 煤炭
]
LOOKBACK = 5 # 动量回看天数
TOP_N = 2 # 持仓数量
EXPOSURE = 0.98
def initialize(context):
set_benchmark('000300.SS')
g.last_rebalance_week = None
run_daily(context, trade, time='09:30')
log.info('12_行业轮动初始化: 行业ETF=%d只, 回看=%d日, Top%d, 周频调仓',
len(SECTOR_ETF_POOL), LOOKBACK, TOP_N)
def _get_momentum(code):
try:
h = get_history(LOOKBACK + 1, '1d', field=['close'],
security_list=code, fq='pre', include=False, is_dict=True)
if h is None or code not in h:
return None
closes = np.asarray(h[code]['close'], dtype=float)
closes = closes[np.isfinite(closes)]
if len(closes) < LOOKBACK + 1 or closes[0] <= 0:
return None
return float(closes[-1] / closes[0] - 1.0)
except Exception:
return None
def _is_first_trading_day_of_week(context):
"""判断是否为本周第一个交易日(用ISO周编号去重)"""
try:
dt = context.current_dt
iso_cal = dt.isocalendar()
current_week = (iso_cal[0], iso_cal[1])
if g.last_rebalance_week is None or current_week != g.last_rebalance_week:
g.last_rebalance_week = current_week
return True
return False
except Exception:
# 兜底:每5个交易日调仓
if not hasattr(g, 'day_counter'):
g.day_counter = 0
g.day_counter += 1
return g.day_counter % 5 == 0
def trade(context):
# 只在每周第一个交易日调仓
if not _is_first_trading_day_of_week(context):
return
# 计算所有行业ETF的动量
scores = {}
for code in SECTOR_ETF_POOL:
mom = _get_momentum(code)
if mom is not None:
scores[code] = mom
if not scores:
log.info('无有效动量数据,跳过')
return
# 取TopN
ranked = sorted(scores.keys(), key=lambda c: scores[c], reverse=True)
target = ranked[:TOP_N]
# 卖出不在目标中的
held = list(context.portfolio.positions.keys())
for code in held:
if code not in target:
order_target_value(code, 0)
log.info('行业轮动卖出 %s (动量=%.2f%%)', code, scores.get(code, 0) * 100)
# 等权买入
if not target:
return
total_value = context.portfolio.total_value
per_value = total_value * EXPOSURE / len(target)
for code in target:
order_target_value(code, per_value)
log.info('行业轮动买入 %s (动量=%.2f%%, 目标市值=%.0f)',
code, scores[code] * 100, per_value)
def handle_data(context, data):
pass
六、风险提示
5日动量非常短,容易把一两天的消息面波动误判成趋势,在行业快速切换的市场里会来回打脸。周频调仓意味着信号有一周的执行延迟。另外本策略标的池中的房地产ETF存在数据质量问题,阅读收益数字时须以校正值为准。
免责声明:本文所有数字来自本地历史回测,回测收益不代表未来表现,也不构成任何投资建议或个股/基金推荐。策略代码仅供研究学习,实盘执行还需额外考虑滑点、冲击成本、流动性、涨跌停与平台差异等因素。投资有风险,决策需谨慎。