#!/usr/bin/env python3 """In-harness monkey baseline for the Clenow weekly recipe. Holds the ENTIRE harness constant (regime gate, >SMA100 / slope>0 / gap filters, ATR inverse-dollar sizing, weekly cadence, exit logic) and replaces ONLY the slope×R² selection step with a seeded random pick among the same eligible candidates. Two monkey distributions per universe: - gated : random selection, regime gate ON → isolates the ranking step (compare vs the real GATED system). - ungated : random selection, regime gate OFF → always-invested baseline (tests the whole system, gate + ranking, vs always-invested random — matches the companion monthly-recipe test). K=200 seeds each. Reports the real system's percentile in each distribution. Usage: python monkey_baseline.py --index QQQ_Nasdaq-100 --regime-etf QQQ python monkey_baseline.py --index SPY_SandP_500 --regime-etf SPY """ import argparse import random from datetime import date import numpy as np from prototype import prepare_run, simulate_portfolio, compute_metrics def sharpe_from_curve(equity_curve) -> float: """Annualized Sharpe from daily equity returns (rf=0, 252d).""" e = np.array([row[1] for row in equity_curve], dtype=float) rets = np.diff(e) / e[:-1] sd = rets.std(ddof=1) if sd == 0: return 0.0 return float(rets.mean() / sd * np.sqrt(252)) def rolling_calmar_median(equity_curve, win: int = 756, step: int = 21) -> float: """Median of the rolling 36-month Calmar ratio. At each step (every ~month), over the trailing ~36 months (756 trading days): annualized return / |max drawdown| within that window. The series' median is a turbulence-robust read on return-per-unit-drawdown that, unlike a single full-period Calmar, does not hinge on one historic drawdown or the endpoints. """ e = np.asarray([row[1] for row in equity_curve], dtype=float) if len(e) < win + 1: return float("nan") vals = [] for i in range(win, len(e), step): w = e[i - win:i + 1] ann_ret = (w[-1] / w[0]) ** (252.0 / win) - 1.0 peak = np.maximum.accumulate(w) dd = float(((w - peak) / peak).min()) if dd < -1e-6: vals.append(ann_ret / abs(dd)) return float(np.median(vals)) if vals else float("nan") def metrics_of(result) -> dict: m = compute_metrics(result) return dict( cagr=m["cagr"], max_dd=m["max_drawdown"], sharpe=sharpe_from_curve(result["equity_curve"]), calmar36=rolling_calmar_median(result["equity_curve"]), final=result["final_equity"], ) def pct_rank(value: float, dist: list[float]) -> float: """Percentile of `value` within `dist` (fraction of dist strictly below).""" n = len(dist) below = sum(1 for x in dist if x < value) return 100.0 * below / n def pctiles(dist: list[float]): a = np.array(dist, dtype=float) return {p: float(np.percentile(a, p)) for p in (5, 25, 50, 75, 95)} def run_monkeys(prep, gate: bool, k: int) -> list[dict]: out = [] for seed in range(k): res = simulate_portfolio( prep, rank_mode="random", rng=random.Random(seed), gate=gate, verbose=False, ) out.append(metrics_of(res)) if (seed + 1) % 25 == 0: print(f" {seed+1}/{k} seeds", flush=True) return out def report(label: str, real: dict, monk: list[dict]): print(f"\n=== {label} monkey (K={len(monk)}) ===") for metric, real_v, fmt, higher_better in [ ("cagr", real["cagr"], "{:+.2%}", True), ("sharpe", real["sharpe"], "{:.2f}", True), ("max_dd", real["max_dd"], "{:.2%}", True), # less negative = better; higher value better ]: dist = [m[metric] for m in monk] pc = pctiles(dist) rp = pct_rank(real_v, dist) line = " ".join(f"p{p}={fmt.format(v)}" for p, v in pc.items()) print(f" {metric:>7}: real={fmt.format(real_v)} [{line}] real_pctile={rp:.1f}") real_final = real["final"] finals = [m["final"] for m in monk] print(f" final$: real=${real_final:,.0f} median_monkey=${np.median(finals):,.0f}") # skill excess CAGR vs monkey median med_cagr = float(np.median([m["cagr"] for m in monk])) print(f" skill excess CAGR (real - monkey_median): {(real['cagr']-med_cagr)*100:+.2f} pp/yr") def main(): ap = argparse.ArgumentParser() ap.add_argument("--index", default="QQQ_Nasdaq-100") ap.add_argument("--regime-etf", default="QQQ") ap.add_argument("--start", default="2005-01-01") ap.add_argument("--end", default="2026-05-15") ap.add_argument("--k", type=int, default=200) args = ap.parse_args() start = date.fromisoformat(args.start) end = date.fromisoformat(args.end) print(f"Preparing {args.index} (gate ETF {args.regime_etf})...") prep = prepare_run(args.index, start, end, args.regime_etf, verbose=True) print("\nRunning REAL system (gated, clenow ranking)...") real = metrics_of(simulate_portfolio(prep, rank_mode="clenow", gate=True, verbose=False)) print(f" real: CAGR {real['cagr']:+.2%} MaxDD {real['max_dd']:.2%} " f"Sharpe {real['sharpe']:.2f} final ${real['final']:,.0f}") print(f"\nRunning {args.k} GATED monkey seeds...") gated = run_monkeys(prep, gate=True, k=args.k) print(f"\nRunning {args.k} UN-GATED monkey seeds...") ungated = run_monkeys(prep, gate=False, k=args.k) report(f"{args.index} GATED", real, gated) report(f"{args.index} UN-GATED", real, ungated) if __name__ == "__main__": main()