简介:华宝证券发布的金融工程专题报告《A股多维度量化择时系统构建与回测》,面向量化投研、资产配置及指数择时实践者。报告沿用前期债券与黄金择时框架,以经济运行、流动性、动量趋势、事件驱动四个维度构建多因子打分体系,每个维度先对单指标进行择时测试,按收益和胜率加权合成维度打分,并配有样本内外净值走势图表。前三维度构成主交易系统,事件驱动因高胜率、非连续性作为额外加分项,经等权合成后采用分仓方式配置权益资产,兼顾收益与回撤控制。回测区间2012年1月至2019年6月,年化收益12.8%,优于同期沪深300指数的6.8%;滚动一年平均最大回撤由-22.2%收窄至-9.6%,收益回撤比达1.33,同时提示模型设定偏差风险,为股市择时提供了可复现的量化框架。资源为PDF文档,共1份约15页,压缩包大小约1.06MB,已有102人学习下载,适合金融工程研究者及量化策略开发人员研读参考。
1. 从“看对方向”到“做对仓位”:A股多维度量化择时到底在解决什么问题
做 A 股的人大多有过这种经历:宏观判断对了,指数也确实涨了,但自己拿的票没动;或者技术指标金叉了,追进去就套在短期高点。说白了,单一维度的择时信号在大类资产联动、市场情绪反复切换的 A 股里,稳定性很差。所谓“A股多维度量化择时系统”,核心思路是把价格、资金、情绪、日历效应等不同来源的信号拆开计算,再用一套固定的合成规则变成最终的仓位建议,最后用历史数据回测验证这套规则是否真的能跑赢简单持有。这套方案适合两类人:一是自己做量化策略、想补择时模块的开发者,二是做资产配置、需要用仓位信号控制回撤的投研人员。需要明确的是,择时系统不追求预测每一天的涨跌,它追求的是在关键拐点上给出可执行的仓位变化,把“看对”转化成“做对”。
一个容易被忽视的事实是:纯多头的 A 股择时,信号的价值不在于收益率有多惊艳,而在于它能不能把最大回撤压到可承受的范围。回测里常见的“年化 30%”如果没有回撤约束,实盘很难拿得住。所以这篇笔记会从因子选择、合成方法、回测框架到实盘前的避坑,完整拆一遍这套系统的构建路径。
2. 多维度信号从哪来:价格、资金、情绪与日历效应的因子设计
2.1 因子维度的选型逻辑:为什么是这四个方向
A 股择时系统里,因子维度不是越多越好,而是要覆盖不同的信息源。常见做法是围绕四个维度展开:价格趋势、资金流向、市场情绪、日历效应。价格趋势反映的是市场自身的中短期动量与反转特征;资金流向反映的是增量资金的态度,尤其是北向资金和两融余额这类可观测的高频数据;市场情绪反映的是参与者行为,比如换手率、涨停家数、成交额占比;日历效应则捕捉 A 股特有的季节性规律,比如春节前后、季末考核期、重要会议窗口。这四个维度跟基本面数据相比有一个共同优势:都是周频或日频可得的,不需要等待财报披露,天然适合择时系统的高频调仓需求。
选维度的标准有三条:第一,逻辑上能解释,不能是纯粹的统计相关性;第二,数据可得且序列足够长,至少覆盖一轮完整的牛熊;第三,维度之间相关性要低,否则合成之后等于重复加权了同一个信息源。这四个维度里,价格趋势和情绪面相关性稍高,但前者偏慢、后者偏快,放在一起反而能构成快慢搭配。资金面与宏观流动性相关,日历效应则是完全独立的时间维度,这四者组合起来,基本覆盖了 A 股的主要波动来源。
2.2 每类因子的计算口径与参数设定
因子计算是整个系统的地基,口径错了,后面所有合成和回测都会失真。先说价格趋势因子。常见做法是取沪深300或中证500指数收盘价,计算三条均线:20日、60日、120日,再定义一个趋势得分。这个得分不是简单的金叉死叉,而是看短期均线在中期均线上方的持续时间和偏离幅度。一个容易踩的细节是:均线用收盘价还是用均价(VWAP)?我建议用收盘价,因为在做日频择时时,收盘价是最终可执行的价格基准,和回测撮合逻辑一致。
资金面因子里,北向资金净流入和两融余额变化是用的最多的两个代理变量。北向资金数据可以从交易所公开数据抓取,但要注意口径变更——2024年后北向资金实时数据调整为按季度披露,日频数据就得改用其他代理,比如成交额占比或行业龙头的资金流向。两融余额的缺点是更新滞后一天,且受监管政策影响大,但它的趋势性比北向资金更强,适合作为慢变量。这两个因子在代码里都要做标准化,不能直接用绝对值。
情绪面因子建议用三个:全市场换手率(成交额/流通市值)、涨停家数(剔除ST和上市未满60日的新股)、以及上涨家数占比。换手率是很好的情绪温度计,A股见顶时往往伴随换手率飙升;涨停家数反映的是短线赚钱效应,这个数据在情绪极端日会快速归零或爆量,需要做平滑处理。日历效应因子则用一个二维结构:月份效应和节假日前后效应。月份效应按过去十年每月收益率的正负比例编码,节假日效应取长假前5个交易日和后3个交易日的统计特征。
2.3 因子的平滑与标准化:让信号可比较
因子裸数据不能直接用,因为不同因子的量纲和波动区间差异太大。比如换手率可能在0.5%到3%之间波动,而均线偏离度可能在-5%到+8%之间。不做标准化的话,合成权重会被高波动的因子主导。我习惯的分两步:第一步是去极值,用滚动窗口的MAD(中位数绝对偏差)方法把极端值压缩到3倍中位数偏差以内,避免单日极端行情对信号造成跳跃;第二步是Z-Score标准化,窗口长度选120个交易日,和均线周期保持一致,这样各个因子的输出都变成均值为0、标准差为1的序列,可以直接加权合成。
标准化之后还有一个容易被忽略的步骤:因子降噪。日频数据的噪声很大,直接用原始Z-Score会让信号频繁翻转。常见做法是对标准化后的因子再做一次5日指数移动平均,平滑掉短周期的毛刺。这里要注意,平滑窗口不能太长,否则因子的及时性会变差,择时系统最怕的就是信号出来后已经错过一段行情。
3. 信号合成与仓位映射:把多因子变成一个可执行的仓位值
3.1 加权合成方法:等权、IC加权与滚动回归的比较
因子合成是多维择时的核心环节。最保守的做法是等权合成,把四个维度的Z-Score直接平均。这种做法的好处是稳健,不会因为某一段行情里某个因子失效而大幅拖累整体信号;缺点是放弃了因子在不同市场状态下的强弱差异。稍微进阶一点的做法是IC加权,即用每个因子过去一段时间(比如60个交易日)与未来5日收益的秩相关系数(Spearman IC)作为权重,IC越高权重越大。IC加权在趋势行情里表现更好,但在市场风格剧烈切换时容易追高因子、踩踏切换。
我实际工程里更倾向于做滚动回归加权,但加了两个约束条件:第一,权重必须非负,负权重意味着某个因子反向预测,这在样本外通常不可靠,不如直接归零;第二,单个因子权重上限设为0.4,防止某一个维度在局部行情里过度主导。滚动回归的窗口选120个交易日,预测目标是未来5个交易日的收益。这个设定需要做滚动,不能使用全样本回归,否则就是未来函数。等权、IC加权、滚动回归加权这三者之间的差异,在单边上涨行情里可能不大,但在震荡市里,滚动回归加权对突变信号的响应明显更平滑,这是选它的主要原因。
3.2 得分到仓位的分段映射:避免满仓空仓的频繁切换
合成得分本身是一个连续值,但实盘交易不能跟着连续值频繁换仓。需要用分段映射把得分变成离散仓位。这里有一个关键参数:调仓阈值。阈值太密(比如每隔0.1个标准差就调一次仓),交易成本会吃掉收益;阈值太疏(比如只有三档),又容易在临界点出现大幅仓位跳变。我一般设五档仓位:0%、25%、50%、75%、100%,并设置滞回区间(Hysteresis)。滞回的意思是:得分从低位上穿到0.5倍标准差时,仓位从25%加到50%;但要从50%降回25%,需要得分跌破0倍标准差,而不是跌回0.5倍以下。这样做能显著减少频繁调仓,回测里年化换手率能降三分之一左右。
滞回参数需要结合交易成本一起调。A股单边交易成本(佣金+印花税+滑点)按0.15%到0.2%估算比较现实。如果调仓阈值设得太小,信号每天翻转,一年下来交易成本可能吃掉5个点以上的收益。回测时要跑一组参数敏感性,看换手率和净值曲线的关系,确认分段映射没有让系统变成一个高频换手策略。
3.3 最小实现:一个可直接跑通的信号合成脚本
下面给出一段精简的因子合成代码,输入是四个因子的日频时间序列DataFrame,输出是每日的目标仓位序列。这段代码可以直接跑通,但参数需要根据自己的数据周期微调。
import pandas as pd import numpy as np def mad_clip(series, window=120, n=3): """MAD去极值:滚动窗口中位数绝对偏差截断""" median = series.rolling(window, min_periods=1).median() mad = (series - median).abs().rolling(window, min_periods=1).median() upper = median + n * 1.4826 * mad lower = median - n * 1.4826 * mad return series.clip(lower, upper) def zscore(series, window=120): """滚动Z-Score标准化""" mean = series.rolling(window, min_periods=1).mean() std = series.rolling(window, min_periods=1).std() return (series - mean) / std def composite_signal(df, weights): """ df: DataFrame,列分别为 ['trend', 'capital', 'sentiment', 'calendar'] weights: dict,各因子权重,例如 {'trend': 0.3, 'capital': 0.3, 'sentiment': 0.2, 'calendar': 0.2} """ df = df.copy() # 去极值 + 标准化 for col in df.columns: df[col] = mad_clip(df[col]) df[col] = zscore(df[col]).ewm(span=5, adjust=False).mean() # 加权合成 total = sum(weights.values()) score = sum(df[col] * weights[col] for col in df.columns) / total return score # 仓位映射,带滞回区间 def map_position(score, up_thresh=0.5, down_thresh=0.0): position = np.zeros(len(score)) current = 0.0 for i in range(len(score)): if current <= 0.0 and score.iloc[i] > up_thresh: current = 0.25 elif current <= 0.25 and score.iloc[i] > up_thresh + 0.3: current = 0.5 elif current <= 0.5 and score.iloc[i] > up_thresh + 0.6: current = 0.75 elif current <= 0.75 and score.iloc[i] > up_thresh + 0.9: current = 1.0 elif current >= 1.0 and score.iloc[i] < down_thresh - 0.1: current = 0.75 elif current >= 0.75 and score.iloc[i] < down_thresh + 0.2: current = 0.5 elif current >= 0.5 and score.iloc[i] < down_thresh + 0.1: current = 0.25 elif current >= 0.25 and score.iloc[i] < down_thresh: current = 0.0 position[i] = current return position这段代码里,mad_clip负责压缩极端值,zscore配合ewm在标准化的同时做了5日平滑,map_position里上穿阈值和下穿阈值不一致,这就是滞回逻辑。权重参数在图上文提到的约束范围内微调,每个因子的权重不能为负,单因子不超过0.4。跑完合成后,检查score序列的直方图,确认分布接近正态、没有长尾,再做下一步回测。
4. 回测框架搭建:从净值计算到绩效指标,还原真实交易约束
4.1 回测引擎选型与撮合细节
择时系统的回测有两种路线:向量化回测和事件驱动回测。向量化回测适合策略逻辑简单、调仓频率低的场景,代码量小,算得快;事件驱动回测适合需要精细控制下单逻辑、冲击成本和极端行情处理的场景,但代码复杂度高一个量级。对于日频调仓的多因子择时,向量化回测足够。关键不是引擎本身,而是撮合逻辑里的几个约束条件必须写对:涨跌停不能成交、停牌不能成交、成交量限制、手续费和印花税。
涨跌停约束是最容易被忽略的。A股有10%(创业板和科创板20%)的涨跌停限制,回测里如果信号在涨停日发出买入指令,实际是买不进的,因为卖单极少。处理方式是在回测数据里加上涨跌停标记,遇到涨停且买入信号时,跳过本次买入;遇到跌停且卖出信号时,同样跳过。停牌也是类似逻辑,直接用当日成交量或成交额为0来判断即可。这些约束如果省略,回测收益会虚高,尤其在捕捉反弹行情的择时策略里,虚高可能达到一倍以上。
4.2 交易成本模型:佣金、印花税与滑点怎么一起算
交易成本的设定直接决定回测结果是否可信。A股现在的费率结构是佣金双边万1到万3,卖出收印花税千分之0.5(2023年8月调整后),过户费双边万0.1。实盘还要加上滑点,尤其在做指数择时时,买卖一篮子成分股的冲击成本不容小觑。我常用的成本模型是:买入成本 = 佣金万2 + 滑点万5,约万分之7;卖出成本 = 佣金万2 + 印花税万5 + 滑点万5,约万分之12。注意印花税只在卖出收取,回测里如果买卖用同一个费率,会低估卖出成本、让换手率高的优化结果显得可行,这是回测里最常见的翻车点之一。
滑点模型可以简单设成固定比例,也可以按历史波动率建模:当日开盘价相对前收的跳空幅度反映隔夜滑点,日内成交量分布反映冲击成本。更精细的做法是用VWAP偏移量来模拟,但对日频择时系统,固定比例的滑点加上灵敏度测试已经足够。关键是要在回测报告里同时给出“净成本”和“毛收益”,两者差距太大时,策略就没有实盘价值。
4.3 最小实现:一次完整回测需要哪些模块与结果指标
一次完整的择时回测,至少要输出下面这些模块:策略净值曲线、基准净值(一般是沪深300)、仓位变化序列、逐年收益表、最大回撤区间、以及换手率统计。净值计算的基础是当日收益率乘以昨日仓位。举个简化流程:假设信号在T日收盘后生成,T+1日按开盘价调仓,当日的收益贡献是T日收盘到T+1日收盘的指数涨幅乘以T+1日的仓位。这里的调仓时点必须遵守“信号生成次日执行”原则,不能当日信号当日成交,这是前视偏差最常见的来源。
绩效指标方面,我建议记录五组:年化收益率、年化波动率、夏普比率、最大回撤、卡玛比率(年化收益/最大回撤)。其中卡玛比率对择时系统尤其重要,因为它直接刻画了收益与回撤的性价比。另外要单独统计空仓期的占比——一个择时系统如果空仓时间超过60%,说明信号阈值太保守,虽然回撤小,但资金利用率太低,实盘很难拿得住。
import pandas as pd import numpy as np def backtest(price, position, cost_buy=0.0007, cost_sell=0.0012): """ price: 指数日收盘价序列 position: 目标仓位序列,0到1之间 """ ret = price.pct_change().fillna(0) # T日生成的仓位,T+1日生效 pos_eff = position.shift(1).fillna(0) # 调仓日检测 turnover = pos_eff.diff().abs().fillna(pos_eff) # 交易成本:买入按调仓增量计算,卖出按调仓减量计算 cost = np.where(turnover > 0, turnover * np.where(pos_eff.diff().fillna(0) > 0, cost_buy, cost_sell), 0) cost = pd.Series(cost, index=ret.index) strategy_ret = pos_eff * ret - cost nav = (1 + strategy_ret).cumprod() bench_nav = (1 + ret).cumprod() return nav, bench_nav, turnover def max_drawdown(nav): """计算最大回撤及区间""" peak = nav.cummax() drawdown = nav / peak - 1 end_idx = drawdown.idxmin() start_idx = nav.loc[:end_idx].idxmax() return drawdown.min(), start_idx, end_idx这段回测代码里,pos_eff = position.shift(1)是防止前视偏差的关键,当天生成的仓位必须等到下一交易日才生效。np.where区分买卖成本,避免统一费率带来的低估。跑完回测后,逐年收益比总收益更重要——很多策略总收益漂亮,但拆开看某一年特别赚钱、其他年份平庸,这种时间集中度高的收益,实盘意义要打折扣。
5. 回测与实盘之间的坑:多维择时最容易翻车的五个细节
5.1 前视偏差:信号当天生效导致回测收益虚高
现象:回测曲线平滑向上,最大回撤极小,但同一套参数在模拟盘里连续两周跑输回测结果,差异达到20%以上。原因:最典型的是因子计算里用了当日收盘后才知道的数据,但回测撮合却假设当日能以收盘价成交。比如用“当日北向资金净流入”作为当日买入信号,实际上北向资金全天净流入数据盘后才出,盘中根本不知道收盘值,这就是必然的未来函数。解决:把因子计算、信号生成、仓位生效拆成三个独立时点,严格执行信号T日生成、T+1日开盘执行。更稳妥的做法是连开盘价都不用,用T+1日的收盘价或VWAP,虽然收益会打折,但更接近可实现的业绩。
5.2 涨跌停和停牌股未处理,回测净值被高估
现象:回测里某个月净值大涨,翻交易记录发现当天买入的标的正处于涨停状态,模拟时根本买不进去。原因:策略的回测数据只用了指数收盘价,没有叠加成分股的涨跌停限制和停牌信息。指数本身不会涨停,但按指数点位下单时,实际买入的一篮子股票可能因为部分成分股涨停而无法按预设权重建仓。解决:回测数据里除指数收盘价外,必须至少带上指数的成交额和当日涨跌幅是否触发涨跌停的标记。更精细的做法是按指数成分股权重,用个股的成交情况做一层过滤,当天涨停的个股权重在调仓时强行置为0,剩余权重归一化后再计算指数收益。
5.3 参数过拟合:滚动回归权重在样本外失效
现象:回测期内的收益曲线漂亮,但把回测窗口挪到另一段年份,策略很快失效,甚至出现连续亏损。原因:滚动回归加权阶段的窗口参数和约束条件是在全样本上调出来的,本质上已经用到了样本外信息。选120日窗口、0.4权重上限这些参数,如果是在看完整个回测表现后倒推出来的,那它们同样属于未来函数。解决:把数据切成三段:训练段用于确定权重和阈值,验证段用于测试滞回参数,测试段完全不参与调参、只跑一次作为最终报告。分段比例建议6:2:2,时间上按先后顺序切,不能随机抽样,因为择时序列有强自相关性。
5.4 调仓成本被低估:只算佣金不算滑点和冲击
现象:回测里年化收益20%,换手率约400%,看着不错。但实盘跑下来收益只剩8%,主要亏在买卖价差上。原因:回测成本只按费率算,没有考虑调仓当日的开盘跳空和盘口深度问题。当信号集中发出时(比如连续多日加仓),实际买入会推高成交价格,这个损耗在回测里是零。解决:成本模型的滑点不能只设一个固定常数,要按滚动波动率折算。波动率高的时段滑点自动放大。同时可以加入调仓限制:单日仓位变化不超过20个百分点,用减少交易频率换取更低的冲击成本。
5.5 指数复权方式错误导致信号失真
现象:换手率和均线因子回测结果异常,信号频繁翻转,且与人工看图判断完全相反。原因:有些数据源的指数行情只做了前复权,除权除息日会出现价格跳空,均线因子被这些跳空干扰,产生虚假趋势信号。解决:指数择时一律用全收益指数(包含分红再投资)或直接用真实点位数据,并检查因子计算所用的数据源里指数在分红日的涨跌幅是否为0。如果发现某日指数振幅极小但均线偏离度剧变,基本就是复权处理出了问题。这个坑在创业板指数和红利类指数上特别常见。
6. 落地前的最后一步:样本外验证与参数灵敏度检查
系统回测结束不代表工作完成,真正决定这个择时系统能不能投入模拟盘甚至实盘的,是样本外验证和参数灵敏度分析。我一般会做一个“滚动再拟合”测试:每3个月重新跑一次回测,用最新数据更新因子权重,然后观察两个指标——样本外超额收益是否稳定、权重在滚动中的漂移幅度是否过大。如果每次滚动后权重变化超过50%,说明因子本身不稳定,系统对参数太敏感,实盘里会不断出现信号置信度下降的问题。
一个具体习惯:把回测报告按年份拆分,重点看每一年的最大回撤发生时间和原因。如果某年的回撤恰好集中在权重最高的因子失效期,说明系统对单一信息源依赖过大。通过日志查看每次调仓时各因子的贡献值,可以定位是哪一类因子主导了这轮仓位下调,这比看净值曲线直观得多。把这些记录放在监控面板里,每天盘后自动更新一次信号状态和因子拆解,连续跟踪两个月后,你对这套系统的脾气就摸清楚了。这套系统的价值不在于预测准确率有多高,而在于它的决策过程透明、可复盘、可干预——这也是量化择时相比主观判断最大的优势。
关于择时系统,我的实际教训是:回测里越精确的东西,实盘里越可疑。留好安全边际,用0.5个标准差的变化作为调仓触发条件,比用一个精确到小数点的信号值可靠得多。希望帮到你。
本文还有配套的精品资源,点击获取