尧图精选

信息离散度与动量双因子:Python量化策略捕捉温水趋势

🕒 发布时间:2026/10/2 4:47:48 📁 来源:尧图网络
简介本资源是一份金融工程实证研究资料包面向具备一定量化投资与金融工程基础的科研人员、基金经理及量化分析师解决动量策略中信息离散度识别与选股优化问题。内容基于“温水煮青蛙”理论对比小幅持续上涨的信息连续型股票与大幅突然上涨的信息离散型股票实证显示前者动量收益可达5.94%后者则为-2.07%并进一步从有限关注度和分析师反应滞后角度解释差异。包内共1个PDF文件压缩包大小约787KBPDF详细给出Python实现流程覆盖数据准备、信息离散度指标计算、动量因子分组、回归分析等方法并附有可运行代码与逐步注释便于读者验证结论并结合实际行情扩展应用。已有115人学习使用适合用于课程设计、基金策略回测或金融工程专题研究。1. 金融工程信息离散度与动量效应不是所有趋势都值得追用过去 60 日涨幅最大的 20 只股票做最朴素的动量组合长期回测往往不是亏损而是波动大到让人拿不住单月回撤动不动超过 12%一次 V 型反转就可能把半年收益全部吐回去。动量效应本身没有被推翻问题出在你追的是“滚水”还是“温水”。金融工程里的信息离散度恰好能把这两种行情分开——低离散度意味着市场对个股信息的解读相对一致、没有剧烈分歧价格沿平滑路径漂移高离散度则意味着噪音与分歧主导任何动量信号都会被反复打断。这套方案把“温水煮青蛙”落到三个可计算的条件上给出信息离散度与动量双因子的选股逻辑、完整的 Python 量化交易策略代码、回测框架与验证方法适合正在 A 股做横截面因子研究的量化工程师和学生参考。2. “温水煮青蛙”如何左右动量效应信息离散度的数学度量与口径选择2.1 信息离散度为什么能当动量的“温控器”横截面动量策略赚的是“信息缓慢扩散”的钱。一条利好如果被所有投资者在同一天消化股价会直接跳涨到位后面没有继续上涨的空间只有当信息分批次、分人群慢慢渗透股价才会沿着多个小台阶连续漂移形成可跟踪的动量。信息离散度衡量的正是“信息被消化得有多不整齐”。个股日收益率里有一部分波动来自大盘系统性的涨跌另一部分来自该公司自身的信息冲击。把市场因素用回归方式剥离后剩下的残差波动越大说明这只股票几乎每天都在吸收独立的意外信息。这种环境下多空双方反复拉锯价格不形成稳定方向过去收益对未来收益几乎没有预测力。残差波动小说明该股票的大部分价格变化来自共性因素自身信息呈渐进渗透态势惯性资金才有机会把方向延续下去。行为金融视角也能解释高信息离散度意味着投资者对同一信息产生分歧成交量放大但价格却不走出趋势这时的动量排序表更多反映的是噪音而非信息。低信息离散度对应的是一致预期的逐步修正类似一锅水温均匀升高的水青蛙在舒适中失去警觉趋势在市场“舒适区”里被一步步坐实。2.2 信息离散度的三种计算口径残差波动率、行业偏离度与预测分歧实际操作中信息离散度有三种常见度量口径我按可复现难度和适用性分开说明。口径数据要求优点缺点残差波动率个股日收益 市场收益序列A 股最容易复现可直接用回测同源数据计算与总波动率相关性偏高需做共线性检查行业偏离度个股日收益 行业指数收益直接对应“行业内信息是否被分散消化”行业分类口径不统一切换分类结果会变分析师预测分歧度盈利预测的一致预期数据终端学术研究的标准口径面向未来信息A 股覆盖不全数据获取成本高小市值基本无覆盖日常研究里我以残差波动率为主因子行业偏离度作稳健性验证。残差波动率的计算并不复杂先把个股收益率对市场收益率做一次线性回归再用残差序列的标准差作为离散度。它不需要分析师预测数据也不依赖行业分类标准只要有日线和指数序列就能算这也是它能进 Python 量化交易策略代码库的原因。2.3 动量窗口的选择为什么过去 60 日但要扣掉最近 5 日动量因子的标准写法是“过去 N 日收益”但 N 的取值直接决定你拿到的是动量还是反转。经典文献里常见的是 12-1 月动量即用过去 12 个月、跳过最近 1 个月的收益。落到日频 A 股回测我一般把窗口压缩到 60 日并跳过最近 5 日理由是 A 股投资者结构里短线资金占比高信息衰减速度比美股快12 个月的窗口太长等你选出来趋势差不多走完了。跳过最近 5 日不是可有可无的细节。如果不跳过榜单很容易被最近几天突然躁动的股票占据这些股票往往是短期反转的候选买入后立刻回落跳过之后动量信号只保留已经经过市场反复确认的漂移相当于把“青蛙刚感觉到烫”的那几天剔除了。窗口大小我会作为超参数放进回测框架后面第 6 章会专门做敏感性验证看收益是集中在某个窗口还是一段区间都稳定。2.4 温水系数把“煮青蛙”写进数学公式“温水煮青蛙”要进入代码必须翻译成可计算的量。我定义了一个叫温水系数的时间序列指标warm |日收益均值| / 日收益标准差分子是日均方向衡量这只股票每天平均向哪个方向挪动分母是单日波动的平均幅度。这个比值高说明上涨是一点一点持续涨出来的像水温均匀上升趋势的主体是大量小幅推进比值低说明涨幅由少数大 K 线贡献大部分时间在震荡像水在翻滚沸腾。注意温水系数与信息离散度是两个维度的东西。信息离散度是截面概念衡量个股对市场回归后剩余波动的平均水平温水系数是时间序列概念衡量个股自身价格路径的平稳程度。两者都可以表征“温水”但信息离散度回答的是“市场分歧大不大”温水系数回答的是“趋势走得稳不稳”策略里两个过滤条件都要用。3. 用 Python 构建信息离散度与动量双因子数据准备与核心函数3.1 数据准备复权口径、交易日历与历史成分股这套策略的因子都基于前复权收盘价面板。面板结构必须是行索引为股票代码、列索引为交易日期的宽表这样才能在任意调仓日做横截面切片。下面的代码先解决交易日历与月度调仓日import numpy as np import pandas as pd import tushare as ts from scipy import stats pro ts.pro_api(你的token) # 拉上交所交易日历只保留开市日 cal pro.trade_cal(exchangeSSE, start_date20150101, end_date20241231) cal cal[cal[is_open] 1][cal_date].astype(str) trade_dates pd.to_datetime(cal) # 每个月最后一个交易日作为调仓日 rebalance_dates trade_dates.resample(ME).max().dropna().tolist() print(rebalance_dates[:5])代码逻辑很直接先过滤出开市日再按自然月做降采样取最后一个交易日。需要注意resample(ME)里的ME是 pandas 2.2 之后的写法老版本用M运行报错时优先检查这一行。调仓日确定后下一步是拉股票池的历史成分股我以中证 500 为例def load_members(date): 取中证500在某一天的成分股列表。 以 tushare 的 index_weight 接口示例换成其他数据源时 保证返回的股票代码与 price_panel 的行索引一致即可。 df pro.index_weight(index_code000905.SH, start_datedate.strftime(%Y%m%d), end_datedate.strftime(%Y%m%d)) return df[con_code].dropna().tolist()这里必须强调一个原则任何时候做历史回测都要用“当天”的成分股而不能用今天的中证 500 名单去回测 2015 年。否则你等于提前知道了哪些公司活到了今天回测会虚高这条在第 5 章会详细展开。3.2 因子计算函数动量、离散度、温水系数的 Python 实现核心因子函数一次算齐三列数据动量、信息离散度、温水系数。输入是价格面板pv和某一个调仓日rb_date输出是一个 DataFrame每一行是一只股票。def calc_factors(pv, rb_date, lookback60, skip5): pv: index股票代码, columns日期, values前复权收盘价 rb_date: 本次调仓日必须是 pv 的列之一 pos pv.columns.get_loc(rb_date) # 调仓日在面板中的位置 hist pv.iloc[:, pos - lookback:pos - skip] # 历史窗口跳过最近5日 rets hist.pct_change(axis1) # 每日收益率 out pd.DataFrame(indexhist.index) # 动量窗口首尾比价减 1等价于期间累计收益率 out[mom] hist.iloc[:, -1] / hist.iloc[:, 0] - 1 # 温水系数日均收益方向 / 单日波动 out[warm] rets.mean(axis1).abs() / rets.std(axis1) # 信息离散度个股收益对市场收益回归后的残差标准差 mkt_ret rets.mean(axis0) # 每日等权市场收益 def _resid_std(y): x mkt_ret.values slope, intercept, _, _, _ stats.linregress(x, y) return (y - (slope * x intercept)).std(ddof1) out[ivol] rets.apply(_resid_std, axis1) return out.dropna()拆开解释几个关键点。动量用的是首尾比价而不是日收益累乘因为在统一前复权口径下两者数学上等价但首尾比价更直观也更少受 NaN 的干扰。信息离散度先做市场回归再取残差标准差这一步剥离的是系统性风险剩下的才是“个股自身信息导致的离散”——如果不剥离你抓到的就只是波动率因子不是信息离散度因子。dropna()会把上市不足 60 日、窗口内停牌导致收益全为空的股票自动剔除避免因子值填 0 混进选股池。参数上lookback60是动量窗口也兼任回归窗口skip5是跳过的最近交易日。这两个参数在第 6 章网格测试里会被反复调用所以函数签名里直接暴露出来方便后续批量换参。3.3 选股逻辑低离散度分层后再选动量因子算完之后进入选股环节。策略的过滤条件按顺序执行动量必须为正、温水系数要高于截面中位数、信息离散度要落在最低的三分之一组里最后在剩余池子里取动量最大的 20 只。def select_stocks(factors, n20, warm_q0.5, disp_q0.33): f factors.copy() # 条件1动量方向为正排除整体下行但靠个别反弹日凑数的股票 f f[f[mom] 0] # 条件2温水系数高于截面分位阈值趋势必须走得稳 f f[f[warm] f[warm].quantile(warm_q)] # 条件3信息离散度低于截面分位阈值市场分歧不能太大 f f[f[ivol] f[ivol].quantile(disp_q)] return f[mom].nlargest(n).index.tolist()这个顺序是有讲究的先过滤方向再过滤质量最后才排序选量。如果把动量排序放在过滤之前你选出来的是“涨幅大”而不是“温水区里涨幅大”逻辑就变了。warm_q0.5表示取温水系数截面中位数以上的股票disp_q0.33表示只看信息离散度最低的三分之一。还有一个必须处理的边界情况当中证 500 成分里满足条件的股票不足 20 只时nlargest(20)会返回全部剩余股票组合不满仓。研究阶段可以先接受这个行为实盘阶段则要加兜底逻辑比如放宽离散度阈值或降低持仓数量否则回测与实盘的持仓结构会对不上。3.4 超参数默认值与调参方向参数默认值含义调大后果调小后果lookback60动量计算窗口信号更平滑但 A 股信息衰减收益钝化更容易落入短期反转区skip5跳过的最近交易日更保守过滤掉最新的躁动保留短期反弹噪音增大warm_q0.5温水系数截面分位阈值只保留最平滑的少数趋势股池子变大质量下降disp_q0.33信息离散度截面分位阈值保留更多股票离散度约束变弱池子变小风险更集中n20持仓数量分散更充分单票冲击小收益集中波动放大这里把调参方向写清楚是因为新手最容易犯的错是只看收益曲线把所有参数调到回测最优然后拿到实盘市场上被真实冲击成本教育。默认值不是最优值而是一个“大概率不翻车”的起点真正的参数选择要建立在第 6 章的敏感性网格之上。4. 完整的量化投资策略回测从选股到净值曲线的工程实现4.1 回测主循环代码换仓、持有与净值累积回测逻辑采用月度调仓期初选股、期末结算换仓时按等权重新分配。下面是主循环def run_backtest(pv, rebalance_dates, n20, warm_q0.5, disp_q0.33, lookback60, skip5, cost0.0): nav [1.0] turnovers [] prev_picks None for i, rb in enumerate(rebalance_dates): fac calc_factors(pv, rb, lookback, skip) picks select_stocks(fac, n, warm_q, disp_q) if i 0: start rebalance_dates[i - 1] # 上期持仓在上一个调仓日到本调仓日之间的等权收益 gross_ret (pv.loc[prev_picks, rb].mean() / pv.loc[prev_picks, start].mean() - 1) if prev_picks: turnover len(set(prev_picks) - set(picks)) / n turnovers.append(turnover) else: turnover 0.0 net_ret gross_ret - cost * turnover nav.append(nav[-1] * (1 net_ret)) prev_picks picks nav pd.Series(nav, indexrebalance_dates) return nav, np.mean(turnovers) if turnovers else 0.0cost参数默认是 0先跑一个不含交易成本的版本目的是验证因子本身的预测力跑通后再把成本加上看实盘落差。turnover计算的是卖出旧持仓的比例买入新持仓时也会有成本但两者方向一致统一用换手率乘以双边成本估算就够了。这里用的pv.loc[prev_picks, rb].mean()有一个隐性约束面板里的价格需要做前向填充。如果某只股票在调仓区间内停牌tushare 的复权数据通常会保留停牌前收盘价但个别数据源会给出空值mean()会自动跳过 NaN这会导致组合收益被高估。稳妥做法是在数据准备阶段统一pv.ffill(axis1)把所有缺失价格填成停牌前最后收盘价。4.2 绩效指标与换手率统计净值序列出来之后用一组标准指标判断这个策略值不值得继续投入def perf_stats(nav, turnover, rf0.02): ret nav.pct_change().dropna() years (nav.index[-1] - nav.index[0]).days / 365 ann_ret (nav.iloc[-1] / nav.iloc[0]) ** (1 / years) - 1 ann_vol ret.std() * np.sqrt(12) # 月度调仓年化按12个月 sharpe (ann_ret - rf) / ann_vol max_dd (nav / nav.cummax() - 1).min() return pd.Series({ 年化收益: ann_ret, 年化波动: ann_vol, 夏普: sharpe, 最大回撤: max_dd, 平均换手率: turnover })这里的np.sqrt(12)是因为净值点是月度频率波动率按月计算后转年化需要乘根号 12。如果改成日频调仓记得换成根号 252否则年化波动率会被系统性低估。最大回撤用的是月度净值精度真实的日内回撤通常比这个数字更大这一点心里要有数。4.3 与全动量、基准指数的对照方式单一策略的绩效指标没有意义必须放在同一股票池、同一回测区间里做横向对比。至少跑三个对照不加任何过滤、直接取动量前 20 的全动量组合只用“信息离散度分层后再选动量”、不加温水系数过滤的版本以及叠加温水系数过滤后的完整策略。基准用中证 500 指数自身。策略年化收益最大回撤夏普平均换手率全动量 Top20待回测输出待回测输出待回测输出待回测输出低离散度 动量待回测输出待回测输出待回测输出待回测输出完整策略温水过滤待回测输出待回测输出待回测输出待回测输出中证 500 指数待回测输出待回测输出待回测输出0读结果时重点看两处一是完整策略是否在收益不变的前提下把最大回撤压下来了二是平均换手率是不是比全动量版本低。如果信息离散度和温水系数真的抓到了“渐进式趋势”换手率应该显著下降因为过滤掉的高波动股票本来就留不住。5. 避坑与常见问题排查从因子翻车到实盘落差5.1 未复权价格造成的动量虚高现象回测收益极高翻看持仓全是“高送转”或大额分红股票买入后价格异常上涨。原因动量因子直接用了不复权价格。除权除息当天价格会向下跳空在动量窗口里被当成一次真实下跌而复权因子修正后的真实收益在长时间窗口里扭曲了价格序列。前复权价格把历史价格整体向下调整除权日的跳空被抹平动量排序才不会选出“假涨”股票。解决所有价格统一用前复权口径。tushare 的pro_bar接口里adjqfq就是前复权。这个坑看起来基础但换数据源时最容易漏尤其是从某些免费接口拿到不复权数据就直接进因子计算回测和实盘的差异会大到无法解释。5.2 信息离散度与总波动率的共线性陷阱现象把信息离散度因子换成普通波动率因子分组收益几乎一致怀疑策略是“换皮”。原因残差波动率和总波动率的相关性经常在 0.9 左右因为市场收益这一项在总波动里占比有限个股自身波动才是主体。用残差做回归剥离市场因素后残差波动与总波动仍然高度重合两者抓到的是同一批股票。解决做任何因子报告之前先看信息离散度在“总波动率分层内”还有没有区分度。把股票先按总波动率分成 5 组再在每组内部按信息离散度分成 2 组比较组内收益差。如果总波动率分层后信息离散度已经没有增量信息那这个因子就没有独立价值标题里的“信息离散度”就只是一个叙事包装。这个验证代码只有十几行但能避免把策略建立在虚假的因子故事上。5.3 月度调仓的交易成本黑洞现象回测年化收益 25%实盘或者扣费后只剩一半最大元凶是换手率。原因我看到很多新手回测时把cost设为 0或者只按单边万三估佣金。实际上月度调仓的换手率如果超过 60%双边佣金加卖出印花税加冲击成本一次调仓就能吃掉 1% 以上净值一年 12 次成本复利滚动下来非常可观。解决在run_backtest里把cost至少设为 0.002 到 0.003 再跑一遍。A 股卖出印花税按成交金额征收简化估算时双边佣金按万三、卖出印花税按万五、冲击成本按买卖各千一合计单次换仓双边在 0.25% 到 0.35% 之间。调仓日换手率越高成本越敏感这也是第 4 章要求统计换手率的原因。5.4 动量崩溃市场 V 型反转时温水系数也会失灵现象策略连续几个月收益稳定某次指数突然急跌后快速拉回组合在一个调仓周期内回撤吃掉前三个月利润。原因动量信号本质假设“信息渐进扩散”但当市场出现剧烈反转所有前期趋势方向都会反向。温水系数用的是 60 日窗口就算最后几天剧烈翻转窗口内的均值与标准差变化也不足以立刻翻转信号等调仓日到来时已经来不及回避。解决加一道市场状态过滤。用指数或全市场等权收益的最近 5 日涨跌幅计算其滚动历史分位如果绝对值超过 95% 分位视为极端行情当期空仓或减半仓。这个分位要用调仓日当天之前的滚动历史计算不能用全体区间内的未来信息否则等于偷看答案。5.5 幸存者偏差用今天的名单回测历史等于拿未来做决策现象回测净值漂亮但换一种股票池口径结果立刻恶化。原因如果直接用今天的中证 500 成分股回测 2015 到 2024 年样本里天然剔除了过去十年里退市、被 ST、被调出指数的公司等于提前知道了谁活到最后。动量策略恰恰容易选中高风险高波动的股票这些股票在历史样本里被清洗掉后回测动量收益会系统性虚高。解决每个调仓日必须用当天的指数成分列表。index_weight接口可以按日期拉历史成分如果数据源不支持至少要退而求其次用每月成分快照做近似。我自己的习惯是任何因子研究的第一版代码里先写成分历史字典再写因子计算顺序不能反。6. 验证策略是否值得投入分组单调性检验与参数敏感性6.1 分组单调性检验怀疑策略时先跑这段代码一个因子策略是不是真的有结构效应最直接的验证是把股票按信息离散度分成 5 组在每组内部独立取动量 Top10看下期等权收益是否从低离散度组到高离散度组单调递减。如果低离散度组收益最高、高离散度组接近亏损说明“温水”逻辑成立如果收益在中间组达到峰值甚至完全无规律说明这个因子在样本里没有稳定驱动动量。def dispersion_group_test(pv, rebalance_dates, sep5, top_n10): rows [] for i in range(len(rebalance_dates) - 1): fac calc_factors(pv, rebalance_dates[i]) fac[grp] pd.qcut(fac[ivol], sep, labelsFalse) for g in range(sep): sub fac[fac[grp] g] picks sub[mom].nlargest(top_n).index fwd (pv.loc[picks, rebalance_dates[i 1]].mean() / pv.loc[picks, rebalance_dates[i]].mean() - 1) rows.append((rebalance_dates[i], g, fwd)) tbl pd.DataFrame(rows, columns[date, grp, fwd_ret]) return tbl.groupby(grp)[fwd_ret].mean()输出会是一个长度为 5 的序列。理想的形态是第 0 组最低离散度收益最高第 4 组最低甚至为负。如果不单调后面的参数优化做得再漂亮都是在过拟合噪音。6.2 参数敏感性网格过拟合还是结构效应参数网格测试的思路是把动量的 lookback 从 40 到 80 分三档信息离散度的 disp_q 从 0.25 到 0.5 分三档跑出 9 个夏普值看收益是集中在一两个“神秘参数点”还是在一整片区域都保持稳定。grid_results {} for lb in [40, 60, 80]: for dq in [0.25, 0.33, 0.5]: nav, to run_backtest(pv, rebalance_dates, lookbacklb, disp_qdq) stats perf_stats(nav, to) grid_results[(lb, dq)] stats[夏普] grid pd.Series(grid_results).unstack() print(grid.round(3))如果整个表格里只有某一个格子特别亮其他格子暗淡这通常意味着你找到了一个对历史数据过拟合的参数组合如果相邻格子数值接近、整体呈渐变趋势才说明因子效应是结构性的参数微调不会翻车。黑匣子式的“某个窗口最优”结论我建议一律用这张表来冷却一下。6.3 增量 IC离散度条件到底有没有贡献最后一步是确认信息离散度过滤的增量信息。在每个调仓日分别计算全股票池和低离散度组内部的动量与下期收益的 Spearman 秩相关也就是 Rank IC。如果低离散度组内的 Rank IC 均值显著高于全池说明离散度条件真的把动量预测力更强的子样本选了出来如果两者差不多说明离散度只是换了一种方式选低波动股票策略的核心贡献仍然来自波动率。这一步没有单独写函数直接基于calc_factors的输出在循环里调corr(methodspearman)即可十几行代码但它是决定这个因子能不能进入实盘候选池的最后一道闸门。我自己第一次跑这套框架时四个条件全开回测绩效漂亮到不敢信后来一个一个拆条件才发现有一半收益来自幸存者偏差和未扣除的交易成本。现在我的习惯是无论新因子的故事讲得多动听先跑一遍分组单调性检验再决定要不要给它实盘预算参数网格里如果只有一个亮点就直接弃用。因子研究的翻车大多不是策略太复杂而是验证环节偷了懒。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →