diff --git a/deploy/profile-scripts/regime_tracker.py b/deploy/profile-scripts/regime_tracker.py new file mode 100644 index 00000000..95e7a057 --- /dev/null +++ b/deploy/profile-scripts/regime_tracker.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""regime_tracker.py — 温区平滑跟踪 + 周期记录(2026-08-13) + +设计(老莫确认方向 + 数据选参): +- 实时温区判定:滞回确认 K=5(连续5天同温区才确认,滞后约5天,宁慢勿错) + 数据依据:K=5 → 112周期/平均20.7天/无1天噪音(vs 原始457周期/151个1天) +- 温区周期记录:regime_cycles 表(start/end/regime/days),供策略评估归因 +- 温度(rsi):不滞后,实时反映恐慌/亢奋(与温区互补:温区滞后、温度实时) + +写表: regime_cycles(date_start, date_end, regime, days) +输出: market_regime_smoothed.json(当前平滑温区 + 温度) +""" +import sys +import json +import sqlite3 +from pathlib import Path +from datetime import datetime + +_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" +OUT = "/home/hmo/MoFin/data/market_regime_smoothed.json" + +# 滞回确认天数(数据选参:K=5 甜区) +CONFIRM_DAYS = 5 + +def load_daily_regime(): + """读取 market_regime 逐日数据(时间正序)""" + conn = sqlite3.connect(DB, timeout=5) + rows = conn.execute( + "SELECT date, above_ma20, adx, regime FROM market_regime ORDER BY date ASC" + ).fetchall() + conn.close() + return rows + +def classify_day(above, adx): + """单日温区(与 market_regime 同逻辑)""" + if above == 1 and adx is not None and adx >= 20: + return "trend_up" + if adx is not None and adx < 20: + return "choppy" + return "trend_down" + +def smooth_states(rows, k=CONFIRM_DAYS): + """滞回确认:连续 K 天同温区才确认切换。返回 (states, cycles)""" + dates = [r[0] for r in rows] + raw = [classify_day(r[1], r[2]) for r in rows] + n = len(dates) + + # 状态机:current 确认态;每 K 天窗口看是否一致 + states = [None] * n + current = None + for i in range(n): + if i < k - 1: + continue + window = raw[i - k + 1:i + 1] + if len(set(window)) == 1: + # 连续 K 天同温区 → 确认(切换) + current = window[0] + states[i] = current if current is not None else raw[i] + # 开头填补(前 K-1 天用原始值) + for i in range(min(k - 1, n)): + states[i] = raw[i] + + # 聚合周期 + cycles = [] + cur = None + for i in range(n): + s = states[i] + if cur is None or s != cur["regime"]: + if cur: + cycles.append(cur) + cur = {"regime": s, "start": dates[i], "end": dates[i], "days": 1} + else: + cur["end"] = dates[i] + cur["days"] += 1 + if cur: + cycles.append(cur) + return states, cycles, dates + +def save_cycles(cycles): + """写入 regime_cycles 表""" + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + conn.execute(""" + CREATE TABLE IF NOT EXISTS regime_cycles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + regime TEXT, + start_date TEXT, + end_date TEXT, + days INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + # 清空重建(保持与 market_regime 同步) + conn.execute("DELETE FROM regime_cycles") + for cy in cycles: + conn.execute( + "INSERT INTO regime_cycles (regime, start_date, end_date, days) VALUES (?,?,?,?)", + (cy["regime"], cy["start"], cy["end"], cy["days"]) + ) + conn.commit() + conn.close() + return len(cycles) + +def get_temp(): + """实时温度(rsi 档位,不滞后)""" + try: + from temp_band import get_market_temp + return get_market_temp() + except Exception: + return {"band": "unknown", "rsi": None} + +def main(): + rows = load_daily_regime() + if len(rows) < CONFIRM_DAYS + 1: + print(f"数据不足: {len(rows)} 条") + return + + states, cycles, dates = smooth_states(rows) + n_cycles = save_cycles(cycles) + + # 当前平滑温区(最新确认态) + current_regime = states[-1] + current_date = dates[-1] + temp = get_temp() + + # 最近周期列表 + recent = cycles[-8:] + out = { + "current_regime": current_regime, + "current_date": current_date, + "confirm_days": CONFIRM_DAYS, + "temp": temp, + "total_cycles": n_cycles, + "recent_cycles": recent, + "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + } + Path(OUT).write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8") + + print(f"平滑温区: {current_regime} (确认期{CONFIRM_DAYS}天, 至{current_date})") + print(f"温度: {temp.get('band')} (rsi={temp.get('rsi')})") + print(f"周期总数: {n_cycles}") + print("最近周期:") + for cy in recent: + print(f" {cy['regime']:<12} {cy['start']} ~ {cy['end']} ({cy['days']}天)") + +if __name__ == "__main__": + main() diff --git a/deploy/profile-scripts/strategy_router.py b/deploy/profile-scripts/strategy_router.py index 48e2dd58..834d7567 100644 --- a/deploy/profile-scripts/strategy_router.py +++ b/deploy/profile-scripts/strategy_router.py @@ -29,7 +29,18 @@ OUT = Path("/home/hmo/MoFin/data/strategy_weights.json") def load_regime(): - """读取最新 market_regime(三态)""" + """读取平滑温区(regime_tracker 输出,K=5 滞回确认)。 + 优先用平滑结果(避免单日噪音误判);regime_tracker 不可用时回退原始 market_regime。""" + try: + p = Path("/home/hmo/MoFin/data/market_regime_smoothed.json") + if p.exists(): + d = json.loads(p.read_text(encoding="utf-8")) + return {"regime": d.get("current_regime", "unknown"), + "date": d.get("current_date", ""), + "temp_band": (d.get("temp") or {}).get("band"), + "temp_rsi": (d.get("temp") or {}).get("rsi")} + except Exception: + pass try: from market_regime import load_market_regime return load_market_regime() @@ -38,9 +49,9 @@ def load_regime(): def get_temp(): - """读取市场温度(rsi 档位)""" + """读取市场温度(rsi 档位)。regime_tracker 已含温度则直接用,否则实时算。""" try: - from temp_band import get_market_temp, temp_multiplier + from temp_band import get_market_temp return get_market_temp() except Exception as e: print(f" [router] 温度获取失败: {e}", file=sys.stderr) diff --git a/docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md b/docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md index 8a5c9407..a8666941 100644 --- a/docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md +++ b/docs/decisions/2026-08-13-事项五-策略组合市场测温动态切换.md @@ -205,3 +205,48 @@ | strategy_alert.py | 三振出局失效预警 | 当前:choppy×neutral → v_weak/v_lurk_v3 weight=0.8,v_next4/v8.1 观察。 + + +--- + +## 七、温区平滑与周期记录(2026-08-13 老莫补充设计) + +### 7.1 设计原则(老莫定) + +1. **策略内部不放温区门控因子**(v_next4 的 行业ADX>25 是反例,应去除)——让策略在所有温区都能发信号,才能测出全温区表现 +2. **策略带"适用温区"属性**——记录温区测试结果,不是内部硬门控 +3. **适用温区是动态的**——常态化测试,记录策略在不同温区随时间的变化 +4. **系统监控温区变化、记录温区周期**——以温区周期为锚点,触发策略周期性评估 + +### 7.2 平滑手段(数据选参) + +**问题**:日级 regime 切换太频繁(原始 457 周期/151个1天周期),无法直接作为评估锚点。 + +**方案**:滞回确认(hysteresis)——连续 K 天同温区才确认切换。 + +**数据选参**: +| K(确认天数) | 周期数 | 平均长度 | 效果 | +|---|---|---|---| +| K=1(原始) | 457 | 5.1天 | 太碎,151个1天 | +| K=3 | 177 | 13.1天 | 无1天,仍偏碎 | +| **K=5** | **112** | **20.7天** | **甜区:无1天噪音,周期合理** | +| K=10 | 49 | 47.3天 | 过度平滑,choppy 只剩391天 | + +**选 K=5**:滞后约5天(宁慢勿错),事后评估与实时判断统一口径。 + +### 7.3 实时 vs 事后 + +- **实时温区**:K=5 滞回确认(滞后5天,确认才切换策略权重) +- **温度(rsi)**:不滞后(连续量,实时反映恐慌/亢奋,用于仓位乘数) +- **事后评估**:用 K=5 平滑后的周期做策略-温区归因 + +**温区与温度互补**:温区滞后、温度实时。例:当前平滑温区=trend_down(7/17起19天)但温度=neutral(rsi 58)——阴跌状态,v_oversold/s2_panic 主导(weight 0.8),v_weak 观察(0.24)。 + +### 7.4 落地文件 + +| 文件 | 功能 | +|---|---| +| regime_tracker.py | 滞回平滑(K=5)+ 周期记录 regime_cycles 表 + 输出平滑温区 | +| strategy_router.py v3 | 读平滑温区 + 温度 → strategy_weights.json | + +**cron 顺序**(每日):16:50 market_regime → 16:52 regime_tracker(平滑)→ 16:55 strategy_router(路由)