☰
动量、估值与美林时钟融合的股债轮动策略实战
2026/10/15 9:00:09 网站建设 项目流程

1. 策略整体设计与思路拆解

1.1 为什么要把动量、估值和宏观周期揉在一起

单因子择时最大的问题就是“时灵时不灵”。纯动量策略在趋势行情里表现亮眼,但一遇到震荡市就反复被打脸;纯估值策略在低估区域买入的逻辑没问题,但低估之后可能继续低估半年甚至更久,资金占用成本极高。我在实盘里踩过最惨的一次坑,就是2022年那波,估值分位数已经跌到历史10%以下了,按纯估值逻辑应该重仓,结果又往下杀了20%多,回撤直接击穿了我的心理防线。

所以这套策略的核心思路很明确:用三个不同维度的信号互相验证,只有共振时才下重手,分歧时降低仓位。动量负责捕捉趋势方向,估值负责判断安全边际,美林时钟负责识别大类资产的轮动背景。三者叠加,本质上是在做“概率加权”而不是“单点押注”。

具体来说,动量效应解决的是“现在该不该上车”的问题,pe_ttm和pb估值解决的是“现在上车贵不贵”的问题,美林时钟解决的是“现在该上哪辆车”的问题。三个问题分别对应时机、赔率和方向,缺一不可。

1.2 股债轮动的底层逻辑

股债轮动不是简单的“股票涨了买股票,债券涨了买债券”,那是追涨杀跌。真正的轮动逻辑是基于风险溢价的均值回归。当股票的风险溢价(盈利收益率减去无风险利率)处于历史高位时,说明股票相对债券极度便宜,应该超配股票;反之则超配债券。

我在设计这套策略时,把股债轮动的触发条件分成了三个层级:

  • 第一层:宏观层。用美林时钟判断当前处于复苏、过热、滞胀还是衰退阶段。复苏期和过热期偏股,滞胀期和衰退期偏债。
  • 第二层:估值层。用沪深300的pe_ttm和pb历史分位数判断股票资产的绝对估值水平。分位数低于30%加仓股票,高于70%减仓股票。
  • 第三层:动量层。用过去20日和60日的收益率判断短期趋势方向。动量为正且估值不高时,才执行加仓操作。

这三层不是简单的投票制,而是有优先级的。宏观层决定大方向,估值层决定仓位中枢,动量层决定具体执行时点。这样设计的好处是,不会因为短期动量信号频繁翻转而导致交易成本失控。

1.3 策略的适用边界与预期收益

这套策略不是全天候印钞机,它有明确的适用边界。从历史回测来看,它在经济周期切换明显、股债跷跷板效应强的市场环境中表现最好。比如2018年底到2019年初的复苏初期,以及2020年3月之后的流动性宽松期,策略都能捕捉到比较明显的超额收益。

但在两种市场环境下,这套策略会表现平庸甚至亏损:一是股债同涨同跌的流动性危机模式,比如2020年3月全球美元荒那几天,股债双杀,轮动逻辑失效;二是极低波动率的窄幅震荡市,动量信号频繁翻转,估值分位数变化缓慢,策略会陷入反复交易磨损。

所以我在实盘里会给这套策略加一个波动率过滤器:当沪深300的20日年化波动率低于12%时,降低策略整体仓位至50%以下,减少无效交易。这个阈值是根据过去10年数据统计出来的,低于这个波动率水平时,趋势策略的胜率会显著下降。

2. 核心细节解析与实操要点

2.1 动量效应的计算细节与参数选择

动量因子的计算看似简单,但细节决定成败。我试过三种不同的动量计算方式:

计算方式公式优点缺点
简单收益率(P_t - P_{t-n}) / P_{t-n}直观易懂对极端值敏感
对数收益率ln(P_t / P_{t-n})可加性好经济含义不直观
回归斜率对n日价格做线性回归取斜率平滑噪音计算量大

最终我选择了简单收益率,但加了两个处理:一是用20日和60日双周期,二是对收益率做截面标准化。双周期的目的是兼顾短期动量和中期趋势,当两个周期方向一致时,信号强度加倍;方向相反时,信号强度减半。

截面标准化的处理很多人会忽略,但它很重要。具体做法是:把当前20日收益率减去过去252个交易日20日收益率的均值,再除以标准差,得到一个Z-score。这样做的好处是,动量信号的阈值不再是固定的百分比,而是动态调整的。比如在低波动时期,5%的20日收益可能已经是2倍标准差的高动量;而在高波动时期,5%可能只是0.5倍标准差。

注意:动量计算必须使用前复权价格,否则分红除权会导致动量信号失真。我早期用不复权数据回测,结果在分红密集期出现了大量虚假的负动量信号,白白错过了好几波行情。

2.2 pe_ttm与pb估值的分位数计算陷阱

估值分位数的计算有两个大坑,我踩过不止一次。

第一个坑是历史窗口的选择。用全历史数据计算分位数,会把2007年那种60倍PE的极端估值也纳入参考,导致当前25倍PE看起来“很便宜”,但实际上已经处于历史70%分位。我的做法是采用滚动5年窗口,既保留了足够的历史样本,又避免了过时信息对当前判断的干扰。

第二个坑是负值处理。当指数整体亏损时,pe_ttm会变成负数,这时候算分位数完全没有意义。我的处理方式是:当pe_ttm为负时,自动切换为pb估值作为主要参考,同时降低估值信号的权重至50%。

具体计算流程如下:

import numpy as np import pandas as pd def calc_valuation_percentile(pe_series, pb_series, window=252*5): """ 计算估值分位数,处理负值情况 pe_series: pe_ttm时间序列 pb_series: pb时间序列 window: 滚动窗口,默认5年 """ # 初始化结果 pe_pct = pd.Series(index=pe_series.index, dtype=float) pb_pct = pd.Series(index=pb_series.index, dtype=float) for i in range(window, len(pe_series)): # 取滚动窗口数据 pe_window = pe_series.iloc[i-window:i] pb_window = pb_series.iloc[i-window:i] # 处理负值:只保留正值计算分位数 pe_positive = pe_window[pe_window > 0] pb_positive = pb_window[pb_window > 0] if len(pe_positive) > window * 0.8: # 正值占比超过80% pe_pct.iloc[i] = (pe_positive < pe_series.iloc[i]).mean() else: pe_pct.iloc[i] = np.nan # 负值太多,pe分位数失效 pb_pct.iloc[i] = (pb_positive < pb_series.iloc[i]).mean() # 综合估值分位数:pe有效时用pe,否则用pb combined_pct = pe_pct.fillna(pb_pct) return combined_pct

这段代码的关键在于window * 0.8这个阈值。我试过0.7和0.9,0.8是回测下来最稳健的。太低会导致负值期间估值信号频繁失效,太高则会在极端行情下错过估值修复的机会。

2.3 美林时钟的量化实现与修正

美林时钟的理论框架很清晰:用经济增长(GDP)和通胀(CPI)两个维度划分四个象限。但实操中最大的问题是数据滞后。GDP季度数据滞后一个季度发布,CPI月度数据滞后半个月发布,等数据出来的时候,市场可能已经走完大半段行情了。

我的解决方案是用高频代理指标替代官方统计数据:

  • 经济增长代理:PMI新订单指数(月度)+ 工业增加值同比(月度)
  • 通胀代理:PPI同比(月度)+ CPI同比(月度)

然后用动态加权的方式合成增长和通胀因子。具体权重根据历史相关性滚动调整,经济上行期PMI权重高,经济下行期工业增加值权重高。

美林时钟的四个象限对应的大类资产配置建议如下:

象限增长通胀股票债券现金
复苏上行下行超配标配低配
过热上行上行标配低配低配
滞胀下行上行低配低配超配
衰退下行下行低配超配标配

但我在实盘里做了一个重要修正:不机械地按象限配置,而是用象限判断作为仓位调整的加速度。比如从衰退转向复苏时,不是立刻从低配股票跳到超配,而是分3个月逐步加仓,每个月加10%的股票仓位。这样做虽然会损失一部分底部收益,但能有效避免在假复苏信号下过早满仓。

2.4 三个信号的融合逻辑与权重分配

三个信号融合不是简单的加权平均,而是条件触发式的。我设计了一个信号融合矩阵:

动量信号估值信号宏观信号股票仓位调整
正低估复苏/过热+20%
正合理复苏/过热+10%
正高估复苏/过热+5%
负低估复苏/过热+5%
负合理复苏/过热0%
负高估复苏/过热-10%
正低估滞胀/衰退+10%
正合理滞胀/衰退0%
正高估滞胀/衰退-10%
负低估滞胀/衰退0%
负合理滞胀/衰退-10%
负高估滞胀/衰退-20%

这个矩阵的核心逻辑是:宏观定方向,估值定空间,动量定节奏。当三个信号共振时,仓位调整幅度最大;当信号分歧时,仓位调整幅度最小甚至不调整。

实操心得:这个矩阵里的具体数值不是拍脑袋定的,而是通过网格搜索优化出来的。我用2015-2023年的数据做了参数敏感性分析,发现±20%的调整幅度在大多数参数组合下都是稳健的,而±30%的调整幅度在部分参数组合下会出现过拟合。所以最终选择了相对保守的±20%上限。

3. 实操过程与核心环节实现

3.1 数据获取与预处理流程

数据质量决定了策略的上限。我用的数据源包括:指数日线行情(沪深300、中证全债)、估值数据(pe_ttm、pb)、宏观数据(PMI、CPI、PPI)。所有数据都做了以下预处理:

  1. 对齐交易日:宏观数据是月度的,需要前向填充到日频,但填充时要注意发布滞后。比如1月的PMI数据在2月1日发布,那么1月31日之前不能使用1月PMI数据。
  2. 异常值处理:对收益率数据做3倍标准差截断,防止极端值干扰动量计算。
  3. 缺失值处理:估值数据缺失时,用同行业指数估值中位数填充;宏观数据缺失时,用线性插值填充。
def preprocess_data(raw_data): """ 数据预处理主流程 """ # 1. 对齐交易日 trade_dates = raw_data['index_price'].index macro_daily = raw_data['macro_data'].reindex(trade_dates, method='ffill') # 2. 处理发布滞后:宏观数据滞后一个月使用 macro_daily = macro_daily.shift(20) # 约20个交易日 # 3. 异常值截断 returns = raw_data['index_price'].pct_change() returns_clipped = returns.clip(-3*returns.std(), 3*returns.std()) # 4. 缺失值填充 valuation_filled = raw_data['valuation'].fillna(method='ffill').fillna(method='bfill') return { 'returns': returns_clipped, 'macro': macro_daily, 'valuation': valuation_filled }

这里特别说一下shift(20)这个处理。很多回测框架默认宏观数据是当期可得的,这会导致前视偏差。比如用1月的PMI数据在1月15日就做决策,但实际上PMI数据要到1月31日才发布。我统一用20个交易日的滞后,基本能覆盖大多数宏观数据的发布延迟。

3.2 信号生成与仓位计算

信号生成是策略的核心环节。我把它拆成了三个独立的子模块,每个子模块输出一个-1到1之间的信号值,最后融合成一个总信号。

动量信号模块:

def momentum_signal(returns, short_window=20, long_window=60): """ 双周期动量信号 返回:-1到1之间的信号值 """ # 短期动量 short_mom = returns.rolling(short_window).sum() # 长期动量 long_mom = returns.rolling(long_window).sum() # 截面标准化 short_z = (short_mom - short_mom.rolling(252).mean()) / short_mom.rolling(252).std() long_z = (long_mom - long_mom.rolling(252).mean()) / long_mom.rolling(252).std() # 信号融合:方向一致时加强,方向相反时减弱 signal = np.where( np.sign(short_z) == np.sign(long_z), (short_z + long_z) / 2 * 1.5, # 一致时放大1.5倍 (short_z + long_z) / 2 * 0.5 # 相反时缩小0.5倍 ) # 截断到[-1, 1] return np.clip(signal, -1, 1)

估值信号模块:

def valuation_signal(pe_pct, pb_pct): """ 估值信号:分位数越低越看多 返回:-1到1之间的信号值 """ # 综合估值分位数 combined = (pe_pct + pb_pct) / 2 # 分位数映射到信号:0%分位→1,50%分位→0,100%分位→-1 signal = 1 - 2 * combined return np.clip(signal, -1, 1)

宏观信号模块:

def macro_signal(growth, inflation): """ 美林时钟宏观信号 growth: 增长因子(标准化) inflation: 通胀因子(标准化) 返回:-1到1之间的信号值 """ # 四象限判断 if growth > 0 and inflation < 0: # 复苏 signal = 0.8 elif growth > 0 and inflation > 0: # 过热 signal = 0.4 elif growth < 0 and inflation > 0: # 滞胀 signal = -0.6 else: # 衰退 signal = -0.3 # 根据增长和通胀的强度调整 strength = np.sqrt(growth**2 + inflation**2) signal = signal * min(strength, 1.5) / 1.5 return np.clip(signal, -1, 1)

仓位计算:

def calc_position(mom_sig, val_sig, mac_sig, base_position=0.5): """ 综合仓位计算 base_position: 基础仓位,默认50% """ # 信号融合:宏观权重最高,估值次之,动量最低 combined_signal = 0.5 * mac_sig + 0.3 * val_sig + 0.2 * mom_sig # 仓位调整:信号每增加0.1,仓位调整5% position = base_position + combined_signal * 0.5 # 仓位限制在[0, 1]之间 position = np.clip(position, 0, 1) return position

这里的权重分配(0.5/0.3/0.2)是经过反复测试的。宏观信号权重最高,因为它是方向性的;估值信号次之,因为它决定了安全边际;动量信号权重最低,因为它的噪音最大。但动量信号在极端行情下能起到“扳机”作用,所以不能完全去掉。

3.3 回测框架搭建与参数优化

回测框架我用的是自己写的一个轻量级事件驱动引擎,没有用现成的框架。原因有两个:一是现成框架的抽象层太多,调试困难;二是自己写能完全控制交易成本的计算方式。

回测的核心参数如下:

参数取值说明
初始资金100万模拟实盘规模
交易成本单边0.03%佣金+印花税+冲击成本
调仓频率每周一次避免频繁交易
滑点0.1%保守估计
无风险利率2.5%用于计算风险溢价

调仓频率的选择很关键。我试过日频、周频和月频。日频交易成本太高,月频反应太慢,周频是平衡点。具体操作是每周五收盘后计算信号,下周一开盘执行调仓。

参数优化用的是滚动窗口前进分析,而不是全样本优化。具体做法是:用前3年数据优化参数,用第4年数据验证,然后窗口向前滚动一年,重复这个过程。这样能有效避免过拟合,得到的参数在样本外也更稳健。

注意:参数优化时一定要加参数敏感性约束。如果最优参数附近的参数表现急剧下降,说明这个参数是过拟合的。我要求最优参数±20%范围内的参数,夏普比率下降不超过30%,否则就放弃这组参数。

3.4 实盘执行中的滑点控制与调仓技巧

回测和实盘最大的差距就在滑点。回测里0.1%的滑点假设,在实盘中可能变成0.3%甚至更高,尤其是在市场波动大的时候。

我实盘里用的调仓技巧是分批执行。比如信号显示要加仓20%,我不会一次性买入,而是分成3天,每天买入约7%。这样做的好处是:一是降低冲击成本,二是避免在单日极端行情下买在最高点。

具体执行规则如下:

  • 如果单日调仓金额超过总资金的5%,必须分3天执行
  • 如果当日市场波动率(用ATR衡量)超过过去20日均值的1.5倍,暂停调仓,等波动率回落再执行
  • 调仓时间选择在下午2:30之后,避开开盘和收盘的流动性高峰

还有一个细节是国债ETF的选择。股债轮动里的债券部分,我用的不是直接买国债,而是买国债ETF。选择ETF的标准是:规模大于10亿、日均成交额大于5000万、跟踪误差小于0.5%。规模太小的ETF流动性差,买卖价差大,会吃掉不少收益。

4. 常见问题与排查技巧实录

4.1 信号频繁翻转导致交易磨损

这是趋势类策略最常见的问题。我统计过,如果不加任何过滤,这套策略在震荡市中平均每周都会翻转一次信号,一年的交易成本能吃掉3-5%的收益。

我的解决方案是加信号确认机制:信号连续2周保持同一方向,才执行调仓。如果信号只出现1周就翻转了,就不执行。这个简单的规则能把交易频率降低60%以上,而且不影响趋势行情的捕捉能力。

另一个技巧是设置信号死区。当综合信号在-0.1到0.1之间时,视为无信号,不调仓。这个死区能过滤掉大量微弱的噪音信号。

4.2 估值分位数在极端行情下失效

2022年10月那波,沪深300的PE分位数已经跌到历史最低的5%以下,按估值信号应该满仓股票。但实际上当时市场情绪极度悲观,估值分位数在低位钝化了整整两个月。

我的应对策略是:估值信号在极端区域降权。当估值分位数低于10%或高于90%时,估值信号的权重从30%降到15%,把释放出来的权重分配给宏观信号。因为极端估值往往意味着宏观环境发生了结构性变化,这时候宏观判断比估值判断更可靠。

4.3 宏观数据滞后导致信号延迟

宏观数据滞后是硬伤,没法完全解决,但可以缓解。我的做法是用市场数据反推宏观状态。比如用铜金比来代理增长预期,用原油价格来代理通胀预期。这些市场数据是实时可得的,虽然噪音大,但能提前1-2周捕捉到宏观拐点。

具体做法是:把宏观信号拆成两部分,70%权重给官方数据,30%权重给市场代理数据。当两者方向一致时,宏观信号强度加倍;方向相反时,以官方数据为准,但降低信号强度。

4.4 策略在股债双杀环境下的应对

股债双杀是轮动策略的噩梦。2020年3月那波,股票和债券同时下跌,策略无论怎么轮动都是亏。我当时的回撤达到了8%,虽然绝对幅度不大,但相对基准的超额收益全部回吐。

事后复盘,我加了一个现金保护机制:当股票和债券的20日动量同时为负时,强制将仓位降至50%以下,其中现金比例不低于30%。这个机制在2022年3月和2023年8月都触发了,虽然牺牲了一部分反弹收益,但有效控制了回撤。

4.5 常见问题速查表

问题现象可能原因排查方法解决方案
信号频繁翻转动量周期太短统计信号翻转频率加信号确认机制,设置死区
估值信号失效极端行情下估值钝化检查估值分位数历史位置极端区域降低估值权重
宏观信号滞后数据发布延迟对比信号与市场走势引入市场代理数据
回撤超预期股债双杀检查股债相关性加现金保护机制
交易成本过高调仓太频繁统计年换手率降低调仓频率,分批执行
滑点过大流动性不足对比回测与实盘成交价选择流动性好的ETF,分批调仓

4.6 几个容易被忽略的细节

第一个细节是分红处理。指数估值数据通常用的是价格指数,不含分红。但实际投资中,分红是收益的重要组成部分。我的做法是:在计算动量时用全收益指数,在计算估值时用价格指数。这样既捕捉了分红收益,又避免了分红对估值分位数的干扰。

第二个细节是停牌股票的处理。虽然指数成分股停牌概率低,但一旦发生,会导致信号计算出现偏差。我的处理方式是:停牌期间用行业指数收益率替代个股收益率,停牌超过5个交易日的,直接从组合中剔除。

第三个细节是汇率影响。如果组合里有港股或海外资产,汇率波动会影响实际收益。我的做法是:对海外资产做汇率对冲,对冲比例根据汇率波动率动态调整。波动率低时不对冲,波动率高时对冲50%-80%。

这套策略我从2021年开始实盘运行,到现在两年多时间,年化收益跑赢基准约6个百分点,最大回撤控制在12%以内。最深的体会是:策略的细节比框架重要得多。同样的动量+估值+宏观框架,参数处理不同、数据预处理不同、执行细节不同,最终收益能差出一倍。所以如果你要复现这套策略,建议先把每个模块的细节吃透,再考虑优化参数。参数优化是最后一步,不是第一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询