摘要: 本文是“行业ETF轮动”系列专题的第四篇。在见证了行业轮动策略的高收益与高回撤后,我们将为它量身打造一套加强版风控体系。文章将从四个维度展开:加强版动量过滤、波动率阈值过滤、最大回撤硬止损、以及行业集中度控制。每一层风控都配有详细的Python实现代码,并逐层叠加回测,让你直观感受风险指标的改善。最终,你将拥有一套既能保持进攻性,又能有效约束下行风险的完整策略框架。
大家好,我是你们的老朋友。上一篇文章,我们亲手把行业ETF轮动策略跑了起来。净值曲线很漂亮,最高点让人心潮澎湃,但紧接着那一段近乎垂直的下跌,估计也让不少人心里一凉。
没错,这就是行业轮动的真实面目:高收益的背后,必然伴随着高波动与高回撤。 如果不加以约束,这把锋利的矛很可能在伤敌之前,先伤到自己。
今天这篇文章只有一个目标:给这把矛配上坚固的盾。 我们将为行业ETF轮动策略逐层添加四道风控防线,每一道防线都针对一个具体的风险来源。最终,你会看到回撤被显著压缩,而收益并未受到致命影响。
一、风控第一层:加强版动量过滤
1.1 基础动量过滤的局限
在基础策略中,我们的风控规则是:如果动量排名第一的ETF其动量值为负数,就切换到国债ETF避险。这条规则的逻辑是“所有行业都在跌,我不玩了”。
但问题在于,有时虽然动量第一的行业是正数,但它可能只是“勉强为正”,比如动量值仅为0.005(0.5%)。这种微弱的上涨往往不具备持续性,可能只是下跌途中的一个小反弹。如果此时冲进去,很容易被套。
1.2 引入动量阈值与分位数过滤
我们可以设置一个最低动量门槛,比如0.03(即3%)。只有第一名的动量超过这个门槛,策略才真正持有它;否则,即使它是正数,也视为不够强,继续留在避险资产中。
更进一步,我们可以用历史分位数来动态设定门槛:只有当动量值进入过去N年的前50%分位时,才认为趋势足够强。这个方法更灵活,能适应不同市场环境。
下面是一段实现动态分位数过滤的代码,假设我们已经有了一段历史的动量数据 momentum_history:
# 设置分位数阈值,例如过去500个交易日的前60%分位
lookback = 500
quantile_threshold = 0.6
# 计算每日的动量阈值(用 expanding 或 rolling 均可)
momentum_threshold = momentum_20.max(axis=1).rolling(lookback).quantile(quantile_threshold)
# 只有在最大动量超过该阈值时才持有,否则切换避险资产
safe_asset = '511010'
final_signal = best_etf.copy()
final_signal[(max_val < 0) | (max_val < momentum_threshold)] = safe_asset
叠加这一层后,策略在弱势反弹中会保持空仓或避险,从而减少无效交易和回撤。
二、风控第二层:波动率阈值过滤——涨得太疯,先出来看看
行业ETF最大的特点就是“疯起来不要命”。单日5%以上的涨跌停都不罕见。当持仓行业ETF的近期波动率飙升到极端水平时,意味着风险急剧放大,随时可能发生剧烈反转。
我们可以设置一条规则:如果当前持仓ETF的20日年化波动率超过40%,则在下一个调仓日强制减半仓,或者直接切换到国债ETF。
下面是判断和执行的代码示例:
# 计算每只ETF的20日年化波动率
daily_std = price_df.pct_change().rolling(20).std()
annual_vol = daily_std * np.sqrt(252)
# 在生成周频信号时,检查上周五持仓ETF的波动率
vol_threshold = 0.40 # 40%年化波动率
# 假设 trade_signal 已经生成了原始信号(未考虑波动率)
for i in range(1, len(trade_signal)):
prev_signal = trade_signal.iloc[i-1]
if prev_signal != safe_asset and prev_signal in annual_vol.columns:
vol = annual_vol.loc[trade_signal.index[i], prev_signal]
if vol > vol_threshold:
# 波动率过高,强制切换至避险资产
trade_signal.iloc[i] = safe_asset
这一层过滤能有效避免在“疯牛”顶部接盘。你可能会因此错过最后一波涨幅,但长期来看,它保护你的净值免受崩盘式下跌的伤害。
三、风控第三层:最大回撤硬止损——断臂求生
前两层风控都作用于信号生成端,属于“预防性”措施。但万一策略已经陷入了持续回撤,我们需要一个“兜底”机制,强行打断亏损的恶性循环。
一个经典的实战规则是:当策略净值从历史最高点回撤超过某个百分比(如15%或20%)时,下一周期强制空仓或全仓国债,直到净值重新创出新高或回撤修复到一定水平。
# 计算策略的累计最大回撤
rolling_max = strategy_nav.expanding().max()
drawdown = strategy_nav / rolling_max – 1
# 设定止损线
max_dd_threshold = -0.15 # 回撤超过15%触发止损
# 生成止损信号
stop_loss_signal = drawdown < max_dd_threshold
# 将止损信号融入最终持仓
final_daily_signal = daily_signal.copy()
# 当止损信号为True时,下一交易日切换到国债
# 注意:这里需要将信号后移一天,避免未来函数
stop_loss_shifted = stop_loss_signal.shift(1).fillna(False)
final_daily_signal[stop_loss_shifted] = safe_asset
加入硬止损后,策略的最大回撤将被强制控制在你设定的阈值附近。代价是可能在底部割肉,错过随后的反弹。你需要根据自己的心理承受能力来设定这个阈值。通常15%-20%是一个比较合理的区间。
四、风控第四层:行业集中度控制——不把鸡蛋放在一个篮子的篮子里
我们的行业候选池看似分散,但实际上某些行业之间存在高度关联。例如,新能源车ETF和光伏ETF都与新能源大主题相关,芯片ETF和5G ETF都属于科技大板块。在某些时段,动量排名前几的ETF可能全部集中在同一个大板块内。
如果策略同时持有2-3只相关度极高的ETF,实际上并未达到分散风险的目的,一旦该板块整体回调,损失会成倍放大。
我们可以增加一个行业板块约束:当已经持有一个板块的ETF时,即使同一板块的其他ETF排名靠前,也跳过它们,选择下一个不同板块的品种。
实现这一点需要预先对候选ETF进行板块分类:
手动定义行业板块(示例)
sector_map = {
'512880': '金融',
'512800': '金融',
'512010': '医药',
'159995': '科技',
'515790': '新能源',
'515030': '新能源',
'512660': '军工',
'512690': '消费',
'159869': '传媒',
'512400': '周期'
}
def diversified_top_n(momentum_row, n=2, safe_asset='511010'):
"""从一行动量数据中选出前N个正动量的ETF,且每个板块最多选一只"""
positive = momentum_row[momentum_row > 0].sort_values(ascending=False)
selected = []
used_sectors = set()
for etf in positive.index:
sector = sector_map.get(etf, '其他')
if sector not in used_sectors:
selected.append(etf)
used_sectors.add(sector)
if len(selected) >= n:
break
if len(selected) == 0:
return [safe_asset]
return selected
这个函数在生成多品种持仓时非常有用。它强制策略在不同的板块间分配仓位,避免了过度集中的风险。
五、分层叠加回测:看风控如何重塑策略
现在,让我们把这四道防线逐层叠加到上一章的基础策略上,看看净值曲线和核心指标会发生什么变化。
以下是一份基于2019-2024年历史数据的回测对比表(具体数字会因实际数据而异,此处为示意):
| 无风控(基础) | 28.5% | -38.2% | 0.95 | 30.0% |
| + 加强动量过滤 | 26.1% | -32.5% | 1.05 | 27.5% |
| + 波动率过滤 | 24.8% | -28.0% | 1.10 | 25.2% |
| + 最大回撤止损 | 23.2% | -18.5% | 1.18 | 23.0% |
| + 行业集中度控制 | 22.0% | -16.0% | 1.20 | 21.5% |
你会发现一个有趣的现象:随着风控的加强,收益率缓慢下降,但最大回撤和波动率显著改善,夏普比率反而提升了。 这正是专业投资者追求的“风险调整后收益”的优化。我们牺牲了一部分进攻性,换来了更平滑的净值曲线和更强的防御力。
在实际操作中,你不必四道防线全上,可以根据自己的风险偏好进行组合。对于大多数个人投资者,“加强动量过滤 + 最大回撤止损” 是一个性价比较高的组合,既能有效控回撤,又不会过度牺牲收益。
六、完整的风控增强代码框架
最后,我将上述风控模块整合为一个可复用的函数,你可以直接嵌入自己的回测系统中使用。
def enhanced_risk_control(momentum_df, price_df, hold_signal, safe_asset='511010',
mom_threshold_quantile=0.5, vol_threshold=0.40,
max_dd_threshold=-0.15, sector_map=None, top_n=2):
"""
对基础持仓信号进行多层风控增强
Parameters:
momentum_df: 动量DataFrame
price_df: 价格DataFrame
hold_signal: 原始周频持仓信号Series
safe_asset: 避险资产代码
mom_threshold_quantile: 动量分位数阈值
vol_threshold: 波动率阈值
max_dd_threshold: 最大回撤止损线
sector_map: 行业板块映射字典
top_n: 持有个股数量
Returns:
增强后的每日持仓信号Series
"""
# 1. 动量分位数过滤
max_momentum = momentum_df.max(axis=1)
mom_threshold = max_momentum.rolling(500).quantile(mom_threshold_quantile)
hold_signal[(max_momentum < 0) | (max_momentum < mom_threshold)] = safe_asset
# 2. 波动率过滤
daily_std = price_df.pct_change().rolling(20).std()
annual_vol = daily_std * np.sqrt(252)
for i in range(1, len(hold_signal)):
prev = hold_signal.iloc[i-1]
if prev != safe_asset and prev in annual_vol.columns:
if annual_vol.loc[hold_signal.index[i], prev] > vol_threshold:
hold_signal.iloc[i] = safe_asset
# 3. 最大回撤止损(在回测循环中动态计算,此处省略)
# 4. 行业集中度控制(略,见上文)
return hold_signal
七、本篇总结
在本文中,我们为行业ETF轮动策略构建了四道风控防线:
| 加强版动量过滤 | 动量值必须超过历史分位数或绝对门槛 | 弱势反弹中追高 |
| 波动率阈值过滤 | 持仓ETF波动率过高时退出 | 极端波动后的反转 |
| 最大回撤硬止损 | 净值回撤超过阈值强制空仓 | 连续亏损的恶性循环 |
| 行业集中度控制 | 同一板块最多持有一只 | 板块集中度过高 |
这四道防线可以单独使用,也可以组合叠加。建议你先在基础策略上逐层测试,找到最适合自己风险偏好的组合。
下一篇预告:行业轮动的实战思考与策略进化
在下一篇,我们将进入行业轮动系列的收官篇。我们会讨论行业轮动策略的实盘可行性、如何在真实交易中处理信号延迟和流动性问题,以及如何建立一个持续进化的策略迭代体系。
经过这四篇文章的打磨,你现在已经拥有了一套既能进攻、又懂防守的完整行业轮动策略。剩下的,就是打开编辑器,跑起来,然后根据市场反馈不断进化。咱们下篇见!

