From fa267f2e7bd2fb846ebda1f457d56f842aa0b450 Mon Sep 17 00:00:00 2001 From: xxm Date: Sat, 15 Aug 2026 22:44:38 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B8=A9=E5=8C=BA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=94=B9=E8=AF=BB=E9=A2=84=E8=AE=A1=E7=AE=97=E8=A1=A8?= =?UTF-8?q?(strategy=5Fregime=5Fperf=5Fby=5Fperiod)=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=8E=B0=E5=9C=BA=E9=87=8D=E7=AE=97+?= =?UTF-8?q?=E5=86=85=E5=AD=98=E7=BC=93=E5=AD=98,=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=8A=A0=E5=B7=A5=E5=B1=82=E6=AF=8F=E6=97=A5=E9=A2=84=E8=AE=A1?= =?UTF-8?q?=E7=AE=97,server=E5=8F=AA=E8=AF=BB=E8=A1=A8=E6=AF=AB=E7=A7=92?= =?UTF-8?q?=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.py | 169 ++++++++++-------------------------------------------- 1 file changed, 31 insertions(+), 138 deletions(-) diff --git a/server.py b/server.py index 8ab62aa2..e865b99c 100644 --- a/server.py +++ b/server.py @@ -45,25 +45,11 @@ _regime_winrates_cache = {"slots": {}} # slots: {period_tag: (data_version_key, def _compute_regime_winrates_cached(pt, _approx_univ): - """按 period_tag 从 strategy_research 对应周期 trades 现场温区归因(带缓存)""" + """按 period_tag 读预计算表 strategy_regime_perf_by_period(2026-08-15 数据加工层) + 数据由 regime_perf_by_period.py 每日盘后预计算(含线性年化),server 只读表,毫秒级。 + 无对应周期记录时回退到最近更长周期(1m/6m→1y,2y→2y,5y→5y,10y→10y)。 + """ import sqlite3 as _sq - import math as _mth - from collections import defaultdict as _dd - from flask import request as _req - - # 缓存键 - _cache_key = None - try: - _cc = _sq.connect(str(DATA_DIR / "mofin.db"), timeout=5) - _ver_s = _cc.execute("SELECT MAX(created_at) FROM strategy_research").fetchone()[0] - _cc.close() - _cache_key = (pt or "2y") + "|" + str(_ver_s) - except Exception as _e: - _cache_key = None - _slots = _regime_winrates_cache.get("slots") or {} - _slot = _slots.get(pt or "2y") - if _cache_key and _slot and _slot[0] == _cache_key: - return _slot[1] _pt_chain = {'1m': '1y', '6m': '1y', '1y': '1y', '2y': '2y', '5y': '5y', '10y': '10y'} _pt_use = _pt_chain.get(pt or '2y', '2y') @@ -71,132 +57,39 @@ def _compute_regime_winrates_cached(pt, _approx_univ): try: _c = _sq.connect(str(DATA_DIR / "mofin.db"), timeout=10) _c.execute("PRAGMA busy_timeout=10000") - _row_map = {} for _r in _c.execute( - "SELECT version, market, period_tag, results_json FROM strategy_research " - "ORDER BY version, market, " - "CASE period_tag WHEN '1y' THEN 1 WHEN '2y' THEN 2 WHEN '5y' THEN 3 WHEN '10y' THEN 4 ELSE 5 END" - ).fetchall(): - _k = (_r[0], _r[1]) - if _k not in _row_map: - _row_map[_k] = {} - _row_map[_k][_r[2]] = _r[3] - _rmap_a = dict(_c.execute("SELECT date, regime FROM market_regime WHERE market='a'").fetchall()) - _rmap_hk = dict(_c.execute("SELECT date, regime FROM market_regime WHERE market='hk'").fetchall()) + "SELECT 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 FROM strategy_regime_perf_by_period " + "WHERE period_tag=? ORDER BY strategy, market, regime", + (_pt_use,)).fetchall(): + _ver, _mkt, _reg = _r[0], _r[1], _r[2] + _cagr_v = _r[9] + _ret_v = _r[8] + _dd_v = _r[10] + _cf_v = _r[11] + _pt_v = _r[12] + _sh_v = _r[13] + _pf_v = _r[14] + _regime_winrates.setdefault(_ver, {})[_reg] = { + "trades": _r[4], "win_rate": _r[5], "avg_pnl": _r[6], + "avg_hold_days": _r[7], + "total_return_pct": _ret_v, "cagr_pct": _cagr_v, + "max_dd_pct": _dd_v, "capital_final": _cf_v, + "positions_taken": _pt_v, "sharpe_ratio": _sh_v, + "profit_factor": _pf_v, + "period_tag": _pt_use, + "portfolio": {"cagr_pct": _cagr_v, "total_return_pct": _ret_v, + "portfolio_max_dd_pct": _dd_v, "capital_final": _cf_v, + "positions_taken": _pt_v, "sharpe_ratio": _sh_v, + "profit_factor": _pf_v}, + "universality": _approx_univ(_ver, _reg, _r[4]), + } _c.close() - - def _calc_extra(_trades): - 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] - _wr = len(_wins) / len(_profits) * 100 if _profits else 0 - _avg = 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 - _mr = _avg / 100 - _std = _mth.sqrt(sum((p / 100 - _mr) ** 2 for p in _profits) / (len(_profits) - 1)) if len(_profits) > 1 else 0 - _sh = _mr / _std * _mth.sqrt(252) if _std > 0 else 0 - _holds = [t.get("hold_days", 0) for t in _trades if t.get("hold_days")] - _ah = sum(_holds) / len(_holds) if _holds else 0 - return {"win_rate": round(_wr, 1), "avg_pnl": round(_avg, 2), - "avg_hold_days": round(_ah, 1), "sharpe_ratio": round(_sh, 2), - "profit_factor": round(_pf, 2)} - - def _portfolio_sim(_trades, _cap=1000000, _slots=10): - if not _trades: - return {} - try: - from strategy_lab import portfolio_sim - return portfolio_sim(_trades, capital=_cap, max_positions=_slots, cost=True) - except Exception: - return {} - - for (_ver, _mkt), _periods in _row_map.items(): - _js = None - _cand = None - for _p in [_pt_use, '5y', '10y']: - if _p in _periods and _periods[_p]: - _cand = _periods[_p] - break - if not _cand: - continue - try: - _trades = json.loads(_cand).get("trades", []) - except Exception: - continue - if not _trades: - continue - _rmap = _rmap_a if _mkt != 'hk' else _rmap_hk - # ── 温区天数(该策略 trades 窗口内,线性年化用)── - _pt_days = {"total": 0, "regimes": {}} - try: - _eds = [t.get("entry_date", "") for t in _trades if t.get("entry_date")] - _eds = [d for d in _eds if d in _rmap] - if _eds: - _d_min, _d_max = min(_eds), max(_eds) - _cnt = {} - _tot = 0 - for _d, _reg in _rmap.items(): - if _d_min <= _d <= _d_max: - _tot += 1 - _cnt[_reg] = _cnt.get(_reg, 0) + 1 - _pt_days = {"total": _tot, "regimes": _cnt} - except Exception: - pass - _by_regime = _dd(list) - for _t in _trades: - _ed = _t.get("entry_date", "") - if _ed in _rmap: - _by_regime[_rmap[_ed]].append(_t) - for _reg, _reg_trades in _by_regime.items(): - if len(_reg_trades) < 2: - continue - _extra = _calc_extra(_reg_trades) - _sim = _portfolio_sim(_reg_trades) - if not _sim: - continue - _wr_v = _extra.get("win_rate") - # 2026-08-15 温区年化线性放大:收益 × (窗口总天数/该温区天数),消除复利爆炸 - _ret_v = _sim.get("total_return_pct") - _reg_days = (_pt_days.get("regimes") or {}).get(_reg, 0) - _pt_total = _pt_days.get("total", 0) - if _ret_v is not None and _reg_days > 0 and _pt_total > 0: - _cagr_v = round(_ret_v * (_pt_total / _reg_days), 1) - else: - _cagr_v = None - _dd_v = _sim.get("portfolio_max_dd_pct") - _cf_v = _sim.get("capital_final") - _pt_v = _sim.get("positions_taken") - _sh_v = _extra.get("sharpe_ratio") - _pf_v = _extra.get("profit_factor") - _regime_winrates.setdefault(_ver, {})[_reg] = { - "trades": len(_reg_trades), - "win_rate": _wr_v, "avg_pnl": _extra.get("avg_pnl"), - "avg_hold_days": _extra.get("avg_hold_days"), - "total_return_pct": _ret_v, "cagr_pct": _cagr_v, - "max_dd_pct": _dd_v, "capital_final": _cf_v, - "positions_taken": _pt_v, "sharpe_ratio": _sh_v, - "profit_factor": _pf_v, - "period_tag": _pt_use, - "portfolio": {"cagr_pct": _cagr_v, "total_return_pct": _ret_v, - "portfolio_max_dd_pct": _dd_v, "capital_final": _cf_v, - "positions_taken": _pt_v, "sharpe_ratio": _sh_v, - "profit_factor": _pf_v}, - "universality": _approx_univ(_ver, _reg, len(_reg_trades)), - } except Exception: pass - if _cache_key: - try: - (_regime_winrates_cache.setdefault("slots", {}))[pt or "2y"] = (_cache_key, _regime_winrates) - except Exception: - pass return _regime_winrates - def _chk_http(host, port, path, timeout=3): try: url = f"http://{host}:{port}{path}"