ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

温水煮青蛙策略:基于信息离散度与动量效应的量化选股实现

温水煮青蛙策略:基于信息离散度与动量效应的量化选股实现 简介一份聚焦金融工程量化策略研究的PDF资料面向具备金融工程与量化投资基础的研究人员、基金经理与量化分析师目的是理解信息离散度如何影响动量效应并以此优化策略选股逻辑。内容基于“温水煮青蛙”理论通过构建信息离散度指标将股票划分为信息连续型与离散型实证发现连续型股票动量组合收益可达5.94%离散型则为-2.07%同时验证了有限关注度与分析师反应滞后等行为学解释。文档附带完整Python代码按数据准备、信息离散度计算、动量因子构造、分组检验、回归分析等模块展开均可直接运行并用于复现论文主要结论也可结合真实市场数据二次开发。整个资源为单个PDF文件压缩包约787KB当前已有115人学习浏览适配专题研读与量化策略实证场景。1. “温水煮青蛙”理论在量化里到底指什么渐变市场才有的信息差红利你大概见过这种行情一只票连续三天每天涨不到1%你觉得它没劲第四天涨了2%你开始关注第五天突然放量拉升你终于追进去接下来就是连续阴跌。这就是典型的“温水煮青蛙”走势。金融工程信息离散度与动量效应分析里这类渐变行情其实可以被结构化水温在慢慢升高青蛙没有挣扎等它挣扎时水温已经失控。基于“温水煮青蛙”理论的量化投资策略核心不是预测那只青蛙什么时候跳而是测量“水温和青蛙反应的差值”。这篇文章的目标读者是正在做中低频日线或周线选股的量化从业者和小团队你们不需要高频柜台只需要日线收盘价、行业分类和一套能落地的因子代码。我们先把信息离散度形态讲清楚然后直接给出可复现的Python量化交易策略代码最后讲参数和踩坑。2. 信息离散度因子用分歧度识别“青蛙没察觉”的窗口2.1 信息离散度的三种度量方式与选型理由信息离散度这个说法在金融工程里没有唯一标准定义常见做法有三种。第一种是用分析师盈利预测的分歧度来度量同一只股票覆盖的分析师给出的目标价或EPS标准差越大说明市场对它的认知越分裂。这个数据本身没问题但个人量化很难稳定获取而且分析师预测一个月才更新几次等分歧度明显变化时渐变行情往往已经走了一半。第二种是用收益率产生的分歧个股日收益相对其所在行业当日均值的绝对偏离再在时间轴上滚动平均。这个指标只需要行情数据每天更新能即时反映一只股票是否开始“脱离集体”走出独立行情。第三种是量价离散度把换手率、振幅和成交量偏离叠进去问题在于换手率受事件影响很大股东大会、解禁都会制造假离散对渐变行情来说噪声太多。我一般选第二种并且用行业口径而不是全市场口径。如果用全市场均值做基准遇到核心资产单边行情时大部分股票都在跌、少部分在涨全市场离散度会被风格分化主导离散度因子等于在赌风格。换成行业内均值后风格影响被剥掉一层剩下的才是“同类型资产之间的分歧”。在A股用申万一级行业就够了二级行业太细会把样本切碎很多行业一天不到20只股票离散度算出来不稳定。这里先把数据需求说清楚一张日线表至少要有date、code、industry、close四列收益率由close计算后面所有因子都从这张表派生。把“温水煮青蛙”映射到指标上渐变早期个股开始慢慢走出独立行情但偏离行业均值很小disp处在低位缓慢抬升到了加速期股价开始放量拉升偏离迅速扩大disp陡峭上升。所以策略要找的不是disp最低的静态股票而是“disp低且动量已经确认向上”的共振状态。这一点决定了后面双因子打分的权重方向动量给正分离散度给负分但负分的惩罚不能太大否则直接把所有活跃股都排除掉。2.2 用Python计算行业信息离散度因子最小可用实现先假设输入表df已经包含date、code、industry、close四列。下面是完整函数import pandas as pd import numpy as np def calc_info_dispersion(df, window20): 计算行业信息离散度因子 输入df: date, code, industry, close window: 滚动天数默认20个交易日 返回值: 新增 ind_ret, dev, disp 三列 df df.copy() df[date] pd.to_datetime(df[date]) df[ret] df.groupby(code)[close].pct_change() # 行业内当日收益均值 df[ind_ret] df.groupby([date, industry])[ret].transform(mean) # 个股与行业均值的绝对偏离 df[dev] (df[ret] - df[ind_ret]).abs() # 按股票滚动20日平均作为信息离散度 df df.sort_values([code, date]) df[disp] ( df.groupby(code)[dev] .rolling(window) .mean() .reset_index(level0, dropTrue) ) return df逻辑说明ret是单日涨跌幅ind_ret是同一行业内所有股票当天收益的等权平均dev取绝对值后代表“这只票今天和行业集体行动的偏差”。rolling窗口沿股票自身的时间轴做滚动均值等于把过去20个交易日的每日偏离做平均。如果一只票跟着行业走dev每天都很小disp就压在低位如果它开始连续走出独立行情disp会逐渐抬起来。这个“慢慢抬起来”的过程就是温水升温的视觉化表现。参数说明window默认20代表一个交易月。想抓更快的变化可以改10但改小之后单日事件冲击会直接顶高因子值信号抖动会明显变大改到40会更平滑代价是渐变行情已经走了两个月你才确认中间会错过一大段。A股中低频策略我建议先试20后面回测再和40对比不要一上来就贪平滑。常见做法里还有“用平方偏差替换绝对值”的版本。如果是做横截面选股平方会让一次5%的偏离压倒二十个0.5%的小偏离等于把“渐变”掩盖成“突变”和温水煮青蛙的逻辑相反所以这里刻意不用平方。若你的研究方向是事件驱动那平方版本另说。2.3 因子有效性检验分层回测与ICIR因子造出来不等于能用先做两个最便宜的检验IC分析和分层回测。先算未来5日收益作为标签注意这是标签不是特征用它当y不构成未来函数def calc_fwd_ret(df, fwd5): df df.copy() df df.sort_values([code, date]) df[fwd_ret] ( df.groupby(code)[close].transform(lambda x: x.shift(-fwd) / x - 1) ) return df这段代码把未来第5天的收盘价除以今天的收盘价减1得到一个可对齐收益。参数说明fwd5是5个交易日和调仓周期保持一致。如果你后面用10日调仓fwd也改成10。然后做IC分析def factor_ic(df, factor_coldisp, fwd_colfwd_ret, date_coldate): 计算每日截面IC并汇总ICIR df df.dropna(subset[factor_col, fwd_col]) ic_series df.groupby(date_col).apply( lambda g: g[factor_col].corr(g[fwd_col]), include_groupsFalse ) ic_series ic_series.dropna() ic_mean ic_series.mean() ic_std ic_series.std() icir ic_mean / ic_std return ic_mean, ic_std, icir, ic_series逻辑说明这里每天算一次“所有股票的disp与它们未来5日收益的相关性”corr默认是Pearson先跑Pearson看线性方向没问题。ic_mean是全年IC均值反映因子整体是否有效ic_std是IC波动icir是两者比值类似收益的夏普衡量因子稳定性。我做因子初筛时icir绝对值大于0.3才继续往下走小于0.2的基本放弃。注意这里的预期方向如果ic_mean明显为负说明低disp的股票未来跑赢温水煮青蛙逻辑成立如果为正说明市场在奖励分歧那这套策略的前提就不成立趁早换方向。分层回测是更直观的验证def layer_test(df, factor_coldisp, fwd_colfwd_ret, date_coldate, q5): 按每日因子值分5层看每层平均未来收益 df df.dropna(subset[factor_col, fwd_col]) df[layer] df.groupby(date_col)[factor_col].transform( lambda x: pd.qcut(x, q, labelsFalse, duplicatesdrop) ) layer_ret df.groupby(layer)[fwd_col].mean() return layer_ret参数说明qcut把每天的因子值均匀切成5档layer0代表当天disp最低的那组layer4代表最高组。返回的layer_ret如果从0到4单调递减说明离散度确实越低越好如果中间高两头低说明因子是非线性U型后面打分不能直接做线性rank要改成两档映射或者U型加权。很多团队因子选了一堆最后不行就是卡在这一步没有先看单调性。在把这套离散度因子放进模型前建议顺手看一眼它与市值因子的相关性。A股小市值股票天然波动大disp也会偏高如果不做市值中性化低离散度组合可能会变成隐性的大盘价值组合。常见做法是截面回归剥离用当日disp对log市值和行业虚拟变量做一次最小二乘回归取残差作为中性化后的因子。这一步会让策略在小市值风格行情里少赚一点但能避免风格切换时净值剧烈回撤。代码只在本地跑用statsmodels的add_constant配合OLS即可注意残差仍然需要做滚动窗口平滑不能直接把单日残差当因子。3. 动量效应合并温水煮青蛙策略的双因子打分与仓位控制3.1 动量效应为什么在低离散度下才可靠动量效应本身是老话题过去一段时间的强势收益会在未来延续学术上叫12-1动量也就是用过去12个月剔除最近1个月的收益做排序。但单纯追动量在A股很容易被“见光死”抽耳光因为很多强势股在拉升末期是靠情绪推动多空分歧剧烈散户和游资都在换手动量因子捕捉到的其实是最后一棒。那么离散度在这里就起到了过滤作用。我把动量拆成两段来看短动量看的是最近几天容易受跳空、涨停板和消息面刺激噪声大长动量看的是趋势位置。温水煮青蛙策略要用的是“中长动量已经形成但市场还没有为这只票吵起来”的状态。一个常见做法是过去60日累计收益不错但最近5日的短期热度没有异常飙升且同行业内收益离散度低。这样“水温”在升“青蛙”没跳才是入场窗口。动量口径窗口捕捉标的主要风险短动量5日短期热度追高、情绪反转中动量20日近一个月相对强弱震荡市频繁切换长动量剔除近期60日剔除近5日缓慢趋势错过急拉加速段本策略选最后一行60日剔除近5日。它把渐变段保留下来又自动过滤掉最后5天已经明显加速的股票。如果你做的标的是期货或者ETF建议把窗口分别改成30日和10日因为商品市场衰减比股票快60日动量往往已经过期。动量计算之前还要做一次样本清洗剔除ST、上市不满60个交易日的次新股、以及调仓日处于停牌或涨跌停状态的股票。原因很直接ST股涨跌幅只有5%动量和离散度都被压缩次新股没有足够的历史窗口动量值本身就是残缺的。清洗逻辑就一行df df[~df[code].str.startswith(ST)]但如果你的表格里没有标记需要先维护一张ST名单。这步做晚了后面的权重全部建立在不可成交的股票上回测曲线再漂亮也和自己无关。3.2 动量因子与离散度因子的时间对齐这里有个容易被忽略的细节离散度窗口20日动量窗口60日两者结束日期必须对齐到同一个交易日。如果动量从60日开始累加、离散度从20日开始累加但数据表里没有统一shift最后打分时就会把不同时间点的信号混在一起。我的做法是统一在t日收盘后计算输出因子值标到t日但t日本身不参与当日交易最早t1日按信号执行。也就是说计算代码里要保证所有窗口的右端点都是t不能用t1的数据。还有像pct_change这样的函数默认用到今天收盘价今天的涨幅在收盘后才能确认所以回测里当天信号必须shift(1)。动量因子计算代码如下def calc_momentum(df, price_colclose, lookback60, skip5): 计算剔除最近skip日的lookback日动量 df df.copy() df df.sort_values([code, date]) df[total_ret] ( df.groupby(code)[price_col].transform( lambda x: x / x.shift(lookback skip) - 1 ) ) df[recent_ret] ( df.groupby(code)[price_col].transform( lambda x: x / x.shift(skip) - 1 ) ) df[mom] df[total_ret] - df[recent_ret] return df逻辑说明total_ret是65个交易日前的收盘价到今天涨了多少recent_ret是5日前到今天涨了多少两者相减近似等于从65日前到5日前的涨跌。更严谨的做法是分别取两个时点的价格相除但用净值比减法在收益率较小时足够接近而且实现更简单。参数说明lookbackskip是实际前驻点比如60565如果遇到停牌这个比例会失真需要在清洗时把停牌日的前后缺口补齐这里先不管复权。3.3 双因子打分什么时候算“水在温但青蛙没反应”def compose_score(df, mom_colmom, disp_coldisp, mom_w0.6, disp_w0.4): 双因子截面打分 mom_pct: 动量截面百分位越高越好 disp_pct: 信息离散度截面百分位越低越好 score: 最终综合得分 df df.copy() df[mom_pct] df.groupby(date)[mom_col].rank(pctTrue) df[disp_pct] df.groupby(date)[disp_col].rank(pctTrue) df[score] mom_w * df[mom_pct] - disp_w * df[disp_pct] return df逻辑说明rank(pctTrue)把动量转成0到1的相对位置消除不同时期市场整体强弱带来的漂移。离散度同样是截面排名但方向相反用负号。mom_w和disp_w是显式权重参数0.6和0.4是初值含义是动量可信度略高于离散度因为离散度已经做过行业剥离仍然可能受个别行业极端值干扰。如果你把两个权重改成各0.5组合会更保守买入池会更少后面回测里夏普会变高但交易次数下降。权重该谁大谁小不是靠感觉而是看前面2.3里两个因子各自的ICIRICIR大的给更高权重更合理。参数说明这个score在每期末做一次截面排序买入top10%的票卖出已不在池内的旧持仓。阈值0.7/0.3是基于分数分布经验值但实际分布每天不同最好直接按百分位选股票而不是按绝对分数阈值否则牛市里score普遍偏高、熊市里普遍偏低信号数量会随行情波动。3.4 仓位管理与调仓代码别把一只青蛙煮没了def build_weights(df, score_colscore, top_n10, max_weight0.1): 每期选score最高的top_n只票 单票权重上限max_weight行业暴露上限25% df df.copy() df df.sort_values([date, score_col], ascending[True, False]) df[rank] df.groupby(date).cumcount() 1 df[in_pool] df[rank] top_n # 行业中性单行业暴露不超过25% industry_w df.groupby([date, industry])[in_pool].transform(mean) df[cap_weight] np.where(industry_w 0.25, 0.25 / (industry_w * top_n), 1) df[weight] df[in_pool] * df[cap_weight] / top_n df.loc[df[weight] max_weight, weight] max_weight df[weight] df[weight] / df.groupby(date)[weight].transform(sum) return df逻辑说明rank按日期分组做序号in_pool决定这只票在不在组合里。cap_weight处理行业暴露如果某行业入选股票数量占比超过25%就把该行业内每一只入选票的权重压回0.25除以上限内票数。最后一步归一化让所有持仓权重和等于1保证满仓。这里不要小看行业暴露控制离散度是按行业算的如果某段时间一个行业整体低波动组合会自动偏到那个行业净值曲线会变成行业beta曲线而不是alpha曲线。参数说明top_n10意味着每期只持有10只票组合集中度高单票暴雷影响大。实盘我建议按比例选择比如每期持有全市场股票数的前5%并将上下限固定在8到30只之间。max_weight0.1限制单票权重如果top_n10理论上每只票0.1等于没限制所以max_weight只在极端集中时生效。调仓周期建议5到10个交易日太短会被印花税和滑点吃掉。4. 回测与参数敏感度先看收益曲线再看参数会不会翻车4.1 向量化回测的最小框架这里先提供一个不含滑点成本的向量化回测函数目的是快速验证因子方向不是替代商业回测平台def run_backtest(df, weight_colweight, ret_colret, rebalance5): 向量化回测每rebalance日调仓一次权重在调仓日更新 df df.copy() df[date] pd.to_datetime(df[date]) rebal_dates sorted(df[date].unique())[::rebalance] df[effective_w] df.groupby(code)[weight_col].transform( lambda x: x.where(df.loc[x.index, date].isin(rebal_dates), None) ) df[effective_w] df.groupby(code)[effective_w].ffill() df[effective_w] df[effective_w].fillna(0) df[port_ret] df[effective_w] * df[ret_col] net_ret df.groupby(date)[port_ret].sum() return net_ret逻辑说明第一步先把所有调仓日提取出来权重只在调仓日更新。第二行where把非调仓日的权重设为None然后按股票做ffill意思是中间持有期权重保持不变这和实际持仓一致。port_ret是每只股票当日贡献的收益最后按日期加总得到组合每日收益。这个框架没有处理资金约束也没有融资融券适合先看beta和相对强弱。如果净收益曲线在某种参数组合下年化超过50%先别高兴多半是涨跌停假成交把净值抬高了。参数说明rebalance5对应周频调仓改成20就是月频。代码里sorted会把日期字符串排好所以date列先转成datetime最稳妥。ffill有一个隐蔽坑如果某只股票在调仓日之后才上市它前一段权重是NaNfillna(0)会把这段时间当作空仓处理实盘遇到这种情况正确做法是排除新股而不是填空仓。4.2 必调参数冷启动、调仓周期、买入阈值把上面策略的五个核心参数集中起来看一个表参数默认值调小调大典型失效信号离散度窗口 window20信号灵敏、单日冲击大反应慢、错过渐变ICIR持续低于0.2动量窗口 lookback60短期热点吃大趋势震荡市连续亏损跳过天数 skip5易受跳空干扰渐变段丢失净值回撤加深调仓周期 rebalance5换手升高持仓钝化组合换手率30%买入池 top_n10集中、波动大分散、收益稀释权重频繁触及上限参数说明调仓周期是最先要动的。A股单边交易成本至少按0.3%算如果你把rebalance从5改到1年化换手率大概率翻倍以上收益不一定变高手续费先把利润磨掉。这个表的价值在于给了你一个“失效信号”检查清单出现对应现象时优先调整的是该参数而不是全盘重搜网格。冷启动问题单独说。动量窗口60日加离散度窗口20日意味着样本前65个交易日没有有效因子值。如果直接dropna前三个月就会空仓看起来很亏如果用ffill把NaN填充成第一个有效值等于用未来信息偷跑回测净值会虚高。正确做法是空仓等待把NaN权重直接置0等到第一个有效调仓日再进场。这条规则写进回测不要写在策略逻辑里否则换平台后很容易被平台的缺失值处理默默改掉。注意任何回测脚本里处理NaN的逻辑都要单独检查一遍尤其是ffill和fillna。这两个函数是未来函数的重灾区。4.3 滚动前推验证比一次性样本切分更抗过拟合很多人在策略验证时把数据按时间切成前70%训、后30%测一次定参数。这个做法的问题在于参数在训练段被拟合测试段只能验证一次碰上运气好就得出一个不可重复的高夏普。我习惯用滚动前推也叫walk-forward每年用过去3年数据定一次参数次年按这个参数跑然后再滚动到下一段。这样每次决策都只用历史信息和实盘调参路径一致。代码上可以用一个简单循环按年滚动调参并在次年回测最终把每年验证段收益拼接起来。这一段不做复杂并行先把思路跑通years sorted(df[year].unique()) results [] for i in range(3, len(years)): train_years years[i-3:i] test_years [years[i]] # 在训练段上微调参数选出夏普最高的(disp_w, lookback) best_params grid_search(df[df[year].isin(train_years)]) # 用best_params在测试段重算因子和持仓 test_net backtest_with_params(df[df[year].isin(test_years)], best_params) results.append(test_net)逻辑说明训练段年份数固定为3年逐年往前滚测试段永远在训练段之后不会混入未来信息。grid_search不要用太宽的网格建议每次只调两个参数lookback和disp_w其他保持默认。参数说明滚动步长是一年过于频繁会导致参数每天在变实盘根本来不及执行一年一调对中低频策略比较务实。还有一类坑在“参数网格搜索”。常见做法是把window、lookback、top_n、rebalance、mom_w全部纳入网格枚举几千组参数选出回测夏普最高的一组。这种全空间搜索很容易找到一组在历史上完美贴合的参数但它本质上是在拟合噪声。我一般限定每次只动一个参数其他保持默认然后看这个参数的“收益-参数”曲线如果最大值出现在搜索边界上说明这个参数还没有被真正约束如果曲线在中间形成明显平台平台中央的值往往比最尖峰的值更可靠。4.4 换手率与交易成本净值曲线的隐形杀手换手率通常被忽略直到实盘跑不出来才后悔。回测里把调仓日的组合持仓变化统计出来def calc_turnover(weights, date_coldate): 调仓日组合换手率 pivot weights.pivot_table(indexdate_col, columnscode, valuesweight, aggfuncfirst) diff pivot.diff().abs().sum(axis1) return diff逻辑说明pivot把权重矩阵转成日期乘股票diff计算相邻调仓日每只股票权重的绝对变化量按行求和就是这一期双边换手率。单边成本按0.3%估包括佣金、印花税和滑点。如果某期换手率为50%那么双边成本就是50%乘以两倍的单边成本等于0.3%的损耗一年调仓50次就是15个点的成本策略年化超额20%都不够扣。这个换手率统计要打印在回测结果旁边别只盯净值。回测的基准选择也影响结论。用中证500还是万得全A做基准超额收益的数值会差很多。这篇策略偏向中盘我一般用中证500加中证1000等权做基准至少要和基准对比超额别只看绝对收益曲线。5. 策略避坑信息离散度与动量策略的5个常见翻车现场这套策略如果只看因子公式几分钟就能跑通真正花时间的是把数据底子清理干净。这5个坑按出现频率排序是我个人实盘的翻车记录也是后来做因子复用时甩不掉的回忆。每一条都用“现象—原因—解决”写你在自己的代码里照方抓药即可。5.1 ST和次新股把分层收益整体掀翻现象因子分层回测里disp最低组未来收益反而最差IC符号飘忽不定前一天负后一天正。原因ST股涨跌幅只有5%波动天然被压缩disp被系统性压到最低组次新股上市初期没有行业可比逻辑动量又是从新股连板历史里算出来的两类股票把最低离散度组污染了。分层结果不再代表“行业内低偏离”而是代表“ST和次新股集中营”。解决在建表阶段就按状态标记剔除ST上市不满60个交易日直接过滤如果个股在调仓日前处于一字涨停或一字跌停当天也不允许进入持仓池。写代码时不要只做一个简单的isin过滤因为ST名单每个月会变最好维护一张状态表做每日join。自查方法统计最新一期最低disp组的ST占比超过10%就该洗数据了。5.2 涨停板假成交回测净值虚高现象回测年化50%实盘月月亏损对不上账。原因高动量高分票在很多交易日已经涨停回测默认以收盘价成交但实际上排队根本买不到。策略越是抓渐变上涨越容易在确认的当日遭遇涨停这个坑对动量策略尤其致命。很多开源回测框架的撮合逻辑是按当日收盘价可成交处理并没有模拟涨停板封单。解决回测引擎里加一条成交约束调仓日当日涨幅大于9.5%时把该票权重强制置0同时要求权重池在t日生成、t1日执行t日涨停的票到t1执行时不再计入。这个约束会砍掉一部分收益但换来的是可交易性。回测报告里单独列出“被涨停拦截的次数”如果这个次数占调仓总数的20%以上说明买入池集中到了过热股票策略逻辑需要往回调。5.3 行业均值引用了盘中不可得信息现象离线回测ICIR很高换成另一个数据源就跑不出来。原因计算ind_ret时用了当天包括收盘竞价在内的全行业收益均值如果数据表里已有复权因子或后复权价格某些字段实际上是收盘后才更新到全表的滚动窗口里就混进了未来。这类隐性未来函数不回读代码很难发现因为结果看起来只是一个普通的高IC。解决所有因子值统一用t-1日收盘可计算的数据。具体做法是计算完ret和ind_ret后整体shift(1)让因子值落到“已知”时点。宁可丢失一天的响应速度也不要让回测自欺欺人。自查方法把回测净值曲线和模拟账户净值曲线并排画如果两条曲线在每年一季度开始出现明显分叉优先怀疑数据对齐。5.4 动量窗口和离散度窗口时间错配现象打分组合在震荡市里频繁开仓亏损集中在小幅阴跌期。原因动量窗口60日、离散度窗口20日两个因子覆盖周期差距太大当短窗口因子先翻转时score会给出错误共振信号。动量还在提示向上离散度已经因为几天的回调快速攀升负分把score拖进卖出区下一期又反向来回打脸。解决把动量窗口设定为离散度窗口的3倍左右例如离散度20日、动量60日两者结束日对齐如果离散度改成40动量要跟着改成120而不是继续用60。保持窗口比例共振信号才稳定。另外给score加一个“最低动量门槛”动量百分位必须大于0.5才允许开仓否则即使score达标也不进池子。5.5 全网格搜索把参数过拟合当成了稳健现象参数搜索报告里某组参数年化45%、夏普3.2换相邻一组参数却只有年化8%。原因网格遍历的参数越多找到一组在历史上恰好匹配噪声的参数的概率越大。参数曲面如果出现尖锐峰值说明模型记住了历史样本的细节而不是一个稳定的规律。尤其是离散度和动量这类滚动窗口参数相邻窗口的因子值高度相关记录贴得太深。解决改用每次只调一个参数的单变量剖面分析并要求峰值不是一个点而是平台所有参数选择只在训练段完成验证段只能验证一次。用滚动前推重复三段以上每段都还有超额才值得上真金白银。如果某组参数在测试段年化高但最大回撤超过25%也建议直接放弃回撤过深的策略实盘拿不住。6. 防止因子失效离散度阈值的自适应校准技巧策略上线后最大的敌人不是参数选错而是市场风格变了因子还在原地。离散度因子的有效期和行业结构、流动性环境强相关一套固定阈值很难熬过两种以上行情。我的做法是给买入阈值加一个波动率自适应阀门。def adaptive_threshold(vol, base_th0.7): 根据市场波动率调节买入阈值 if vol 0.15: return min(base_th 0.05, 0.8) elif vol 0.25: return max(base_th - 0.1, 0.55) return base_th逻辑说明vol是过去20日全市场年化波动率。市场很平静时渐变行情稀少把阈值抬高到0.75甚至0.8只留下分数最高的少数票避免在低波动环境里频繁交易市场波动剧烈时动量信号快速翻转把阈值降低到0.55多给开仓机会防止因为阈值太高而错过整段趋势。base_th就是默认0.7这个参数和回测里的买入阈值保持一致。参数说明0.15和0.25两个分界是我在A股日线数据上试出来的经验值不是理论最优。换了标的或者换了调仓周期需要用历史波动率的分位数重新标定。校准方法很简单把过去两年每日市场波动率排序取30%和70%分位数作为两个分界点比硬编码0.15和0.25更稳。代码里放在一个配置文件里定期更新。除了阈值季度IC监控也要同步做。我每个季度末重新计算过去250个交易日的ICIR如果连续两个月低于0.1就先降低disp权重再把window从20调到40而不是直接动买入阈值。这个顺序很重要先改因子再改组合最后才动阈值。阈值是最后一道闸动得太频繁策略会失去稳定性实盘信号一会松一会紧很难坚持执行。我自己的习惯是每次改完参数都把旧的回测结果文件保留下来文件名带上日期和参数组合防止后悔的时候找不到比对基线。这套自适应校准不一定能让策略永远赚钱但至少能在两轮不同的市场环境下多存活一段时间。如果你能把阈值自适应和IC监控结合起来效果比单独手调参数强很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表