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
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())
|