feat: 温区自适应前端横幅+router v5+overview API——概览页置顶显示温区/温度/激活策略, server.py /api/overview 返回 regime_weights, strategy_router v5 动态策略库
This commit is contained in:
@@ -1,20 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""strategy_router.py v2 — MoFin 策略动态路由(三态 + 温度维度,2026-08-13 重写)
|
"""strategy_router.py v5 — MoFin 策略动态路由(动态策略库,2026-08-13)
|
||||||
|
|
||||||
结论(数据验证):三态骨架 + 温度(rsi)连续维度。
|
核心(老莫原则):
|
||||||
- 三态(trend_up/choppy/trend_down)→ 选策略类型(market_regime.py 判定)
|
- 策略全温区发信号(不留温区门控在策略内)
|
||||||
- 温度(rsi 档位)→ 仓位乘数(temp_band.py 判定,连续量)
|
- 策略-温区表现常态化记录(regime_perf.py / strategy_regime_perf),适用温区动态化
|
||||||
|
- 组合按当前温区自动激活/休眠策略:当前温区实测胜率高的策略优先
|
||||||
|
- 不漏历史策略(v_mr_sel trend_down 93%、v7.3 trend_up 94% 等被埋没的强者)
|
||||||
|
|
||||||
策略-适用温度映射(数据来源:2026-08-13 分市场状态检验,见 docs/decisions/2026-08-13-事项五):
|
数据流:
|
||||||
v_weak → choppy × [fear, neutral] (震荡市+恐慌最佳 71%)
|
market_regime(三态) + regime_tracker(平滑K=5) → 当前温区
|
||||||
v_oversold → trend_down × [panic, fear] (下跌市+恐慌最佳 64-86%)
|
temp_band(rsi) → 温度/仓位乘数
|
||||||
v_next4 → trend_up × [greed, euphoria](趋势市 63%)
|
regime_perf(实测) → 各策略温区表现
|
||||||
v8.1 → trend_up (趋势市 57%)
|
→ strategy_weights.json(各策略权重/激活状态)
|
||||||
s2_panic → trend_down × [panic] (极端恐慌 86%)
|
|
||||||
v_lurk_v3 → [choppy, trend_down] × [fear, neutral](77%/71%)
|
|
||||||
|
|
||||||
输出:strategy_weights.json(各策略 weight 由 状态×温度 决定)
|
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
@@ -27,121 +25,124 @@ sys.path.insert(0, "/home/hmo/MoFin")
|
|||||||
|
|
||||||
OUT = Path("/home/hmo/MoFin/data/strategy_weights.json")
|
OUT = Path("/home/hmo/MoFin/data/strategy_weights.json")
|
||||||
|
|
||||||
|
DEFAULT_FAMILY = "mr"
|
||||||
|
FAMILY_MAP = {
|
||||||
|
"v_weak": "mr", "v_oversold": "mr", "v_mr": "mr", "v_mr2": "mr", "v_mr3": "mr",
|
||||||
|
"v_mr4": "mr", "v_mr_sel": "mr", "v_lurk_v1": "mr", "v_lurk_v2": "mr", "v_lurk_v3": "mr",
|
||||||
|
"s2_panic": "mr", "v_osc": "mr",
|
||||||
|
"v_next": "trend", "v_next3": "trend", "v_next4": "trend", "v_next5": "trend",
|
||||||
|
"v8.0": "trend", "v8.1": "trend", "v8.2": "trend", "v8.3": "trend",
|
||||||
|
"v7.0": "trend", "v7.1": "trend", "v7.1b": "trend", "v7.2": "trend", "v7.3": "trend",
|
||||||
|
"v6.0": "trend", "v6.1": "trend", "v6.2": "trend",
|
||||||
|
"v9.0": "trend", "v9.1": "trend", "v9.2": "trend",
|
||||||
|
"v_combo": "trend", "v1.0": "trend", "v2.0": "trend", "v3.0": "trend",
|
||||||
|
"v4.0": "trend", "v4.0a": "trend", "v4.0b": "trend", "v4.0c": "trend",
|
||||||
|
"v4.0d": "trend", "v4.0e": "trend", "v4.0f": "trend", "v4.0g": "trend",
|
||||||
|
"v4.0h": "trend", "v4.1": "trend", "v5.0": "trend",
|
||||||
|
"v11.0": "trend", "v11.1": "trend",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def load_regime():
|
def load_regime():
|
||||||
"""读取平滑温区(regime_tracker 输出,K=5 滞回确认)。
|
|
||||||
优先用平滑结果(避免单日噪音误判);regime_tracker 不可用时回退原始 market_regime。"""
|
|
||||||
try:
|
try:
|
||||||
p = Path("/home/hmo/MoFin/data/market_regime_smoothed.json")
|
p = Path("/home/hmo/MoFin/data/market_regime_smoothed.json")
|
||||||
if p.exists():
|
if p.exists():
|
||||||
d = json.loads(p.read_text(encoding="utf-8"))
|
d = json.loads(p.read_text(encoding="utf-8"))
|
||||||
return {"regime": d.get("current_regime", "unknown"),
|
return {"regime": d.get("current_regime", "unknown"),
|
||||||
"date": d.get("current_date", ""),
|
"date": d.get("current_date", "")}
|
||||||
"temp_band": (d.get("temp") or {}).get("band"),
|
|
||||||
"temp_rsi": (d.get("temp") or {}).get("rsi")}
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
from market_regime import load_market_regime
|
from market_regime import load_market_regime
|
||||||
return load_market_regime()
|
return load_market_regime()
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return {"regime": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
def get_temp():
|
def load_temp():
|
||||||
"""读取市场温度(rsi 档位)。regime_tracker 已含温度则直接用,否则实时算。"""
|
|
||||||
try:
|
try:
|
||||||
from temp_band import get_market_temp
|
from temp_band import get_market_temp
|
||||||
return get_market_temp()
|
return get_market_temp()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f" [router] 温度获取失败: {e}", file=sys.stderr)
|
|
||||||
return {"band": "unknown", "rsi": None}
|
return {"band": "unknown", "rsi": None}
|
||||||
|
|
||||||
|
|
||||||
# 策略-适用温度映射(基础数据,数据驱动固化)
|
def load_regime_perf():
|
||||||
# weight = 状态匹配度 × 温度乘数
|
conn = None
|
||||||
STRATEGY_TEMP = {
|
try:
|
||||||
"v_weak": {
|
import sqlite3
|
||||||
"family": "mr",
|
conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30)
|
||||||
"regimes": ["choppy"],
|
conn.execute("PRAGMA busy_timeout=30000")
|
||||||
"desc": "震荡市超跌反弹(choppy+恐慌71%最佳)",
|
rows = conn.execute(
|
||||||
},
|
"SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf"
|
||||||
"v_oversold": {
|
).fetchall()
|
||||||
"family": "mr",
|
result = {}
|
||||||
"regimes": ["trend_down"],
|
for strat, reg, n, wr, pnl in rows:
|
||||||
"desc": "下跌市超跌(trend_down+恐慌64-86%)",
|
result.setdefault(strat, {})[reg] = {"trades": n, "win_rate": wr, "avg_pnl": pnl}
|
||||||
},
|
return result
|
||||||
"v_next4": {
|
except Exception:
|
||||||
"family": "trend",
|
return {}
|
||||||
"regimes": ["trend_up"],
|
finally:
|
||||||
"desc": "趋势市追涨(trend_up 63%)",
|
if conn:
|
||||||
},
|
conn.close()
|
||||||
"v8.1": {
|
|
||||||
"family": "trend",
|
|
||||||
"regimes": ["trend_up"],
|
|
||||||
"desc": "波段先出再进(trend_up 57%)",
|
|
||||||
},
|
|
||||||
"s2_panic": {
|
|
||||||
"family": "mr",
|
|
||||||
"regimes": ["trend_down"],
|
|
||||||
"desc": "极端恐慌买超跌(panic 86%)",
|
|
||||||
},
|
|
||||||
"v_lurk_v3": {
|
|
||||||
"family": "mr",
|
|
||||||
"regimes": ["choppy", "trend_down"],
|
|
||||||
"desc": "潜伏型(choppy/trend_down 71-77%)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def route(regime, temp):
|
def route(regime, temp, perf):
|
||||||
"""三态 + 温度 → 各策略权重"""
|
current_regime = regime.get("regime", "unknown")
|
||||||
current_regime = regime.get("regime", "unknown") if regime else "unknown"
|
|
||||||
band = temp.get("band", "unknown")
|
band = temp.get("band", "unknown")
|
||||||
family = temp.get("family", "mr")
|
|
||||||
|
|
||||||
weights = {}
|
weights = {}
|
||||||
for name, cfg in STRATEGY_TEMP.items():
|
for name, regs in perf.items():
|
||||||
# 状态匹配度:适用温区包含当前状态 → 1.0,否则 0.3(保留观察)
|
if not regs:
|
||||||
matched = current_regime in cfg["regimes"]
|
continue
|
||||||
|
cur = regs.get(current_regime)
|
||||||
|
best = max(regs.items(), key=lambda x: x[1].get("win_rate", 0))
|
||||||
|
best_wr = best[1].get("win_rate", 0)
|
||||||
|
cur_wr = cur.get("win_rate") if cur else None
|
||||||
|
matched = (best_wr >= 50) and (cur_wr is not None and cur_wr >= 50)
|
||||||
|
family = FAMILY_MAP.get(name, DEFAULT_FAMILY)
|
||||||
base = 1.0 if matched else 0.3
|
base = 1.0 if matched else 0.3
|
||||||
# 温度乘数(按策略家族)
|
|
||||||
try:
|
try:
|
||||||
from temp_band import temp_multiplier
|
from temp_band import temp_multiplier
|
||||||
mult = temp_multiplier(band, cfg["family"])
|
mult = temp_multiplier(band, family)
|
||||||
except Exception:
|
except Exception:
|
||||||
mult = 0.8
|
mult = 0.8
|
||||||
weights[name] = {
|
weights[name] = {
|
||||||
"weight": round(base * mult, 2),
|
"weight": round(base * mult, 2),
|
||||||
"regime": current_regime,
|
"regime": current_regime,
|
||||||
"temp_band": band,
|
"temp_band": band,
|
||||||
"family": cfg["family"],
|
"family": family,
|
||||||
"matched": matched,
|
"matched": matched,
|
||||||
"desc": cfg["desc"],
|
"best_regime": best[0],
|
||||||
|
"best_win_rate": best[1].get("win_rate", 0),
|
||||||
|
"cur_win_rate": cur.get("win_rate") if cur else None,
|
||||||
|
"cur_avg_pnl": cur.get("avg_pnl") if cur else None,
|
||||||
|
"trades": cur.get("trades") if cur else 0,
|
||||||
}
|
}
|
||||||
return weights
|
return dict(sorted(weights.items(), key=lambda x: -x[1]["weight"]))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
regime = load_regime()
|
regime = load_regime()
|
||||||
temp = get_temp()
|
temp = load_temp()
|
||||||
if not regime:
|
perf = load_regime_perf()
|
||||||
print(" [router] market_regime 无数据", file=sys.stderr)
|
weights = route(regime, temp, perf)
|
||||||
regime = {"regime": "unknown"}
|
|
||||||
|
|
||||||
weights = route(regime, temp)
|
|
||||||
out = {
|
out = {
|
||||||
"state": regime.get("regime", "unknown"),
|
"state": regime.get("regime", "unknown"),
|
||||||
|
"state_date": regime.get("date", ""),
|
||||||
"temp_band": temp.get("band", "unknown"),
|
"temp_band": temp.get("band", "unknown"),
|
||||||
"temp_rsi": temp.get("rsi"),
|
"temp_rsi": temp.get("rsi"),
|
||||||
"weights": weights,
|
"weights": weights,
|
||||||
|
"active": [k for k, v in weights.items() if v["matched"]],
|
||||||
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"note": "三态(trend_up/choppy/trend_down)选策略类型 + 温度(rsi)乘数决定仓位",
|
"note": "温区(平滑K=5)选策略类型 + 温度(rsi)乘数;策略全温区发信号,适用温区由实测动态决定",
|
||||||
}
|
}
|
||||||
OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
|
OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
print(f"strategy_weights.json: 状态={out['state']} 温度={out['temp_band']}(rsi={out['temp_rsi']})")
|
print(f"strategy_weights.json: 状态={out['state']} 温度={out['temp_band']}(rsi={out['temp_rsi']})")
|
||||||
for name, w in weights.items():
|
print(f"激活策略: {out['active']}")
|
||||||
print(f" {name:<12} weight={w['weight']:<5} regime={w['regime']:<10} band={w['temp_band']:<8} {'✓' if w['matched'] else '观察'}")
|
print("权重排名(前15):")
|
||||||
|
for name, w in list(weights.items())[:15]:
|
||||||
|
mark = "✓" if w["matched"] else "观察"
|
||||||
|
print(f" {name:<12} w={w['weight']:<5} {w['best_regime']:<11} 最佳{w['best_win_rate']:>3.0f}% {mark}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -622,6 +622,8 @@ def api_overview():
|
|||||||
"cash": cash, "position_pct": position_pct, "total_pnl": total_pnl,
|
"cash": cash, "position_pct": position_pct, "total_pnl": total_pnl,
|
||||||
"top_movers": top_movers, "market": market,
|
"top_movers": top_movers, "market": market,
|
||||||
"alerts": _load_json(DATA_DIR / "alerts.json", [])[:10],
|
"alerts": _load_json(DATA_DIR / "alerts.json", [])[:10],
|
||||||
|
# 2026-08-13 温区自适应:概览页横幅数据源
|
||||||
|
"regime_weights": _load_json(DATA_DIR / "strategy_weights.json", None),
|
||||||
"updated_at": summary.get("updated_at", ""),
|
"updated_at": summary.get("updated_at", ""),
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
+31
-1
@@ -380,7 +380,37 @@ function renderOverview() {
|
|||||||
</div>
|
</div>
|
||||||
`).join('') : '<div class="text-xs text-slate-500">暂无大幅异动</div>';
|
`).join('') : '<div class="text-xs text-slate-500">暂无大幅异动</div>';
|
||||||
|
|
||||||
el.innerHTML = `
|
// ── 温区自适应横幅(2026-08-13 概览页置顶)──
|
||||||
|
let regimeHtml = '';
|
||||||
|
try {
|
||||||
|
const rw = d.regime_weights || null;
|
||||||
|
if (rw && rw.state) {
|
||||||
|
const regIcon = {trend_up:'📈', choppy:'〰️', trend_down:'📉'}[rw.state] || '🌐';
|
||||||
|
const regName = {trend_up:'趋势市', choppy:'震荡市', trend_down:'下跌市'}[rw.state] || rw.state;
|
||||||
|
const regColor = rw.state==='trend_up' ? 'text-emerald-400' : rw.state==='choppy' ? 'text-amber-400' : 'text-red-400';
|
||||||
|
const bandIcon = {panic:'🔴', fear:'🟠', neutral:'🟡', greed:'🟢', euphoria:'🔵'}[rw.temp_band] || '⚪';
|
||||||
|
const bandName = {panic:'恐慌', fear:'偏冷', neutral:'中性', greed:'偏热', euphoria:'亢奋'}[rw.temp_band] || rw.temp_band;
|
||||||
|
const activeList = (rw.active || []).slice(0, 6);
|
||||||
|
regimeHtml = '<div class="bg-indigo-900/25 border border-indigo-500/40 rounded-xl p-3 mb-4">' +
|
||||||
|
'<div class="flex items-center justify-between flex-wrap gap-2">' +
|
||||||
|
'<div class="flex items-center gap-2"><span class="text-lg">🌡️</span>' +
|
||||||
|
'<span class="font-bold ' + regColor + '">' + regIcon + ' ' + regName + '</span>' +
|
||||||
|
'<span class="text-xs text-slate-400">(平滑' + (rw.state_date||'') + ')</span></div>' +
|
||||||
|
'<div class="flex items-center gap-3 text-xs">' +
|
||||||
|
'<span>温度: <b>' + bandIcon + ' ' + bandName + '</b>' + (rw.temp_rsi ? ' <span class="text-slate-500">RSI=' + rw.temp_rsi + '</span>' : '') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
(activeList.length ? '<div class="flex flex-wrap gap-1.5 mt-2">' + activeList.map(n => {
|
||||||
|
const w = rw.weights && rw.weights[n];
|
||||||
|
const wr = w && w.best_win_rate != null ? ' ' + w.best_win_rate + '%' : '';
|
||||||
|
return '<span class="text-[11px] bg-emerald-500/15 text-emerald-300 border border-emerald-500/30 rounded px-2 py-0.5"><span class="font-mono font-bold">' + n + '</span>' + wr + '</span>';
|
||||||
|
}).join('') + '</div>' : '') +
|
||||||
|
'<div class="text-[11px] text-slate-500 mt-1.5">策略按当前温区自动激活(strategy_weights.json),选股/买卖/重评已温区感知</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
el.innerHTML = regimeHtml + `
|
||||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||||
${statCard('总资产', formatMoney(d.total_assets), '💰')}
|
${statCard('总资产', formatMoney(d.total_assets), '💰')}
|
||||||
${statCard('股票市值', formatMoney(d.stock_value), '📈')}
|
${statCard('股票市值', formatMoney(d.stock_value), '📈')}
|
||||||
|
|||||||
Reference in New Issue
Block a user