feat(regime): 趋势图可解释化 — 综合分主线 + 子维度曲线 + 状态背景色带

问题: 原趋势图标题「环境综合分·涨停数趋势」, 但综合分是4维加权结果,
图里只画涨停数一个副指标, 用户无法理解综合分为何涨跌(黑盒)。

后端(regime_builder.py)
- 抽出 _compute_subscores(metrics): 计算4子维度分(赚钱/投机/抗跌/趋势)+综合分
- classify_state 复用 _compute_subscores(逻辑不重复)
- _aggregate_daily 持久化4个子分列: profit_score/speculation_score/
  resilience_score/trend_score(0-100), 供趋势图展示+未来策略按子维度过滤

前端
- RegimeRow 类型加8个可选字段(4子分+4原始指标, 兼容旧数据)
- 趋势图重构:
  · 综合分主线(加粗2.5px置顶) + 半透明面积
  · 4子维度点状曲线(赚钱橙/投机紫/抗跌绿/趋势蓝), 默认隐藏, 点图例展开
  · 状态背景色带(markArea, 连续同状态日期段用状态色低透明度着色)
  · 涨停数柱状降为半透明背景(opacity 0.35)
  · 阈值横虚线更新为新模型 70/45/30
  · 默认 legend.selected 只显示综合分+涨停数(简洁), 子维度按需展开
- 标题改为「环境综合分趋势」+ hint 说明各曲线含义

验证
- 后端 583 passed; 子分加权一致性100%(综合分=Σ子分×权重)
- 实测解释性: weak日(7-30综合23) 赚钱9+抗跌0 清楚显示普跌+大跌股多
- tsc + pnpm build 通过
This commit is contained in:
shy3130
2026-08-02 17:29:09 +08:00
parent fa5583c2cb
commit 6a750de722
3 changed files with 97 additions and 17 deletions
+34 -8
View File
@@ -60,14 +60,11 @@ def _score(value: float, low: float, high: float) -> float:
return float(max(0, min(100, round((value - low) / (high - low) * 100))))
def classify_state(metrics: dict) -> tuple[str, int]:
"""规则引擎: 4 维指标 → 离散状态 + 综合分(0-100)
def _compute_subscores(metrics: dict) -> dict:
"""计算 4 个子维度分 + 综合分(未取整)。供 classify_state 和持久化复用
对齐看板情绪分的轻量维度(去掉量能/主线以控制内存):
- 赚钱 profit: 涨家数占比 + 均涨幅 + 中位涨幅 + 强弱差
- 投机 speculation: 涨停数 + 封板率 + 连板高度
- 抗跌 resilience: 跌家数占比 + 大跌股占比(大跌日此项暴跌 → 总分进 weak)
- 趋势 trend: 指数涨幅 + MA20 上方占比
返回 {profit, speculation, resilience, trend, score(float, 0-100)}。
子维度分也是 0-100, 供趋势图展示"综合分由什么驱动"
metrics 期望字段(由 _aggregate_daily 聚合):
up_pct, down_pct, avg_pct, median_pct, strong_up_pct, strong_down_pct,
@@ -109,7 +106,29 @@ def classify_state(metrics: dict) -> tuple[str, int]:
+ resilience * WEIGHTS["resilience"]
+ trend * WEIGHTS["trend"]
)
score = max(0, min(100, round(score)))
return {
"profit": profit, "speculation": speculation,
"resilience": resilience, "trend": trend,
"score": max(0, min(100, score)),
}
def classify_state(metrics: dict) -> tuple[str, int]:
"""规则引擎: 4 维指标 → 离散状态 + 综合分(0-100)。
对齐看板情绪分的轻量维度(去掉量能/主线以控制内存):
- 赚钱 profit: 涨家数占比 + 均涨幅 + 中位涨幅 + 强弱差
- 投机 speculation: 涨停数 + 封板率 + 连板高度
- 抗跌 resilience: 跌家数占比 + 大跌股占比(大跌日此项暴跌 → 总分进 weak)
- 趋势 trend: 指数涨幅 + MA20 上方占比
metrics 期望字段(由 _aggregate_daily 聚合):
up_pct, down_pct, avg_pct, median_pct, strong_up_pct, strong_down_pct,
strong_diff_pct, limit_up, seal_rate(0-1), max_consecutive,
index_pct(小数), above_ma20_pct(0-1)
"""
sub = _compute_subscores(metrics)
score = max(0, min(100, round(sub["score"])))
if score >= STATE_STRONG:
state = "strong"
@@ -242,6 +261,8 @@ def _aggregate_daily(df: pl.DataFrame, index_pct_map: dict | None = None) -> pl.
"strong_diff_pct": strong_up_pct - strong_down_pct,
}
state, score = classify_state(metrics)
# 4 个子维度分(供趋势图展示"综合分由什么驱动" + 未来策略按子维度过滤)
sub = _compute_subscores(metrics)
rows.append({
"date": r["date"],
"state": state,
@@ -263,6 +284,11 @@ def _aggregate_daily(df: pl.DataFrame, index_pct_map: dict | None = None) -> pl.
"median_pct": round(median_pct, 4),
"strong_up_pct": round(strong_up_pct, 4),
"strong_down_pct": round(strong_down_pct, 4),
# 4 个子维度分(0-100, 综合分加权来源): 赚钱/投机/抗跌/趋势
"profit_score": round(sub["profit"]),
"speculation_score": round(sub["speculation"]),
"resilience_score": round(sub["resilience"]),
"trend_score": round(sub["trend"]),
})
return pl.DataFrame(rows) if rows else pl.DataFrame()