From 772fcc3ac9fedb7b83ac524f2cefd85c75bb5027 Mon Sep 17 00:00:00 2001 From: xxm Date: Sat, 15 Aug 2026 22:38:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=A9=E5=8C=BA=E8=A1=A8=E7=8E=B0?= =?UTF-8?q?=E6=8C=89=E5=91=A8=E6=9C=9F=E9=A2=84=E8=AE=A1=E7=AE=97=E8=84=9A?= =?UTF-8?q?=E6=9C=AC(regime=5Fperf=5Fby=5Fperiod.py)=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=8A=A0=E5=B7=A5=E5=B1=82=E8=90=BD=E5=9C=B0?= =?UTF-8?q?,1y/2y/5y/10y=E5=90=84=E7=AE=97=E4=B8=80=E4=BB=BD,=E7=BA=BF?= =?UTF-8?q?=E6=80=A7=E5=B9=B4=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../profile-scripts/regime_perf_by_period.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 deploy/profile-scripts/regime_perf_by_period.py diff --git a/deploy/profile-scripts/regime_perf_by_period.py b/deploy/profile-scripts/regime_perf_by_period.py new file mode 100644 index 00000000..ae8b20cd --- /dev/null +++ b/deploy/profile-scripts/regime_perf_by_period.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""regime_perf_by_period.py — 策略-温区表现按周期预计算(2026-08-15 数据加工层落地) + +背景(老莫指正):温区数据是数据加工层,必须预计算存储,不能每次请求现场重算。 +本脚本把每个策略×市场×周期(1y/2y/5y/10y)×温区的表现预先算好落库, +server 直接读表返回(毫秒级,无需内存缓存/现场计算)。 + +与 regime_perf.py 的区别: + - regime_perf.py 只算最长窗口(全量),写 strategy_regime_perf + - 本脚本按周期分窗,写 strategy_regime_perf_by_period(新增 period_tag 维度) + - 年化用【线性放大】:温区组合总收益 × (窗口交易日数 / 该温区窗口内天数) + (替代 portfolio_sim 的复利年化——复利在温区 trades 跨度短时会爆炸) + +用法: + python3 regime_perf_by_period.py --market=a --periods="1y 2y 5y 10y" + python3 regime_perf_by_period.py --market=hk --periods="2y" +""" +import sys +import json +import math +import sqlite3 +from datetime import datetime +from collections import defaultdict + +sys.path.insert(0, "/home/hmo/MoFin") +sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") + +DB = "/home/hmo/MoFin/data/mofin.db" + +# 周期 → 目标 period_tag(strategy_research 里的记录标签) +PERIOD_TAGS = ["1y", "2y", "5y", "10y"] + + +def get_conn(): + conn = sqlite3.connect(DB, timeout=60) + conn.execute("PRAGMA busy_timeout=60000") + return conn + + +def create_table(conn): + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_regime_perf_by_period ( + strategy TEXT, + market TEXT NOT NULL DEFAULT 'a', + regime TEXT, + period_tag TEXT NOT NULL DEFAULT '2y', + trades INTEGER, + win_rate REAL, + avg_pnl REAL, + avg_hold_days REAL, + total_return_pct REAL, + cagr_pct REAL, + portfolio_max_dd_pct REAL, + capital_final REAL, + positions_taken INTEGER, + sharpe_ratio REAL, + profit_factor REAL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (strategy, market, regime, period_tag) + ) + """) + + +def calc_extra(trades): + """从 trades 算温区级 win_rate/avg_pnl/avg_hold/sharpe/profit_factor""" + if not trades: + return {} + profits = [t.get("profit_pct", 0) for t in trades] + wins = [p for p in profits if p > 0] + losses = [p for p in profits if p <= 0] + win_rate = len(wins) / len(profits) * 100 if profits else 0 + avg_p = sum(profits) / len(profits) if profits else 0 + avg_w = sum(wins) / len(wins) if wins else 0 + avg_l = abs(sum(losses) / len(losses)) if losses else 1 + pf = avg_w / avg_l if avg_l > 0 else 0 + mean_r = avg_p / 100 + std_r = math.sqrt(sum((p / 100 - mean_r) ** 2 for p in profits) / (len(profits) - 1)) if len(profits) > 1 else 0 + sharpe = mean_r / std_r * math.sqrt(252) if std_r > 0 else 0 + holds = [t.get("hold_days", 0) for t in trades if t.get("hold_days")] + avg_hold = sum(holds) / len(holds) if holds else 0 + return {"win_rate": round(win_rate, 1), "avg_pnl": round(avg_p, 2), + "avg_hold_days": round(avg_hold, 1), "sharpe_ratio": round(sharpe, 2), + "profit_factor": round(pf, 2)} + + +def portfolio_sim_wrap(trades, capital=1000000, max_positions=10): + """温区 trades → 组合模拟(复用 strategy_lab.portfolio_sim)""" + if not trades: + return {} + try: + from strategy_lab import portfolio_sim + return portfolio_sim(trades, capital=capital, max_positions=max_positions, cost=True) + except Exception: + return {} + + +def regime_days_in_window(conn, market, d_min, d_max): + """窗口内各温区天数 + 总天数(线性年化用)""" + rows = conn.execute( + "SELECT regime, COUNT(*) n FROM market_regime WHERE market=? AND date>=? AND date<=? GROUP BY regime", + (market, d_min, d_max)).fetchall() + total = sum(r[1] for r in rows) + return {r[0]: r[1] for r in rows}, total + + +def process_period(conn, market, period_tag): + """处理单个周期:所有策略的温区表现,写入 strategy_regime_perf_by_period""" + # 温区映射 + rmap = dict(conn.execute( + "SELECT date, regime FROM market_regime WHERE market=?", (market,)).fetchall()) + # 该周期所有策略记录 + rows = conn.execute( + "SELECT version, results_json FROM strategy_research " + "WHERE COALESCE(market,'a')=? AND period_tag=? ORDER BY version", + (market, period_tag)).fetchall() + + written = 0 + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for v, results_json in rows: + if not results_json: + continue + try: + trades = json.loads(results_json).get("trades", []) + except Exception: + continue + if not trades: + continue + # 温区归因 + by_regime = defaultdict(list) + for t in trades: + ed = t.get("entry_date", "") + if ed in rmap: + by_regime[rmap[ed]].append(t) + if not by_regime: + continue + # 窗口天数(该策略 trades 的 entry_date 范围) + eds = [t.get("entry_date") for t in trades if t.get("entry_date") and t.get("entry_date") in rmap] + if not eds: + continue + reg_days, total_days = regime_days_in_window(conn, market, min(eds), max(eds)) + for reg, reg_trades in by_regime.items(): + if len(reg_trades) < 2: + continue + extra = calc_extra(reg_trades) + sim = portfolio_sim_wrap(reg_trades) + if not sim: + continue + ret = sim.get("total_return_pct") + # 线性年化:收益 × (窗口总天数 / 该温区窗口内天数) + rd = reg_days.get(reg, 0) + cagr = round(ret * (total_days / rd), 1) if ret is not None and rd > 0 and total_days > 0 else None + conn.execute( + """INSERT OR REPLACE INTO strategy_regime_perf_by_period + (strategy, market, regime, period_tag, trades, win_rate, avg_pnl, avg_hold_days, + total_return_pct, cagr_pct, portfolio_max_dd_pct, capital_final, + positions_taken, sharpe_ratio, profit_factor, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + (v, market, reg, period_tag, len(reg_trades), + extra.get("win_rate"), extra.get("avg_pnl"), extra.get("avg_hold_days"), + ret, cagr, sim.get("portfolio_max_dd_pct"), + sim.get("capital_final"), sim.get("positions_taken"), + extra.get("sharpe_ratio"), extra.get("profit_factor"), now)) + written += 1 + return written + + +def main(): + market = "a" + periods = PERIOD_TAGS + for a in sys.argv[1:]: + if a.startswith("--market="): + market = a.split("=", 1)[1] + elif a.startswith("--periods="): + periods = a.split("=", 1)[1].split() + + conn = get_conn() + create_table(conn) + # 先清该市场旧数据 + conn.execute("DELETE FROM strategy_regime_perf_by_period WHERE market=?", (market,)) + total = 0 + for pt in periods: + n = process_period(conn, market, pt) + print(f"[{market}][{pt}] 写入 {n} 条", flush=True) + total += n + conn.commit() + conn.close() + print(f"完成: market={market} periods={periods} 共 {total} 条") + + +if __name__ == "__main__": + main()