201 lines
7.8 KiB
Python
201 lines
7.8 KiB
Python
#!/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")
|
||
# 复利年化(2026-08-16 修正):按该温区 trades 实际时间跨度
|
||
# 原线性放大 ret×(total_days/rd) 对高频复利策略失真(b_td1 388%×4=1547%)
|
||
_eds = [t.get("entry_date") for t in reg_trades if t.get("entry_date")]
|
||
if ret is not None and _eds:
|
||
from datetime import datetime as _dt
|
||
_d0 = _dt.strptime(min(_eds), "%Y-%m-%d")
|
||
_d1 = _dt.strptime(max(_eds), "%Y-%m-%d")
|
||
span_days = max((_d1 - _d0).days, 30)
|
||
cagr = round(((1 + ret / 100) ** (365 / span_days) - 1) * 100, 1)
|
||
else:
|
||
cagr = 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()
|