/~learn/s/c7274c9f
> 复盘日期:2026-08-13 > 触发:本周"新进 Top10 原因解释"里,香港证券等 ETF 的「稳定性」因子分数单周 +36pp,持仓 Top10 一组变一组,观感异常。 > 结论:不是数据 bug,是市场状态(regime)离散硬切换 → 因子权重/仓位/大类偏好阶跃导致的系统性"过渡太直接"问题。
---
截面选基页(ETF 策略持仓)「新进 Top10」子表里,某只 ETF 的解释出现:
📌 主要变化:稳定性走强
🟢 📊稳定性↑+36pp
用户直觉是"稳定性评分怎么瞬间加 36 个百分点,是不是市场风格切换 / bug"。
+36pp 里的 稳定性 对应 DB 字段 factor_vol_stability,它存的是经过 rank 百分位归一化的稳定性因子分数(Factor.normalize(series, method='rank')),与 regime 权重无关。
VolatilityStabilityFactor(factor/mean_reversion_factor.py)用 60 日滚动窗口的 mean_return /vol + sharpe 算出。> 注意:rank 百分位会放大"截面密集区"的小变化。这是稳定性因子分数单周跳几十 pp 的放大器,但本身不是错误。
RegimeDetector.detect() 用硬阈值把市场切成 6 个离散状态(BULL/MILD_BULL/RANGING/HIGH_VOL/BEAR/SEVERE_BEAR),AdaptiveWeightEngine / get_exposure_cap / 大类偏好都按离散状态查表:
| 状态 | momentum | overbought_avoid | vol_stability |
| ------ | ---------- | ------------------ | --------------- |
| MILD_BULL | 0.30 | 0.20 | 0.22 |
| RANGING | 0.20 | 0.27 | 0.25 |
| HIGH_VOL | 0.10 | 0.33 | 0.30 |
只要 price_vs_ma_200 或 vol_ratio 在阈值附近轻轻一穿,标签一翻,5 个因子权重整体跳变,综合评分重排 → Top10 换一组。这就是用户说的"今天温和牛、明天震荡,持仓直接换一批"。
w = Σ pᵢ · wᵢ,用状态概率混合各状态组合,而不是硬选一个状态。→ 对应本方案的"连续 regime_score + 权重插值"。
核心思路:权重、仓位、大类偏好全部连续化,离散标签仅作展示语义并加滞后。
regime_score ∈ [0,1](factor/regime.py)由三个特征连续映射:
trend = clip(price_vs_ma / 0.05, -1, 1) * 0.5 # 趋势轴
vol_penalty = max(0, vol_ratio - 1.0) * 0.5 # 波动惩罚
trend_bonus = clip(trend_strength, 0, 1) * 0.10 # 趋势强度微调
score = clip(0.5 + trend - vol_penalty + trend_bonus, 0, 1)
score 越高越进攻,越低越防守,无阈值跳变。
factor/weight_engine.py)
AdaptiveWeightEngine._continuous_base(score) 用 np.interp 在 5 个锚点(0=SEVERE_BEAR, 0.2=BEAR, 0.5=RANGING, 0.8=MILD_BULL, 1.0=BULL)之间对 5 因子权重线性插值,锚点数值与原离散表完全一致。
校准后权重随 score 平滑过渡:
| score | momentum | overbought_avoid | vol_stability |
| ------- | ---------- | ------------------ | --------------- |
| 0.00 (SEVERE_BEAR) | 0.080 | 0.350 | 0.320 |
| 0.22 (HIGH_VOL) | 0.107 | 0.326 | 0.315 |
| 0.52 (RANGING) | 0.207 | 0.265 | 0.248 |
| 0.72 (MILD_BULL) | 0.273 | 0.219 | 0.228 |
| 0.97 (BULL) | 0.343 | 0.158 | 0.203 |
RegimeDetector.get_exposure_cap)
exposure_cap 不再按离散标签查表,改用 np.interp(score, [0,0.2,0.5,0.8,1], [0.15,0.35,0.6,0.9,1.0]),再叠加原有 vol 惩罚。
selector/multi_factor_selector.py)
债券 / 贵金属 / REITs 的防御加成原来按 is_defensive_regime / is_bull_regime 二值切换(如债券 1.10 ↔ 0.95),现按 regime_score 做 np.interp 平滑过渡(中性=1.0)。
RegimeDetector._apply_hysteresis)
标签切换需 score 越过「当前状态锚点 ↔ 目标状态锚点」的中点 + 0.06 缓冲带,避免边界抖动。标签只用于展示,不再影响权重/仓位。
> 回测前会 reset_hysteresis()(engine._compute_weekly_backtest_drawdown 入口),避免复用实盘状态污染历史回测。
| 文件 | 改动 |
| ------ | ------ |
adata/aohan/quant/selection/factor/regime.py | 新增 regime_score、_risk_score、_apply_hysteresis、连续 exposure_cap |
adata/aohan/quant/selection/factor/weight_engine.py | 新增 _continuous_base(score),compute() 优先用连续插值 |
adata/aohan/quant/selection/selector/multi_factor_selector.py | 债券/贵金属/REITs 大类偏好连续化 |
adata/aohan/quant/selection/engine.py | 回测入口 reset_hysteresis() |
unified_server.py | 修复市场状态 hint 点击外部不隐藏的 UI 问题 |
市场状态标识(.regime-badge)的 hint 弹窗:点击标识能开,但点击其他区域不关闭。修复为全局 document.addEventListener('click'),点击 .regime-badge 之外时移除所有 tip-open。
py_compile 通过,选择模块 import 通过。
regime_score 再做日频 EMA(半衰期 ~3-5 天),进一步压降日间抖动。需要引入跨日状态持久化(避免污染回测),可做成 detect_from_cache(smooth=True) + 回测 smooth=False。used_weights 落库并渲染。