改进美林时钟的Python量化轮动策略:信号加工与回测实战

📅 发布时间:2026/10/14 9:06:30
改进美林时钟的Python量化轮动策略:信号加工与回测实战
上篇把改进版的美林时钟框架搭起来之后有不少做量化的朋友来问我两个问题一是经典时钟用GDP和CPI就能画为什么非要绕一圈做改进二是这套逻辑听起来有道理代码到底怎么写才能跑出靠谱的回测。这篇就当集中回答这两个问题。先说个前提这篇是基于Python的量化策略实战向文章不是宏观经济学讲义。我们要做的是把一个概念框架变成可回测、可执行的轮动策略——增长和通胀不再是抽象名词而是固定频率更新的数据序列时钟的四个象限也不再是纸上的示意图而是对应着一篮子可交易资产。第二篇会沿着“为什么改进、改了什么、怎么实现、踩过哪些坑”这条线索往下走。为了避免文章又臭又长我把重点放在“改进点背后的取舍”和“可直接照抄的代码结构”上。代码会分块解释不追求一次贴出全部脚本——那样反而看不清逻辑。你在自己机器上按小节复制粘帖攒起来就是一个完整的策略框架。1. 为什么经典时钟框架落地时“总是慢半拍”整体设计与思路拆解1.1 经典时钟的底层逻辑与痛点回顾经典投资时钟的本质是一个“增长-通胀”四象限。横轴经济增长快慢纵轴通胀高低分成复苏、过热、滞胀、衰退四个状态每个状态对应一类占优资产。这套框架的优点是把宏观经济高度压缩能快速给出资产配置的直觉方向。但真正拿来做量化轮动会发现跑不动。为什么跑不动关键问题有三个。第一GDP是季度数据发布还有滞后等你确认了那个季度的经济状态周期可能已经走了大半个身位。第二CPI虽然月度但波动大、季节性强直接用原始值做判据会频繁穿越象限边界。第三经典框架只是给出排序方向比如“复苏期买股票”但它不会告诉你买什么股票、什么时候换仓、换了之后回撤多大。这些恰恰是量化策略必须回答的问题。所以如果直接拿经典时钟去回测大概率会看到两种情况换仓非常频繁手续费把收益吃得差不多或者信号滞后等到信号确认时资产价格已经走完主升浪。我用一个简单的例子说明滞后问题。假设某季度GDP增速回升但数据要到下季度末才公布。等到你确认增长加速时股市通常已经涨了三个月。这不是数据错了而是宏观数据天然存在发布时滞。策略要做的不是对抗时滞而是找更早、更频繁的代理变量来降低时滞影响。1.2 我理解的“改进”到底改在哪三个层面既然痛点集中在频率、稳定性和可交易性改进就必须沿着这三条线走。第一层是把增长维度的代理指标换成月度工业增加值同比或者工业企业利润总额同比。后者的实际波动更贴近企业盈利周期但也更毛糙所以通常要平滑。第二层是引入货币信用维度用社融存量同比或者M1同比来刻画流动性环境。这是因为很多时期资产轮动的驱动力不是“增长通胀”而是“信用扩张信用收缩”流动性拐点往往领先于增长拐点。把货币信用纳入进来相当于给时钟加了一个“提前量”。第三层是把状态判断从硬四象限改成连续打分和缓冲确认。这跟生活里的空调温控器一个道理室温降到设定温度以下才开升到另一个设定温度以上才关中间的差值避免压缩机频繁启停。策略里就是在状态切换时设置滞回带确认信号连续持续若干个月才承认切换牺牲一点灵敏度换取换仓频率大幅下降。这三层改造做完经典时钟才从“宏观框架”变成“可编程的信号系统”。1.3 这个策略适合谁、不适合谁适合的人群有两类。一类是已经有基础ETF配置思路、想给组合加一层宏观择时逻辑的人。另一类是量化新手想找一个“原理清晰、要素完整、适合练手”的多资产轮动框架。对于这两类人改进时钟的边际收益最高——你不需要每天盯盘只需要每个月末看一次信号做一次调仓。不适合的人群也有两类。一类是追求极高换手、靠短线波动的交易者时钟框架天然匹配不了这种节奏。另一类是只会照抄代码、不理解指标逻辑的人——因为宏观代理指标在极端环境下的行为常常超出历史样本不理解原理就很难在异常时做人工干预。理解了适用边界之后接下来要解决的是具体怎么选指标、怎么处理数据。这是整篇里最琐碎也最关键的一环。2. 指标选型与信号加工的实操细节核心细节解析与实操要点2.1 增长维度为什么用工业增加值同比而不是GDP我最早做回测时直接用GDP同比做增长代理变量结果非常糟糕。回测曲线在大多数时间是横着的偶尔在季报公布后跳一次。原因不复杂GDP全年四个数据点其中还有一些是累计值做轮动根本没有足够的决策频率。后来换成月度工业增加值同比情况立刻不同。它的发布时间比GDP早频率高和GDP增速的相关性也很高。对于策略来说哪怕这个指标不如GDP全面但只要它稳定领先或同步于经济周期就是一个合格的代理变量。实际上在量化宏观策略里有一个常见原则不追求指标“理论上最正确”而是追求“频率够高、口径稳定、发布及时”。工业增加值同比基本满足这些条件。如果你希望更贴近企业盈利周期可以用工业企业利润总额同比但要注意它的波动更大极端值更多必须做平滑处理否则会把状态识别带偏。数据清洗方面要注意三点。第一多一个春节因素1月和2月的同比数据往往不可比实践中建议对1-2月数据合并处理或者直接舍弃这两个点。第二同比数据受到基数效应影响去年基数低会造成今年同比虚高这种虚假信号往往出现在经济复苏初期需要结合环比趋势综合判断。第三所有宏观数据发布都有统一日程回测时必须把数据发布时间和取值时间对齐这一点后面单独讲。2.2 通胀与货币维度CPI、PPI和信用指标怎么配合使用经典时钟只用CPI代表通胀但在资产轮动里PPI的作用往往比CPI更直接。PPI反映的是工业品价格和周期股、资源品、商品期货的走势高度相关CPI则更多影响消费端和货币政策预期。我的做法是把CPI和PPI都纳入通胀维度用两者同比的核心分位值合成一个通胀分项。为什么还要加货币信用指标因为增长和通胀描述的是“经济温度”但资产价格直接受到“货币水位”的影响。信用扩张时即使实体增长一般资本市场也可能先走一波信用收缩时即使增长还行资产价格也容易承压。所以我在框架里引入社融存量同比作为信用维度和增长维度正交之后构成新的状态空间。这里要提醒一个容易混淆的点货币信用框架和经典时钟不是替代关系而是互补关系。经典时钟擅长回答“处于周期哪个位置”货币信用框架擅长回答“流动性拐点是否出现”。实际使用时我会用信用指标作为状态切换的“加速器”——增长和通胀分项的方向由经典逻辑决定但只有在信用指标同向确认时才允许完成象限切换。2.3 数据标准化不同口径的宏观指标怎么放进同一套模型宏观指标的单位各不相同工业增加值同比是百分比社融存量同比也是百分比但有些指标可能是比值或者绝对值。要让它们在一个打分系统里可比我的做法是统一转成历史分位值。具体操作是这样对每个指标取过去36个月的滚动窗口把当前值映射到0到100的分位区间。比如当前工业增加值同比在近三年里处于78%分位说明增长偏强CPI同比处于30%分位说明通胀偏温和。分位值的好处是不受绝对水平影响而且天然适应不同指标的分布形态。使用分位值时还会遇到一个经典问题极端时期之后滚动窗口里的历史分布会被拉偏。比如经历过一次严重通缩后后续一年任何正通胀都会显得很高。我的应对办法是给滚动窗口设定一个最低长度并加入指数衰减权重让近期数据在分位计算中权重更大。这样既有历史比较的稳定性又能对近期的拐点更敏感。表格里总结一下我用到的指标和加工方式维度原始指标加工方式用途增长工业增加值同比 / 工业企业利润总额同比去春节效应、平滑、36个月滚动分位判断经济扩张或收缩通胀CPI同比、PPI同比平滑合成核心通胀分位判断通胀压力方向信用社融存量同比 / M1同比平滑、滚动分位判断流动性扩张或收缩指标加工完了下一步就是怎么把这些分位值变成具体的买入卖出信号。这里涉及到整个策略最核心的判断逻辑——状态识别。3. Python实现从信号到资产轮动的完整链路3.1 数据获取与数据结构准备我在这个策略里用到的数据分为两类宏观指标数据和资产价格数据。宏观指标通常以月度频率发布资产价格我把它们统一重采样成月末水平保持频率对齐。代码的第一步是准备统一的时间索引import pandas as pd import numpy as np # 假设df_macro已经包含宏观指标索引为月份周期 macro pd.DataFrame({ gdp_proxy: [...], # 工业增加值同比 cpi: [...], # CPI同比 ppi: [...], # PPI同比 credit: [...] # 社融存量同比 }, indexpd.to_datetime([...])) # 资产净值数据统一从日频重采样到月末 def resample_monthly(price_df): return price_df.resample(ME).last()有一个特别重要的细节日频数据重采样到月末时如果直接用月末最后一个交易日的价格可能出现“只看价格、不看数据发布时间”的问题。正确做法是先把宏观信号的发布日期当作信号生效日资产价格只取信号生效日之前已收盘的价格。这样回测才不会用到未来数据。用伪代码描述的话核心步骤是获取宏观数据的发布日期、生成信号、然后让信号在发布日期之后才生效。我之前见过不少回测脚本直接把月度数据放在月末计算信号结果信号生效时使用的是当月月末价格看起来收益率很高实际是用了未来信息。3.2 指标打分与状态识别滞回带的实现状态识别的核心是一个打分函数。我给增长、通胀、信用三个维度分别打分然后映射到四个象限。打分逻辑不复杂核心是滞回带设计。先看基础版的信号计算def calculate_signal(macro, lookback36): df macro.copy() # 滚动分位值统一到0-100区间 for col in [gdp_proxy, cpi, ppi, credit]: df[col _pct] df[col].rolling(lookback).rank(pctTrue) * 100 # 合成维度分项 df[growth_score] df[gdp_proxy_pct] df[inflation_score] 0.6 * df[cpi_pct] 0.4 * df[ppi_pct] df[credit_score] df[credit_pct] return df得分出来后怎么决定当前的象限这里我采用“先等级判断、再加缓冲”的策略增长和通胀按50分位为界分成高低两组但在边界附近不是立即切换而是要求信号突破一个更高的阈值并连续保持若干个月。这是滞回带的核心逻辑def state_machine(signal_series, upper55, lower45, min_months3): 状态机阈值缓冲 最短持续时间确认 states [] current_state None hold_duration 0 for s in signal_series: if current_state hot: if s lower: hold_duration 1 if hold_duration min_months: current_state normal hold_duration 0 else: hold_duration 0 else: if s upper: hold_duration 1 if hold_duration min_months: current_state hot hold_duration 0 else: hold_duration 0 states.append(current_state) return states这个设计其实回答了一个关键问题宏观指标在边界附近来回抖动时策略会怎么样如果不加缓冲一个指标在48到53之间波动策略就会在“过热”和“正常”之间反复切换换仓一次的手续费加上价格滑点一年可以吃掉几个点的收益。加了滞回带之后切换频率大幅下降虽然信号反应慢了一两个月但在实战中反而更稳健。3.3 象限映射与资产篮子配置状态识别完成后需要把四个象限映射到具体的资产组合。我这里用的是市场上流动性较好的宽基指数和行业指数以模拟基金的日净值数据做回测。配置比例不是固定的会根据信用维度分位做微调。一个基本的映射如下象限状态宏观特征资产配置倾向复苏增长强、通胀弱企业盈利改善、政策仍偏松成长风格宽基为主辅以中盘指数过热增长强、通胀强盈利与物价同涨、政策开始收紧资源品指数、商品类资产滞胀增长弱、通胀强成本挤压盈利、政策难以放松短债类资产、防御性红利资产衰退增长弱、通胀弱政策转松、利率下行债券类资产、高股息资产轮动调仓的逻辑是每个月末检查一次信号如果当前状态和上月末状态一致保持仓位不动如果状态切换第二天按目标比例调仓。为了避免每个月都触发调仓我会额外设置一个阈值只有状态连续两个月确认不同才真正执行调仓。我试过把调仓频率从每周拉到每月再到每季度回测结果很扎心周频调仓收益最高但手续费和滑点吃掉了大部分超额收益月频调仓收益略低但扣费后净值曲线明显更平滑季频调仓太迟钝会错过周期切换初期的行情。最终我选了月度调仓再叠加滞回带整体换手率可以压到年均两到三次。3.4 回测框架从信号到绩效统计回测框架的核心代码并不长。我用一个简单的向量化思路信号序列生成目标权重然后和资产月收益相乘累计得到组合净值。基本框架如下def backtest(weights, asset_returns): # weights: 每月各资产目标权重, asset_returns: 各资产月收益 # 假设次月按目标权重持有 portfolio_returns (weights.shift(1) * asset_returns).sum(axis1) nav (1 portfolio_returns).cumprod() return nav这里有一个很重要的细节是shift(1)权重信号在个月末生成下个月才开始生效。如果忘了shift等于用当月信号去赚当月的钱又是一种前视偏差。回测统计还需要包括年化收益、最大回撤、夏普比率、卡玛比率这几个核心指标。我自己额外会统计一个“换手率”指标用来衡量调仓成本。换手率过高说明状态机设计过分灵敏需要调整滞回带参数。就我自己的模拟回测结果来看在固定样本区间内改进后的时钟策略比经典时钟的年化收益能高出几个百分点同时最大回撤明显缩小。关键收益来源不是“每次判断都对”而是“在滞胀和衰退状态下大幅降低权益暴露”这避免了经典框架在衰退期照常买股票的尴尬。3.5 参数选择与过拟合防范参数选择方面滚动窗口长度、滞回带阈值、最短确认月数这三个参数最敏感。我建议不要直接照搬别人的参数而是做一个三因素小网格搜索然后在网格表面观察参数平原是否平滑。如果某个参数稍微变动就导致结果剧烈变化说明模型已经过拟合。我自己常用的参数范围是滚动窗口24到60个月滞回带阈值上下分别为55/45到65/35最短确认月数2到6个月。在这个范围内策略绩效不会出现断崖式变化说明框架是稳定的。跑参数网格时的几个补充建议第一不要只看年化收益要看最大回撤和换手率第二不要用全样本做参数选择应该用前段选参、后段验证否则你得到的是专门拟合过的曲线第三不同资产价格周期对参数敏感性不同固定参数长期不做调整本身就是一种风险。4. 常见问题与排查技巧实录4.1 数据对齐与前视偏差回测收益虚高的元凶我在做这个策略时第一个版本的回测结果非常漂亮年化收益高得惊人。后来逐笔复盘发现问题出在信号生成时间和价格匹配错位上。我的宏观数据用的是月末值资产价格也取月末值信号在月末生成、当月价格直接计入收益相当于“用月末已经发生的行情去生成月末的决策”。修正方法很简单所有信号生成之后都要对价格做shift(1)确保当月的信号只能影响下个月的持仓。同时要检查数据发布时间尤其是宏观指标经常有“数据月份”和“发布时间”的区别。最好的做法是在数据源里保留发布时间字段信号生效日以发布时间为准。这个问题非常隐蔽因为净值曲线看起来没有问题只有逐期对比信号和持仓时才能发现。建议回测代码里加一个check函数每期打印信号日期、生效日期、持仓状态抽检几期就能发现问题。4.2 指标之间“打架”了怎么办权重与优先级实战中经常出现增长指标说复苏、通胀指标说滞胀、信用指标又给出另一套指引的情况。三个方向各说各话这时候状态机怎么判断我试过用等权加权合成一个总评分效果一般。后来调整成“增长优先、信用确认”的规则先由增长维度决定权益仓位方向再由信用维度决定加仓还是减仓通胀维度主要影响内部风格选择。这套规则不是从理论上推导出来的而是对不同经济阶段的实际表现做了复盘后总结的经验值。具体来说当增长分位高于50且信用分位高于50时权益仓位可以打到上限增长分位高但信用分位低时权益仓位降至中高水平因为流动性收缩会压制估值增长分位低于50时无论信用和通胀怎么走权益仓位都要降下来重点转向防守资产。这套优先级规则简单粗糙但胜在可解释性强。我见过用机器学习模型直接拟合象限的做法样本外效果并不好——宏观状态是非平稳的统计模型很容易把极端环境当成常态来学习。4.3 手续费和涨跌停限制回测到实盘的距离回测里我默认单边手续费万分之二滑点设千分之一看似合理但对轮动策略来说真正的成本大头是冲击成本和涨跌停无法成交。轮动策略的特点是换仓集中在月初的一两天如果管理规模稍大买入选定的指数基金或ETF时市场价格会被推高实际成交价往往比收盘价高不少。我的处理办法是在回测中给一个悲观的滑点假设比如千分之二然后用这个假设再跑一遍看策略是否依然能盈利。如果悲观假设下超额收益大幅缩水说明策略的收益来源依赖高频交易需要重新审视状态机的灵敏度。另外还要检查资产池里有没有长期停牌或流动性差的品种。有些行业指数基金看似收益好实际日成交额很小大资金根本进不去。我在模拟演示里用的是宽基和行业模拟指数但在实盘方案中会换成真实可交易的ETF并核对近一年的日均成交额。4.4 宏观数据修正另一个容易忽略的坑宏观数据发布之后经常会有修订。比如工业增加值初值、修正值和最终值可能相差不小。回测时如果用的是最终修订版数据实盘中根本拿不到就会产生另一种前视偏差。解决思路有两个。一是用初值数据做回测二是对最终值数据人为加一点噪声或在信号判断上留出余量。大多数公开数据接口只能拿到最终值实际操作中我倾向于在信号判断时故意降低阈值灵敏度——既然数据本身有不确定性状态机就不要用太尖锐的阈值。4.5 常见问题速查表问题可能原因解决办法回测收益异常高信号与价格同期匹配权重shift(1)确认信号生效日期换仓太频繁阈值过窄、没有滞回带加宽缓冲阈值增加最短确认月数牛市跑不赢基准信号滞后于行情启动引入信用指标作为领先确认熊市回撤过大增长指标下穿不及时缩短确认周期或增加趋势追踪过滤器指标方向矛盾没有明确优先级固定“增长优先、信用确认、通胀定风格”实盘表现和回测差异大滑点估计不足、流动性不足悲观滑点重测核对ETF成交额这些坑我基本都踩过一遍尤其是第一个当时一度以为自己找到了“圣杯”后来才发现是未来函数在作祟。做量化策略回测得再好看都不如账算得清楚重要。5. 实测心得与后续扩展方向从最早搭框架到现在我最大的体会是改进美林时钟的最有价值部分不是多赚了多少收益而是让组合在极端环境下的回撤变得可控。轮动策略不可能每次都踩对节奏但只要结构性回避掉明显的衰退和滞胀区间长期净值曲线就会比恒定配置平滑很多。这个策略后续还有不少可以扩展的地方。一个是把资产池从宽基指数切换到风格指数和行业指数让复苏和过热象限的表达更细腻另一个是引入估值百分位作为辅助判断增长和通胀没说清楚的时候用估值位置做补充还有一个方向是并行运行多套参数用模型池的方式做集成降低单参数过拟合的风险。最后再分享一个小技巧状态机的参数不要调完就扔在那里每隔半年用滚动窗口重新校验一次。宏观环境在变数据的口径也在变策略参数需要跟着微调。这就跟养花一样定期修剪才能让框架保持生命力。希望这篇能让你在上篇搭建的框架基础上真正跑出自己的改进时钟轮动策略。