refactor: 温区数据改读预计算表(strategy_regime_perf_by_period)——删除现场重算+内存缓存,数据加工层每日预计算,server只读表毫秒级
This commit is contained in:
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user