You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

107 lines
3.2 KiB
Python

"""OOS slice metrics for v15/v18/v19 candidate variants.
Splits the 119d window into:
TRAIN: 2025-11-11 .. 2025-12-31 (~36 td)
TEST: 2026-01-01 .. 2026-05-04 (~83 td)
Computes return, MaxDD, Sharpe, WR, PF on each slice.
A variant that holds up in TEST as well as in TRAIN is the most likely to be real.
"""
from __future__ import annotations
import json
import math
import sys
from pathlib import Path
def _load(path: str) -> list[dict]:
with open(path) as f:
return json.load(f)["daily_results"]
def _max_dd(equity: list[float]) -> float:
peak = equity[0]
worst = 0.0
for eq in equity:
if eq > peak:
peak = eq
if peak > 0:
dd = (eq - peak) / peak
if dd < worst:
worst = dd
return worst
def _slice_metrics(label: str, daily: list[dict], date_pred) -> dict:
sliced = [d for d in daily if date_pred(d["date"])]
if not sliced:
return {"label": label, "n_days": 0}
initial = 10_000.0
eq = [initial]
for d in sliced:
eq.append(eq[-1] + d["day_pnl"])
final = eq[-1]
ret = (final - initial) / initial
dd = _max_dd(eq)
daily_ret = [d["day_pnl"] / initial for d in sliced]
n = len(daily_ret)
mean = sum(daily_ret) / n
var = sum((r - mean) ** 2 for r in daily_ret) / n if n > 1 else 0
sd = math.sqrt(var)
sharpe = (mean / sd) * math.sqrt(252) if sd > 0 else 0
all_trades = [t for d in sliced for t in d["trades"]]
wins = [t for t in all_trades if t["pnl"] > 0]
losses = [t for t in all_trades if t["pnl"] <= 0]
wr = len(wins) / len(all_trades) if all_trades else 0
gross_w = sum(t["pnl"] for t in wins)
gross_l = -sum(t["pnl"] for t in losses)
pf = gross_w / gross_l if gross_l > 0 else float("inf") if gross_w > 0 else 0
return {
"label": label,
"n_days": n,
"return": ret,
"max_dd": dd,
"sharpe": sharpe,
"wr": wr,
"pf": pf,
"n_trades": len(all_trades),
}
def main() -> int:
runs_dir = Path("runs/intraday/topgainer_holdtoclose_v1")
candidates = [
("v1_recheck", "v1 baseline"),
("v7_strict_entry", "v7 strict (top5/3%/$10M)"),
("v15_strict_top10", "v15 strict+top10"),
("v18_top10_pmtight", "v18 v15+PM tighten"),
("v19_top15", "v19 strict+top15"),
]
train_pred = lambda d: d <= "2025-12-31"
test_pred = lambda d: d >= "2026-01-01"
print(f"\n{'variant':<28} {'slice':<8} {'days':>5} {'return':>8} {'DD':>8} {'Sharpe':>7} {'WR':>6} {'PF':>5} {'trades':>6}")
print("-" * 90)
for fname, label in candidates:
path = runs_dir / f"{fname}.json"
if not path.exists():
continue
daily = _load(str(path))
for slice_label, pred in [("FULL", lambda d: True), ("TRAIN", train_pred), ("TEST", test_pred)]:
m = _slice_metrics(slice_label, daily, pred)
if m.get("n_days", 0) == 0:
continue
print(f"{label:<28} {slice_label:<8} {m['n_days']:>5} {m['return']:>+8.4f} {m['max_dd']:>+8.4f} {m['sharpe']:>7.2f} {m['wr']:>6.3f} {m['pf']:>5.2f} {m['n_trades']:>6}")
print()
return 0
if __name__ == "__main__":
raise SystemExit(main())