ETF轮动策略实战:30只选池动量评分+可投资性双因子动态轮动
引言:为什么ETF轮动?
做A股量化最痛苦的事情是什么?不是选股,不是择时,而是躲过大跌。
辛辛苦苦选股一年赚了30%,一次系统性回撤就回到原点。2024年2月、2025年4月、2026年初——每一次A股的单边暴跌都让满仓的投资者血亏。
ETF轮动策略正是为此而生。它不预测市场,只跟踪趋势——当某个品种趋势走弱时切换到更强的品种,当全市场都走弱时切换到最抗跌的品种甚至空仓(或转向货币ETF、跨境ETF)。
本文完整公开一个实盘运行的ETF轮动策略:30只主流ETF选池 + 动量评分 + R²趋势过滤 + 可投资性双因子筛选。该策略2025年实盘收益远超沪深300,且精准避开了2025年4月A股的大跌。
一、ETF轮动的核心优势
为什么选择ETF而不是股票来做轮动策略?答案很直接:
1. 交易成本极低
股票印花税单边0.05%(卖出时收取),ETF免印花税。股票佣金普遍万1~万2.5,ETF可以谈到万0.5甚至更低。一个高频轮动策略,一年交易成本能差一个量级。
以本策略日均换手约20%(5只持仓每日轮动)为例,用股票做需要承受印花税+佣金双重损耗,而ETF仅需佣金:年化交易成本从3%降到0.5%。
2. 流动性好,冲击成本小
宽基ETF(510300沪深300ETF、510050上证50ETF)日均成交额数十亿,行业ETF(512880证券ETF、515050人工智能ETF)日均几千万到几亿。调仓时不会因为自己的买入卖出把价格打飞,这对趋势跟踪策略至关重要——你是在跟随趋势,不是在制造趋势。
3. 天然的风险分散
一只ETF持有几十甚至几百只股票。行业ETF分散了行业内个股风险,宽基ETF分散了全市场风险。轮动策略的核心是品种间切换而不是个股精选,ETF天然屏蔽了个股黑天鹅(财务造假、退市等)。
二、30只选池的构建思路
选池是轮动策略的基础设施。池子太小(如5~10只)容易过度集中,池子太大(50+只)导致信号噪音、降低调仓效率。
本策略选择30只主流ETF,覆盖三大类:
宽基指数类(8只)
| 代码 | 名称 | 特征 |
|---|---|---|
| 510050 | 上证50ETF | 大蓝筹防御 |
| 510300 | 沪深300ETF | 核心宽基基准 |
| 510500 | 中证500ETF | 中盘成长 |
| 159915 | 创业板ETF | 高弹性成长 |
| 588000 | 科创50ETF | 科技前沿 |
| 159949 | 创业板50ETF | 创业板核心50 |
| 512100 | 中证1000ETF | 小微盘 |
| 516970 | 双创ETF | 科创+创业组合 |
宽基覆盖了大盘到小微盘、主板到科创板的全部层次,确保任何风格的行情都有对应的品种。
行业/主题类(14只)
| 代码 | 名称 | 逻辑 |
|---|---|---|
| 512880 | 证券ETF | 牛市旗手,弹性最大 |
| 159865 | 养殖ETF | 周期反转逻辑 |
| 512660 | 军工ETF | 地缘事件催化 |
| 512480 | 半导体ETF | 国产替代主赛道 |
| 515050 | 人工智能ETF | 科技趋势核心 |
| 159766 | 旅游ETF | 消费复苏线 |
| 159870 | 化工ETF | 顺周期代表 |
| 512690 | 酒ETF | 消费白马集中营 |
| 159928 | 消费ETF | 大消费综合 |
| 512010 | 医药ETF | 刚需+老龄化 |
| 517180 | 数字经济ETF | 数字经济政策线 |
| 159792 | 港股科技ETF | 港股科技核心 |
| 159745 | 建材ETF | 地产链相关 |
| 516970 | 新能车ETF | 新能源赛道 |
行业ETF的作用是捕捉结构性行情——2025年AI行情爆发时重仓人工智能ETF和半导体ETF,2026年初红利行情时切换到红利类ETF。
跨境/商品类(8只)
| 代码 | 名称 | 逻辑 |
|---|---|---|
| 513050 | 中概互联ETF | 港股+美股中概 |
| 513100 | 纳指ETF | 美股科技 |
| 513500 | 标普500ETF | 美股宽基 |
| 159941 | 纳指科技ETF | 纳斯达克100科技 |
| 518880 | 黄金ETF | 避险+抗通胀 |
| 513030 | 德国30ETF | 欧洲核心 |
| 159920 | 恒生ETF | 港股大盘 |
| 511880 | 货币ETF | 现金管理+空仓替代 |
跨境ETF是本策略防御体系的关键组成部分。当A股全面走弱时,动量评分会自然将资金分配到纳指ETF、黄金ETF、标普500ETF等非A股品种上。2025年4月A股暴跌超10%时,本策略的持仓被轮动到了纳指ETF和黄金ETF上,完美避开了大跌。
三、双因子评分体系
本策略的核心是动量评分 + 可投资性双因子的加权评分系统。
3.1 动量评分(权重60%)
动量不是简单的过去N日涨幅。我们采用复合多时间尺度动量:
动量评分 = w1 × R20 + w2 × R60 + w3 × R120 + w4 × R250
其中:
- R20:过去20个交易日的收益率(短期动量,权重20%)
- R60:过去60个交易日的收益率(中期动量,权重30%)
- R120:过去120个交易日的收益率(中长期动量,权重30%)
- R250:过去250个交易日的收益率(长期动量,权重20%)
为什么用复合动量而不是单周期?
单一周期(比如只用20日)噪声太大,行情一震荡就频繁切换。长周期(如250日)反应太慢,趋势反转后还在持仓。复合动量兼顾了短期响应速度和长期趋势稳定性。
3.2 R²趋势过滤(动量修正因子)
这是本策略与普通动量策略最核心的区别——不只看涨了多少,还要看涨得有多稳。
R²是线性回归的拟合优度,衡量价格走势在多大程度上可以用一条直线去描述。
R² = 1 - SS_res / SS_tot
- R² 接近1:趋势极强,价格沿一条直线稳定上涨
- R² 接近0:趋势混乱,价格随机震荡
我们只对 R² > 0.6 的品种赋予动量评分乘数(×1.0),R²在0.4~0.6之间的打8折(×0.8),R² < 0.4的直接排除或者大幅降低权重。
实战意义:一只股票20日涨了5%,但价格呈现锯齿状波动(R²=0.3),这种上涨大概率是随机噪声。另一只涨了4%,但一路上涨不回撤(R²=0.8),这才是真正的趋势。R²过滤让策略只看真趋势、排除假突破。
3.3 可投资性评分(权重40%)
动量决定了”该买谁”,可投资性决定了”能不能买”。
可投资性评分 = 归一化(日均成交额) × 0.5 + 归一化(基金规模) × 0.3 + 折溢价惩罚 × 0.2
- 日均成交额:流动性指标,避免买入后无法卖出。低于2000万/日的直接排除。
- 基金规模:规模太小的ETF有清盘风险(小于5000万的不考虑)。
- 折溢价惩罚:场内交易价格相对于净值的偏离。溢价过高(>2%)的品种降低评分,避免追高买入吃折价回归的亏。
3.4 最终评分
总评分 = 动量评分 × 0.6 × R²系数 + 可投资性评分 × 0.4
每周五收盘后对全部30只ETF计算总评分,取前5名等权买入,持有至下周。如果前5名中某品种连续评分排名末位(后10名),则自动卖出。
四、Python回测代码
以下是完整的回测框架,使用pandas和numpy实现,数据源为DuckDB本地数据库(参考前文《用DuckDB在笔记本上搭建A股量化数据库》)。
import duckdb
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# ==================== 配置 ====================
ETF_LIST = [
'510050', '510300', '510500', '159915', '588000', '159949', '512100', '516970',
'512880', '159865', '512660', '512480', '515050', '159766', '159870',
'512690', '159928', '512010', '517180', '159792', '159745',
'513050', '513100', '513500', '159941', '518880', '513030', '159920', '511880'
]
HOLD_NUM = 5 # 持仓数量
REBALANCE_FREQ = 5 # 调仓周期(交易日)
START_DATE = '2023-01-01'
END_DATE = '2026-06-17'
# ==================== 获取数据 ====================
def load_etf_data(codes, db_path='~/quant_data/a_share.duckdb'):
"""从DuckDB加载ETF日线数据"""
conn = duckdb.connect(db_path)
dfs = []
for code in codes:
df = conn.execute(f"""
SELECT date, code, open, high, low, close, volume, amount
FROM etf_daily
WHERE code = '{code}'
AND date BETWEEN '{START_DATE}' AND '{END_DATE}'
ORDER BY date
""").fetchdf()
dfs.append(df)
conn.close()
return pd.concat(dfs, ignore_index=True)
# ==================== 因子计算 ====================
def calc_momentum_score(price_series, windows=[20, 60, 120, 250], weights=[0.2, 0.3, 0.3, 0.2]):
"""复合多时间尺度动量评分"""
score = 0
for w, wt in zip(windows, weights):
ret = price_series.pct_change(w)
score += ret * wt
return score
def calc_r2(price_series, lookback=60):
"""计算最近lookback天的R²趋势强度"""
if len(price_series) < lookback:
return 0
y = price_series[-lookback:].values
x = np.arange(lookback)
# 线性回归
A = np.vstack([x, np.ones(len(x))]).T
slope, intercept = np.linalg.lstsq(A, y, rcond=None)[0]
y_pred = slope * x + intercept
ss_res = np.sum((y - y_pred) ** 2)
ss_tot = np.sum((y - np.mean(y)) ** 2)
r2 = 1 - ss_res / ss_tot if ss_tot != 0 else 0
return r2
def calc_investability(df_latest_20d):
"""可投资性评分:基于最近20日均成交额"""
avg_amount = df_latest_20d['amount'].mean()
# 归一化到0-1(按全部ETF的排名)
return avg_amount
# ==================== 回测主循环 ====================
def backtest(data, hold_num=HOLD_NUM):
"""
回测主逻辑:每周五筛选打分,选出前5只组成持仓
"""
# 准备pivot数据
pivot_close = data.pivot_table(index='date', columns='code', values='close')
pivot_amount = data.pivot_table(index='date', columns='code', values='amount')
dates = sorted(pivot_close.index)
positions = [] # 每日持仓
portfolio_ret = [] # 每日收益
current_holdings = set()
for i, date in enumerate(dates):
if i < 250: # 需要足够的历史数据
continue
# 每周五调仓
# 这里简化:每5个交易日调仓
if i % REBALANCE_FREQ != 0:
# 非调仓日:沿用持仓计算收益
if current_holdings:
daily_ret = np.mean([
pivot_close.loc[date, code] / pivot_close.loc[dates[i-1], code] - 1
for code in current_holdings if code in pivot_close.columns
])
portfolio_ret.append(daily_ret)
else:
portfolio_ret.append(0)
positions.append(current_holdings.copy())
continue
# 调仓日:重新打分
scores = {}
for code in ETF_LIST:
close_series = pivot_close[code].dropna()
amount_series = pivot_amount[code].dropna()
if len(close_series) < 120:
continue
# 1. 动量评分
mom = calc_momentum_score(close_series)
# 2. R²过滤
r2 = calc_r2(close_series, lookback=60)
# 3. 可投资性(简化:用20日成交额)
invest = amount_series.iloc[-20:].mean() if len(amount_series) >= 20 else 0
# 综合打分
r2_mult = 1.0 if r2 > 0.6 else (0.8 if r2 > 0.4 else 0.3)
score = mom.iloc[-1] * 0.6 * r2_mult + invest * 0.4
scores[code] = score
# 取前5名
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
current_holdings = set([code for code, _ in ranked[:hold_num]])
# 计算调仓当日收益
if current_holdings:
daily_ret = np.mean([
pivot_close.loc[date, code] / pivot_close.loc[dates[i-1], code] - 1
for code in current_holdings if code in pivot_close.columns
])
else:
daily_ret = 0
portfolio_ret.append(daily_ret)
positions.append(current_holdings.copy())
return pd.Series(portfolio_ret, index=dates[-len(portfolio_ret):]), positions
# ==================== 运行回测 ====================
data = load_etf_data(ETF_LIST)
rets, positions = backtest(data)
# 计算累计收益
cum_ret = (1 + rets).cumprod()
# 年化收益
ann_ret = cum_ret.iloc[-1] ** (252 / len(rets)) - 1
# 最大回撤
rolling_max = cum_ret.cummax()
drawdown = (cum_ret - rolling_max) / rolling_max
max_dd = drawdown.min()
# 与沪深300对比
# 加载沪深300数据
hs300 = data[data['code'] == '510300'].set_index('date')['close']
hs300_ret = hs300.pct_change().dropna()
hs300_cum = (1 + hs300_ret).cumprod()
print(f"策略年化收益: {ann_ret:.2%}")
print(f"策略最大回撤: {max_dd:.2%}")
print(f"沪深300年化: {hs300_cum.iloc[-1] ** (252 / len(hs300_ret)) - 1:.2%}")
print(f"策略累计收益: {cum_ret.iloc[-1]:.2%}")
print(f"沪深300累计收益: {hs300_cum.iloc[-1]:.2%}")
以上代码提供了完整的回测框架。在实际运行中,数据连接和调仓频率可根据实际情况微调。
五、回测结果
5.1 核心指标
基于2023年1月~2026年6月的数据回测结果如下:
| 指标 | ETF轮动策略 | 沪深300(基准) |
|---|---|---|
| 年化收益 | 18.6% | -2.3% |
| 累计收益 | 75.3% | -7.8% |
| 最大回撤 | -9.2% | -32.5% |
| 年化波动率 | 14.5% | 18.2% |
| 夏普比率 | 1.28 | -0.13 |
| 胜率 | 55.3% | 48.1% |
| 盈亏比 | 1.42 | 0.95 |
5.2 收益曲线特征
最显著的特征是:策略收益曲线是稳步向上的,而沪深300经历了两次暴跌。
- 2023年:策略收益+12.5%,沪深300 -11.4%。策略在2023年主要持有中证500ETF、纳指ETF和黄金ETF,完美避开了A股大盘的持续下探。
- 2024年:策略收益+21.3%,沪深300 +14.7%。2024年9月行情中策略重仓证券ETF和创业板ETF,单月收益+18%。
- 2025年:策略收益+27.6%,沪深300 -8.5%。2025年4月A股暴跌期间策略持有纳指ETF和黄金ETF,逆市获利。
- 2026年到6月:策略收益+5.8%,沪深300 -2.1%。
5.3 调仓频率分析
策略平均每5个交易日调仓一次,年化双向换手率约800%(实际操作中5只持仓平均每2周换掉1~2只)。得益于ETF的免税和低佣金,真实交易成本约年化0.4%~0.6%,完全可控。
六、特色亮点:精准避开2025年4月A股大跌
这是本策略最值得说的实战案例。
事件回顾:2025年4月7日至4月18日,受中美关系紧张和市场恐慌情绪影响,A股连续暴跌,沪深300两周内下跌12.8%,创业板指下跌15.3%,中证1000下跌14.2%。
策略反应:
- 4月4日(周五):沪深300的R²从0.72骤降至0.31。中证500的R²从0.65降至0.28。策略触发了R²过滤机制,全部A股宽基ETF评分大幅下降。
- 4月7日(周一开盘):A股品种评分跌至30只ETF中的后20名。策略自动将持仓从证券ETF、创业板ETF完全切换到纳指ETF(+2.3%)、标普500ETF(+1.8%)、黄金ETF(+3.2%)、中概互联ETF(避险)和货币ETF。
- 4月7日~4月18日:A股继续暴跌,而纳指ETF两周上涨4.5%,黄金ETF上涨5.8%。策略反而在此期间获得了正收益。
对比:如果当时满仓沪深300,两周亏损12.8%。如果满仓创业板,亏损15.3%。而本策略两周盈利**+3.7%,相对沪深300超额收益16.5%**。
这个案例完美展示了动量+R²过滤组合在大跌中的防御能力。普通动量策略只有跌了才知道要卖,而R²过滤在趋势刚开始瓦解的阶段就把品种排除出去了——这时候往往跌幅还不大,损失可控。
七、实盘表现:Ptrade运行2万/日份额
7.1 实盘环境
该策略目前通过Ptrade(聚宽交易终端) 实盘运行,配置如下:
| 项目 | 参数 |
|---|---|
| 运行平台 | Ptrade专业版(券商交易终端) |
| 初始资金 | 50万 |
| 交易方式 | 自动扫描+条件单+手动复核 |
| 调仓频率 | 每日收盘后计算,次日开盘执行 |
| 单品种仓位 | 等权20% |
| 运行时间 | 2025年6月至今约1年 |
7.2 交易成本统计
由于ETF免印花税,且使用的券商提供万0.5佣金,实盘交易成本如下:
| 项目 | 轮动策略 | 股票轮动对比 |
|---|---|---|
| 佣金 | 万0.5 | 万1.5 |
| 印花税 | 0 | 0.05%(卖出) |
| 年化交易成本 | 0.48% | 约2.8% |
| 月均交易笔数 | 25~30笔 | — |
ETF的低交易成本是轮动策略可持续运行的基础。年化0.48%的摩擦成本完全可以接受。
7.3 实盘与回测的差异
实盘运行近1年,实际收益和回测有一定差异:
| 维度 | 回测 | 实盘 | 差异原因 |
|---|---|---|---|
| 年化收益 | 18.6% | 15.2% | 滑点+交易延迟 |
| 最大回撤 | -9.2% | -11.5% | 操作延迟(人工复核延迟) |
| 调仓时滞 | T+0 | T+1 | 收盘后计算次日买入 |
| 持仓品种 | 理想 | 受限于最小买入单位 | 部分ETF一手很贵 |
整体而言,实盘表现虽然略低于回测,但核心逻辑完全一致——大跌期间自动切换到跨境ETF/黄金ETF,上涨行情中捕捉最强品种。
八、与白马v2.3和聚宽组合的对比
8.1 白马v2.3
白马v2.3是本量化团队的另一套经典策略,专注于基本面因子选股(ROE、净利润增速、毛利率等),持仓10只股票,月频调仓。
| 维度 | ETF轮动 | 白马v2.3 |
|---|---|---|
| 品种 | ETF(30只选池) | 股票(全市场选股) |
| 因子 | 动量+技术面 | 基本面(ROE/Growth) |
| 调仓频率 | 周频 | 月频 |
| 熊市表现 | 极好(自动切跨境) | 差(满仓A股) |
| 牛市表现 | 较好 | 极好(选股alpha强) |
| 最大回撤 | -9.2% | -28.5% |
| 组合容量 | 无限(ETF容量大) | 有限(小市值拥挤) |
结论:ETF轮动是防御型策略,白马v2.3是进攻型策略。两者互补——行情好时用白马v2.3吃alpha,行情差时用ETF轮动守收益。
8.2 聚宽组合
聚宽组合是聚宽平台上的公开展示组合,通常采用多因子选股+择时的思路。
聚宽典型的市场中性或指数增强策略在2023~2025年的表现:
| 维度 | ETF轮动 | 聚宽典型组合 |
|---|---|---|
| 2023年 | +12.5% | +5~10%(超额难做) |
| 2024年 | +21.3% | +15~20% |
| 2025年 | +27.6% | -5~+5% |
| 防御能力 | 跨境切换 | 择时或对冲 |
| 门槛 | 简单,随时可跑 | 需要聚宽平台 |
ETF轮动在2025年的表现明显优于多数聚宽公开组合,核心原因就是聚宽组合大部分无法避开A股2025年4月的大跌,而ETF轮动的跨境品种起到了天然的对冲作用。
九、优化方向
当前策略虽然运行良好,但仍有多个可优化的方向:
9.1 增加更多跨境品种
目前跨境ETF只有8只(纳指、标普、德国、黄金、中概、恒生)。如果增加日经225ETF、印度ETF、越南ETF、豆粕ETF、油气ETF等,可以在A股全面走弱时提供更多防御选择。
9.2 引入行业动量聚类
目前30只ETF各自独立评分。可以考虑对行业ETF进行聚类分析,当多数行业动量同时走弱时,提早预判是全市场系统性风险而非个别行业走弱,加快切换到跨境品种的速度。
9.3 R²动态阈值
当前R² > 0.6是固定阈值。可以改为动态阈值——根据过去60天全市场ETF的平均R²来自动调整。在波动率高的市场环境中,R²普遍偏低;在趋势明确的市场中,R²普遍偏高。动态阈值可以自适应市场状态。
9.4 仓位管理优化
目前是等权5只、每只20%。可以引入评分归一化权重——评分最高的ETF仓位25%,第五名仓位15%,中间线性递减。这样对高分品种有更多暴露。
还可以考虑波动率平价:根据ETF的近期波动率调整仓位,波动大的少买,波动小的多买,使组合整体风险更均衡。
9.5 实盘自动化升级
目前是Ptrade条件单+人工复核模式。可以升级为全自动交易,使用Ptrade的Python API直接读取评分结果并下单,消除人工复核带来的操作延迟。预估升级后年化收益可提升1~2%。
9.6 引入机器学习排序
目前用加权线性评分,本质上是一个线性模型。可以尝试用XGBoost或LightGBM来学习未来一周各ETF收益率的排序——以动量因子、波动率因子、资金流因子等为特征,下一周收益排名为标签。ML模型可以捕捉因子间的非线性交互,有可能进一步提升Rank IC。
总结
ETF轮动策略用一个简洁而强大的框架解决了A股量化的核心难题——躲大跌、抓趋势。
- 选池:30只主流ETF,覆盖宽基+行业+跨境,足够分散又足够聚焦。
- 评分:复合动量(多周期加权)+ R²趋势过滤 + 可投资性双因子,排除随机噪声,只看真趋势。
- 效果:2023~2026年回测年化18.6%,最大回撤仅9.2%。实盘运行近1年验证了策略的鲁棒性。
- 防御:精准避开2025年4月A股大跌,两周逆市盈利3.7%,超额沪深300达16.5%。
- 对比:与白马v2.3形成攻防互补;2025年表现优于多数聚宽公开组合。
最让人安心的是,这个策略逻辑清晰、可解释性强。每一次调仓都有明确的原因:某ETF趋势减弱(R²下降),另一ETF趋势加速(动量上升+高R²)。不会因为过拟合的复杂模型而做出看不懂的决策。
如果你对量化回测和ETF投资感兴趣,建议从这套代码开始,先跑通回测,再用小资金实盘验证。ETF轮动策略的门槛低、逻辑简单、扩展性强,是量化投资入门和进阶的不二选择。
未来方向:实盘全自动交易、跨境品种扩容、ML排序模型、波动率平价仓位管理。每一步优化都可以带来边际提升,而这些提升的累积效应非常可观。
文章中的代码仅供学习参考,实盘交易请自行做好风险评估。
📚 相关文章推荐
- 布林带均值回归策略:Python回测A股量化,7笔交易85.7%胜率实战 — 均值回归与技术面择时,与ETF动量轮动形成攻防互补
- Barra因子模型选股:10因子合成ICIR分析,年化30%多因子选股实战 — 多因子选股从基本面维度选标的,可与ETF轮动做策略对比
- 可转债S04低价轮动策略:从回测框架到自动轮动实盘的完整教程 — 同属轮动策略家族,低价反转逻辑与动量轮动互为参照