From 1094bf75affd50544f1769d8843856532ce876b6 Mon Sep 17 00:00:00 2001 From: xxm Date: Thu, 13 Aug 2026 10:36:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=A9=E5=8C=BA=E8=87=AA=E9=80=82?= =?UTF-8?q?=E5=BA=94=E5=85=A8=E9=93=BE=E8=B7=AF=E2=80=94=E2=80=94=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E7=AD=96=E7=95=A5=E5=BA=93(regime=5Fperf=E5=85=A8?= =?UTF-8?q?=E7=AD=96=E7=95=A5=C3=97=E6=B8=A9=E5=8C=BA=E5=AE=9E=E6=B5=8B,?= =?UTF-8?q?=E5=90=AB=E8=A2=AB=E5=9F=8B=E6=B2=A1=E7=9A=84v=5Fmr=5Fsel=20tre?= =?UTF-8?q?nd=5Fdown=2093%/v7.3=20trend=5Fup=2094%),=20router=20v5?= =?UTF-8?q?=E6=8C=89=E5=BD=93=E5=89=8D=E6=B8=A9=E5=8C=BA=E6=BF=80=E6=B4=BB?= =?UTF-8?q?,=20=E9=80=89=E8=82=A1(mr=5Fscanner/predictive=5Foversold?= =?UTF-8?q?=E5=B9=B3=E6=BB=91=E6=B8=A9=E5=8C=BA=E9=97=A8=E6=8E=A7)+?= =?UTF-8?q?=E4=B9=B0=E5=8D=96(price=5Fmonitor=E9=9D=9E=E6=BF=80=E6=B4=BB?= =?UTF-8?q?=E6=8A=91=E5=88=B6=E4=B9=B0=E5=85=A5=E6=9C=BA=E4=BC=9A=E9=A3=8E?= =?UTF-8?q?=E6=8E=A7=E4=BB=8D=E6=8E=A8)+=E9=87=8D=E8=AF=84(strategy=5Flife?= =?UTF-8?q?cycle=E5=B9=B3=E6=BB=91=E6=B8=A9=E5=8C=BAtrend=5Fdown=20vs=20?= =?UTF-8?q?=E5=8E=9F=E5=A7=8Bchoppy)=E5=85=A8=E6=8E=A5=E5=85=A5,=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AFresearch=20Tab=E6=B8=A9=E5=8C=BA=E6=A8=AA?= =?UTF-8?q?=E5=B9=85+=E6=B8=A9=E5=BA=A6+=E6=BF=80=E6=B4=BB=E7=AD=96?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/regime_gate.py | 100 +++++++++++ deploy/profile-scripts/regime_perf.py | 174 +++++++++++++++++++ deploy/profile-scripts/strategy_lifecycle.py | 15 +- evolution/evolution_api.py | 23 +++ 4 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 deploy/profile-scripts/regime_gate.py create mode 100644 deploy/profile-scripts/regime_perf.py diff --git a/deploy/profile-scripts/regime_gate.py b/deploy/profile-scripts/regime_gate.py new file mode 100644 index 00000000..9878cbf4 --- /dev/null +++ b/deploy/profile-scripts/regime_gate.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""regime_gate.py — 温区门控共享工具(2026-08-13) + +scanner/重评统一调用:读取当前温区(优先平滑 K=5,回退原始 market_regime), +判断某策略是否在当前温区激活。 + +用法: + from regime_gate import get_current_regime, is_strategy_active, strategy_enabled + rg = get_current_regime() # {"regime": "trend_down", "date": "..."} + ok = is_strategy_active("v_weak") # 该策略是否当前温区激活(读 strategy_weights.json) +""" +import json +import sqlite3 +from pathlib import Path + +MOFIN_DATA = "/home/hmo/MoFin/data" +WEIGHTS_FILE = Path(MOFIN_DATA) / "strategy_weights.json" +SMOOTHED_FILE = Path(MOFIN_DATA) / "market_regime_smoothed.json" +DB = Path(MOFIN_DATA) / "mofin.db" + +_cache_regime = None +_cache_weights = None + + +def get_current_regime(use_smoothed=True): + """读取当前温区。优先平滑(K=5),回退原始 market_regime。返回 {"regime","date"}""" + global _cache_regime + if _cache_regime: + return _cache_regime + # 1. 平滑温区(regime_tracker K=5) + if use_smoothed: + try: + if SMOOTHED_FILE.exists(): + d = json.loads(SMOOTHED_FILE.read_text(encoding="utf-8")) + _cache_regime = { + "regime": d.get("current_regime", "unknown"), + "date": d.get("current_date", ""), + } + return _cache_regime + except Exception: + pass + # 2. 原始 market_regime 表 + try: + conn = sqlite3.connect(str(DB), timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + row = conn.execute( + "SELECT date, regime FROM market_regime ORDER BY date DESC LIMIT 1" + ).fetchone() + conn.close() + if row: + _cache_regime = {"regime": row[1], "date": row[0]} + return _cache_regime + except Exception: + pass + return {"regime": "unknown", "date": ""} + + +def _load_weights(): + """读取 strategy_weights.json(缓存)""" + global _cache_weights + if _cache_weights is not None: + return _cache_weights + try: + if WEIGHTS_FILE.exists(): + _cache_weights = json.loads(WEIGHTS_FILE.read_text(encoding="utf-8")) + return _cache_weights + except Exception: + pass + _cache_weights = {} + return _cache_weights + + +def is_strategy_active(strategy_name): + """该策略是否在当前温区激活(matched)。无数据默认激活""" + w = _load_weights() + if not w or not w.get("weights"): + return True + entry = w["weights"].get(strategy_name) + if entry is None: + return True # 不在权重表 → 默认激活(持仓管理类) + return entry.get("matched", True) + + +def strategy_enabled(strategy_name): + """该策略权重>0(激活 + 非0乘数)""" + w = _load_weights() + if not w or not w.get("weights"): + return True + entry = w["weights"].get(strategy_name) + if entry is None: + return True + return entry.get("weight", 0) > 0 + + +if __name__ == "__main__": + rg = get_current_regime() + print(f"当前温区: {rg['regime']} ({rg['date']})") + for s in ["v_weak", "v_oversold", "v_next4", "s2_panic", "v_mr_sel"]: + print(f" {s}: active={is_strategy_active(s)} enabled={strategy_enabled(s)}") diff --git a/deploy/profile-scripts/regime_perf.py b/deploy/profile-scripts/regime_perf.py new file mode 100644 index 00000000..339a6cc8 --- /dev/null +++ b/deploy/profile-scripts/regime_perf.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""regime_perf.py — 策略-温区表现常态化记录(2026-08-13) + +记录策略在不同温区(trend_up/choppy/trend_down)的表现,供"适用温度"动态评估。 +- 数据来源:strategy_research 回测 trades(按入场日归入温区)+ 实盘 strategy_tracking +- 表:strategy_regime_perf(strategy, regime, trades, win_rate, avg_pnl, updated_at) +- 原则:策略全温区发信号(去门控后),记录各温区真实表现;适用温区是动态的,随数据更新 + +用法: + python3 regime_perf.py # 全量更新(从回测+实盘重算) + from regime_perf import get_regime_perf +""" +import sys +import json +import sqlite3 +from pathlib import Path +from datetime import datetime +from collections import defaultdict + +_SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(_SCRIPT_DIR)) +sys.path.insert(0, "/home/hmo/MoFin") + +DB = "/home/hmo/MoFin/data/mofin.db" + +def load_all_strategies(): + """从 strategy_research 读取所有策略版本(含历史/表现不佳的——可能在特定温区能打)""" + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + rows = conn.execute("SELECT DISTINCT version FROM strategy_research ORDER BY version").fetchall() + conn.close() + return [r[0] for r in rows if r[0]] + +# 关注的策略(动态:全部版本) +STRATEGIES = load_all_strategies() + +def load_regime_map(): + """date -> regime(用平滑 regime_tracker 的周期反查更合理,这里用 market_regime 原始 + 手动按 K=5 平滑) + 简化:直接用 market_regime 的 regime(与平滑 K=5 差异主要在边界几天,评估可接受)""" + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + rows = conn.execute("SELECT date, regime FROM market_regime").fetchall() + conn.close() + return dict(rows) + +def get_trades_from_research(version): + """从 strategy_research 取最新回测 trades""" + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + rows = conn.execute( + "SELECT results_json FROM strategy_research WHERE version=? ORDER BY period_tag DESC, created_at DESC LIMIT 1", + (version,) + ).fetchall() + conn.close() + if not rows: + return [] + try: + return json.loads(rows[0][0]).get("trades", []) + except Exception: + return [] + +def get_trades_from_tracking(): + """从实盘 strategy_tracking 取已平仓交易""" + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + rows = conn.execute( + "SELECT version_seq, tracked_at, theoretical_pnl FROM strategy_tracking WHERE status='closed'" + ).fetchall() + conn.close() + result = [] + for version_seq, tracked_at, pnl in rows: + if version_seq and tracked_at: + result.append({"version": version_seq, "entry_date": tracked_at[:10], "profit_pct": pnl}) + return result + +def compute(use_tracking=True): + """计算所有策略各温区表现""" + regime_map = load_regime_map() + stats = defaultdict(lambda: defaultdict(lambda: {"n": 0, "win": 0, "pnl": 0})) + + for v in STRATEGIES: + trades = get_trades_from_research(v) + for t in trades: + ed = t.get("entry_date", "") + if ed not in regime_map: + continue + reg = regime_map[ed] + pnl = t.get("profit_pct", 0) or 0 + stats[v][reg]["n"] += 1 + stats[v][reg]["pnl"] += pnl + if pnl > 0: + stats[v][reg]["win"] += 1 + + if use_tracking: + for t in get_trades_from_tracking(): + v = t["version"] + if v not in stats: + stats[v] = defaultdict(lambda: {"n": 0, "win": 0, "pnl": 0}) + ed = t["entry_date"] + if ed in regime_map: + reg = regime_map[ed] + pnl = t["profit_pct"] or 0 + stats[v][reg]["n"] += 1 + stats[v][reg]["pnl"] += pnl + if pnl > 0: + stats[v][reg]["win"] += 1 + + return stats + +def save(stats): + """写入 strategy_regime_perf 表(清空重建,保持与最新数据同步)""" + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_regime_perf ( + strategy TEXT, + regime TEXT, + trades INTEGER, + win_rate REAL, + avg_pnl REAL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (strategy, regime) + ) + """) + conn.execute("DELETE FROM strategy_regime_perf") + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for v, regs in stats.items(): + for reg, s in regs.items(): + if s["n"] < 2: + continue # 样本太少不记录(>=2 给观察机会) + wr = s["win"] / s["n"] * 100 + avg = s["pnl"] / s["n"] + conn.execute( + "INSERT OR REPLACE INTO strategy_regime_perf (strategy, regime, trades, win_rate, avg_pnl, updated_at) VALUES (?,?,?,?,?,?)", + (v, reg, s["n"], round(wr, 1), round(avg, 2), now) + ) + conn.commit() + conn.close() + +def main(): + stats = compute(use_tracking=True) + save(stats) + # 打印 + print("=== 策略-温区表现(strategy_regime_perf)===") + conn = sqlite3.connect(DB, timeout=30) + rows = conn.execute("SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf ORDER BY strategy, regime").fetchall() + conn.close() + for r in rows: + print(f" {r[0]:<12} {r[1]:<12} {r[2]:>4}笔 胜率{r[3]:.0f}% 均盈{r[4]:+.2f}%") + +def get_regime_perf(strategy=None): + """读取策略-温区表现(供 router 动态适用温区)""" + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + if strategy: + rows = conn.execute( + "SELECT regime, trades, win_rate, avg_pnl FROM strategy_regime_perf WHERE strategy=?", + (strategy,) + ).fetchall() + else: + rows = conn.execute( + "SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf" + ).fetchall() + conn.close() + if strategy: + return {r[0]: {"trades": r[1], "win_rate": r[2], "avg_pnl": r[3]} for r in rows} + result = defaultdict(dict) + for r in rows: + result[r[0]][r[1]] = {"trades": r[2], "win_rate": r[3], "avg_pnl": r[4]} + return dict(result) + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/strategy_lifecycle.py b/deploy/profile-scripts/strategy_lifecycle.py index 15ac37dd..c911ee1f 100644 --- a/deploy/profile-scripts/strategy_lifecycle.py +++ b/deploy/profile-scripts/strategy_lifecycle.py @@ -2035,8 +2035,19 @@ def reassess_with_context(code, name, price, cost, shares, current_action, # 大盘市场阶段(market_regime)— 与回测 _load_index_ctx 同算法,趋势市放行追涨 market_regime = None try: - import market_regime as _mr - market_regime = _mr.load_market_regime() + # 2026-08-13 平滑温区优先(K=5 regime_tracker),回退原始 market_regime + import sys as _sys + from pathlib import Path as _P + _sp = _P("/home/hmo/MoFin/deploy/profile-scripts") + if str(_sp) not in _sys.path: + _sys.path.insert(0, str(_sp)) + from regime_gate import get_current_regime + _rg = get_current_regime() + if _rg and _rg.get("regime") != "unknown": + market_regime = _rg + else: + import market_regime as _mr + market_regime = _mr.load_market_regime() except Exception: pass # market_regime 不可用时不阻塞单只重评 diff --git a/evolution/evolution_api.py b/evolution/evolution_api.py index dc6ac615..d1f01ffb 100644 --- a/evolution/evolution_api.py +++ b/evolution/evolution_api.py @@ -163,8 +163,31 @@ def get_combo_dashboard(): } conn.close() + + # 2026-08-13 温区自适应:并入 strategy_weights.json(当前温区+温度+各策略权重/激活) + # + strategy_alerts.json(三振出局状态) + import json as _json + from pathlib import Path as _Path + _d = _Path("/home/hmo/MoFin/data") + weights_data = None + alerts_data = None + try: + _w = _d / "strategy_weights.json" + if _w.exists(): + weights_data = _json.loads(_w.read_text(encoding="utf-8")) + except Exception: + pass + try: + _a = _d / "strategy_alerts.json" + if _a.exists(): + alerts_data = _json.loads(_a.read_text(encoding="utf-8")) + except Exception: + pass + return { "regime": regime, + "regime_weights": weights_data, # 当前温区/温度/各策略权重/激活 + "strategy_alerts": alerts_data, # 三振出局状态 "combos": combos, "members": members, "routing": [