refactor: 温区数据改读预计算表(strategy_regime_perf_by_period)——删除现场重算+内存缓存,数据加工层每日预计算,server只读表毫秒级

This commit is contained in:
xxm
2026-08-15 22:44:38 +08:00
parent 772fcc3ac9
commit fa267f2e7b
+31 -138
View File
@@ -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_period2026-08-15 数据加工层)
数据由 regime_perf_by_period.py 每日盘后预计算(含线性年化),server 只读表,毫秒级。
无对应周期记录时回退到最近更长周期(1m/6m→1y2y→2y5y→5y10y→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}"