mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
Merge pull request #160 from shy3130/feat/regime-trend-explainable
feat(regime): 趋势图可解释化(综合分主线+子维度曲线+状态背景色带)
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -437,6 +437,15 @@ export interface RegimeRow {
|
||||
above_ma20_pct: number
|
||||
total_amount: number
|
||||
avg_turnover: number
|
||||
// 4 个子维度分(0-100, 重算后才有; 旧数据可能缺) — 综合分的加权来源
|
||||
avg_pct?: number
|
||||
median_pct?: number
|
||||
strong_up_pct?: number
|
||||
strong_down_pct?: number
|
||||
profit_score?: number
|
||||
speculation_score?: number
|
||||
resilience_score?: number
|
||||
trend_score?: number
|
||||
}
|
||||
|
||||
export interface RegimeHistory {
|
||||
|
||||
@@ -134,19 +134,49 @@ export function Regime() {
|
||||
const rows: RegimeRow[] = history.data?.rows ?? []
|
||||
const latest = rows.length > 0 ? rows[rows.length - 1] : null
|
||||
|
||||
// 趋势图: 综合分曲线 + 涨停数柱状
|
||||
// 趋势图: 综合分主线 + 4 子维度曲线(可切换) + 状态背景色带 + 涨停数柱状
|
||||
const trendOption = useMemo<echarts.EChartsOption | null>(() => {
|
||||
if (rows.length === 0) return null
|
||||
const dates = rows.map(r => r.date)
|
||||
const scores = rows.map(r => r.score)
|
||||
const limitUps = rows.map(r => r.limit_up)
|
||||
const profit = rows.map(r => r.profit_score ?? null)
|
||||
const speculation = rows.map(r => r.speculation_score ?? null)
|
||||
const resilience = rows.map(r => r.resilience_score ?? null)
|
||||
const trend = rows.map(r => r.trend_score ?? null)
|
||||
|
||||
// 状态背景色带: 合并连续同状态日期段, 每段用状态色低透明度着色
|
||||
const stateBands: any[] = []
|
||||
let bandStart = rows[0]?.date
|
||||
let prevState = rows[0]?.state
|
||||
rows.forEach((r, i) => {
|
||||
if (r.state !== prevState || i === rows.length - 1) {
|
||||
const bandEnd = i === rows.length - 1 ? r.date : rows[i - 1].date
|
||||
if (prevState && REGIME_STATE_COLORS[prevState as RegimeState]) {
|
||||
stateBands.push([
|
||||
{ xAxis: bandStart, itemStyle: { color: REGIME_STATE_COLORS[prevState as RegimeState], opacity: 0.08 } },
|
||||
{ xAxis: bandEnd },
|
||||
])
|
||||
}
|
||||
bandStart = r.date
|
||||
prevState = r.state
|
||||
}
|
||||
})
|
||||
|
||||
const subLineStyle = { width: 1.2, type: 'dotted' as const, opacity: 0.8 }
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: { trigger: 'axis', backgroundColor: ct.tooltipBg, borderColor: ct.tooltipBorder, textStyle: { color: ct.tooltipText } },
|
||||
legend: { data: ['综合分', '涨停数'], textStyle: { color: ct.text }, top: 0 },
|
||||
grid: { left: 48, right: 48, top: 32, bottom: 56 },
|
||||
legend: {
|
||||
data: ['综合分', '涨停数', '赚钱', '投机', '抗跌', '趋势'],
|
||||
textStyle: { color: ct.text, fontSize: 10 }, top: 0,
|
||||
// 默认只显示综合分 + 涨停数(简洁); 4 个子维度默认隐藏, 点图例展开看驱动因素
|
||||
selected: { '综合分': true, '涨停数': true, '赚钱': false, '投机': false, '抗跌': false, '趋势': false },
|
||||
},
|
||||
grid: { left: 48, right: 48, top: 36, bottom: 56 },
|
||||
xAxis: {
|
||||
type: 'category', data: dates,
|
||||
type: 'category', data: dates, boundaryGap: false,
|
||||
axisLabel: { color: ct.text, fontSize: 10, formatter: (v: string) => v.slice(5) },
|
||||
axisLine: { lineStyle: { color: ct.grid } },
|
||||
},
|
||||
@@ -159,13 +189,27 @@ export function Regime() {
|
||||
{ type: 'slider', bottom: 8, height: 16, borderColor: ct.border, fillerColor: ct.zoomFill, textStyle: { color: ct.text } },
|
||||
],
|
||||
series: [
|
||||
// 涨停数柱状(半透明背景)
|
||||
{ name: '涨停数', type: 'bar', data: limitUps, yAxisIndex: 1, barMaxWidth: 6,
|
||||
itemStyle: { color: REGIME_STATE_COLORS.strong, opacity: 0.35 }, z: 1 },
|
||||
// 4 子维度曲线: 帮助理解综合分由什么驱动(点图例可切换)
|
||||
{ name: '赚钱', type: 'line', data: profit, smooth: true, symbol: 'none',
|
||||
lineStyle: { ...subLineStyle, color: '#f59e0b' }, z: 2 },
|
||||
{ name: '投机', type: 'line', data: speculation, smooth: true, symbol: 'none',
|
||||
lineStyle: { ...subLineStyle, color: '#a855f7' }, z: 2 },
|
||||
{ name: '抗跌', type: 'line', data: resilience, smooth: true, symbol: 'none',
|
||||
lineStyle: { ...subLineStyle, color: '#10b981' }, z: 2 },
|
||||
{ name: '趋势', type: 'line', data: trend, smooth: true, symbol: 'none',
|
||||
lineStyle: { ...subLineStyle, color: '#3b82f6' }, z: 2 },
|
||||
// 综合分主线(加粗置顶) + 状态背景色带 + 阈值横虚线
|
||||
{ name: '综合分', type: 'line', data: scores, smooth: true, symbol: 'none',
|
||||
lineStyle: { width: 2, color: ct.textStrong }, areaStyle: { opacity: 0.08 },
|
||||
lineStyle: { width: 2.5, color: ct.textStrong }, areaStyle: { opacity: 0.06 }, z: 3,
|
||||
markArea: { silent: true, data: stateBands },
|
||||
markLine: { silent: true, lineStyle: { type: 'dashed', color: ct.grid }, data: [
|
||||
{ yAxis: 75, label: { formatter: '强势', color: ct.text, fontSize: 9 } },
|
||||
{ yAxis: 40, label: { formatter: '震荡', color: ct.text, fontSize: 9 } },
|
||||
{ yAxis: 70, label: { formatter: '强势', color: ct.text, fontSize: 9 } },
|
||||
{ yAxis: 45, label: { formatter: '震荡', color: ct.text, fontSize: 9 } },
|
||||
{ yAxis: 30, label: { formatter: '偏弱', color: ct.text, fontSize: 9 } },
|
||||
] } },
|
||||
{ name: '涨停数', type: 'bar', data: limitUps, yAxisIndex: 1, barMaxWidth: 6, itemStyle: { color: REGIME_STATE_COLORS.strong } },
|
||||
],
|
||||
}
|
||||
}, [rows, days, ct])
|
||||
@@ -373,7 +417,8 @@ export function Regime() {
|
||||
{/* ── 趋势图 + 分布图 ── */}
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<div className={cn(cardCls, 'p-3 lg:col-span-2')}>
|
||||
<SectionTitle icon={Activity} title="环境综合分 · 涨停数趋势" />
|
||||
<SectionTitle icon={Activity} title="环境综合分趋势"
|
||||
hint="综合分(粗) · 赚钱/投机/抗跌/趋势(细, 可点图例切换) · 背景色=状态" />
|
||||
<div ref={trendRef} className="mt-2 h-[320px]" />
|
||||
</div>
|
||||
<div className={cn(cardCls, 'p-3')}>
|
||||
|
||||
Reference in New Issue
Block a user