#!/usr/bin/env python3 """V14 experiment runner: bar close confirmation entry + V12 validation. Based on V11 champion. Tests: G. Bar close confirmation entry (entry_on_bar_close) — require breakout bar's CLOSE above ORB level, not just HIGH touching it. Filters wick-only false breakouts. H. Combinations with score_sizing_multiplier (the only confirmed V13 improvement) Key insight from V13: ~7 false breakout trades (wick-only ORB high touches) cause ~$4,900 in losses. The biased confirmation bar experiment showed +77% by retroactively canceling these. entry_on_bar_close achieves similar filtering without lookahead — trader waits for the 5-min bar to close, enters at the close price. Usage: python -u scripts/v14_experiments.py [--days N] """ import subprocess import sys import tempfile import yaml from pathlib import Path BASE_CONFIG = "configs/intraday/strategies/orb_gainers_v11.yaml" DAYS = 200 def load_base(): with open(BASE_CONFIG) as f: return yaml.safe_load(f) def run_experiment(name: str, overrides: dict, days: int = DAYS) -> dict | None: """Run a single backtest with config overrides and return parsed metrics.""" cfg = load_base() for key, val in overrides.items(): cfg["orb_strategy"][key] = val with tempfile.NamedTemporaryFile( mode="w", suffix=".yaml", prefix="v14_", delete=False, dir="/tmp" ) as f: yaml.dump(cfg, f, default_flow_style=False) tmp_path = f.name cmd = [ sys.executable, "-u", "-m", "apps.intraday_bt.run", "--config", tmp_path, "--days", str(days), ] print(f"\n{'='*70}") print(f" {name}") print(f" Overrides: {overrides or '(V11 baseline)'}") print(f"{'='*70}", flush=True) try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=900) except subprocess.TimeoutExpired: print(f" TIMEOUT after 900s") return None finally: Path(tmp_path).unlink(missing_ok=True) if result.returncode != 0: print(f" FAILED (rc={result.returncode})") if result.stderr: print(f" stderr: {result.stderr[-500:]}") return None output = result.stdout metrics = {} for line in output.split("\n"): line = line.strip() if "Total return" in line and "%" in line: try: val = line.split("│")[-2].strip().replace("%", "").replace("+", "") metrics["total_return"] = float(val) except (ValueError, IndexError): pass elif "Max drawdown" in line and "%" in line: try: val = line.split("│")[-2].strip().replace("%", "").replace("+", "") metrics["max_drawdown"] = float(val) except (ValueError, IndexError): pass elif "Sharpe ratio" in line: try: val = line.split("│")[-2].strip() metrics["sharpe"] = float(val) except (ValueError, IndexError): pass elif "Total trades" in line: try: val = line.split("│")[-2].strip() metrics["trades"] = int(val) except (ValueError, IndexError): pass elif "Win rate" in line and "%" in line: try: val = line.split("│")[-2].strip().replace("%", "").replace("+", "") metrics["win_rate"] = float(val) except (ValueError, IndexError): pass elif "Profit factor" in line: try: val = line.split("│")[-2].strip() metrics["profit_factor"] = float(val) except (ValueError, IndexError): pass if not metrics: print(" WARNING: Could not parse metrics from output") for line in output.split("\n")[-30:]: print(f" {line}") return None print(f" => return={metrics.get('total_return', '?'):+.2f}%, " f"DD={metrics.get('max_drawdown', '?'):.2f}%, " f"Sharpe={metrics.get('sharpe', '?'):.2f}, " f"trades={metrics.get('trades', '?')}, " f"WR={metrics.get('win_rate', '?'):.1f}%, " f"PF={metrics.get('profit_factor', '?'):.3f}", flush=True) return metrics def main(): days = DAYS if "--days" in sys.argv: idx = sys.argv.index("--days") days = int(sys.argv[idx + 1]) results = {} # V11 baseline (no overrides) results["V11_baseline"] = run_experiment("V11 Baseline (control)", {}, days) # ═══════════════════════════════════════════════════════════════════════ # G. Bar close confirmation entry — wick filter # ═══════════════════════════════════════════════════════════════════════ results["V14g1_bar_close"] = run_experiment( "V14g1: Bar close entry (require close above breakout level)", {"entry_on_bar_close": True}, days, ) # ═══════════════════════════════════════════════════════════════════════ # H. Combinations with confirmed improvements # ═══════════════════════════════════════════════════════════════════════ # H1: Bar close + score sizing 1.5x (V12 candidate) results["V14h1_close_score1.5"] = run_experiment( "V14h1: Bar close + score sizing 1.5x (V12 candidate)", {"entry_on_bar_close": True, "score_sizing_multiplier": 1.5}, days, ) # H2: Bar close + score sizing 2.0x results["V14h2_close_score2.0"] = run_experiment( "V14h2: Bar close + score sizing 2.0x", {"entry_on_bar_close": True, "score_sizing_multiplier": 2.0}, days, ) # H3: Score sizing 1.5x alone (V13 confirmed, re-validate) results["V14h3_score1.5_only"] = run_experiment( "V14h3: Score sizing 1.5x only (V13 confirmed, re-validate)", {"score_sizing_multiplier": 1.5}, days, ) # ═══════════════════════════════════════════════════════════════════════ # Summary table # ═══════════════════════════════════════════════════════════════════════ print(f"\n\n{'='*95}") print(" V14 EXPERIMENT RESULTS SUMMARY (base: V11 champion)") print(f"{'='*95}") print(f"{'Experiment':<30} {'Return':>9} {'DD':>9} {'Sharpe':>7} {'Trades':>7} {'WR':>7} {'PF':>7}") print(f"{'-'*30} {'-'*9} {'-'*9} {'-'*7} {'-'*7} {'-'*7} {'-'*7}") baseline_ret = (results.get("V11_baseline") or {}).get("total_return", 0) for name, m in results.items(): if m is None: print(f"{name:<30} {'FAILED':>9}") continue ret = m.get("total_return", 0) delta = ret - baseline_ret ret_str = f"{ret:+.2f}%" dd = f"{m.get('max_drawdown', 0):.2f}%" sh = f"{m.get('sharpe', 0):.2f}" tr = f"{m.get('trades', 0)}" wr = f"{m.get('win_rate', 0):.1f}%" pf = f"{m.get('profit_factor', 0):.3f}" marker = " <== BASE" if name == "V11_baseline" else (f" ({delta:+.2f}pp)" if delta != 0 else "") print(f"{name:<30} {ret_str:>9} {dd:>9} {sh:>7} {tr:>7} {wr:>7} {pf:>7}{marker}") if __name__ == "__main__": main()