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.

308 lines
13 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/usr/bin/env python3
"""V13 experiment runner: new engine features for exit timing and candidate selection.
Based on V11 champion. Tests:
A. VWAP trailing exit (exit/floor modes with buffer/activation variants)
B. Score-based position sizing (top picks get larger positions)
C. Confirmation bar (filter false breakouts)
D. Gap fill protection (exit when gap is fully filled)
E. Max hold time (morning momentum capture, exit before afternoon fade)
F. Combinations of winners
Usage:
python -u scripts/v13_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="v13_", 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)
# ═══════════════════════════════════════════════════════════════════════
# A. VWAP Exit — running VWAP as dynamic support/resistance
# ════════════════════════════════<E29590><E29590>══════════════════════════════════════
# A1: Simple VWAP cross exit (exit when close < VWAP)
results["V13a1_vwap_exit"] = run_experiment(
"V13a1: VWAP exit (close < VWAP → exit)",
{"vwap_exit_mode": "exit"}, days,
)
# A2: VWAP exit with buffer (close < VWAP - 0.3ATR → exit)
results["V13a2_vwap_buf03"] = run_experiment(
"V13a2: VWAP exit with 0.3ATR buffer",
{"vwap_exit_mode": "exit", "vwap_exit_buffer_atr": 0.3}, days,
)
# A3: VWAP exit only after 1R reached (avoid premature exit)
results["V13a3_vwap_after1r"] = run_experiment(
"V13a3: VWAP exit after reaching 1R",
{"vwap_exit_mode": "exit", "vwap_exit_after_r": 1.0}, days,
)
# A4: VWAP exit after 1R with buffer
results["V13a4_vwap_1r_buf"] = run_experiment(
"V13a4: VWAP exit after 1R + 0.3ATR buffer",
{"vwap_exit_mode": "exit", "vwap_exit_after_r": 1.0, "vwap_exit_buffer_atr": 0.3},
days,
)
# A5: VWAP as trailing stop floor (VWAP raises the trailing stop minimum)
results["V13a5_vwap_floor"] = run_experiment(
"V13a5: VWAP floor (trailing stop can't go below VWAP)",
{"vwap_exit_mode": "floor"}, days,
)
# A6: VWAP floor with buffer
results["V13a6_vwap_floor_buf"] = run_experiment(
"V13a6: VWAP floor with 0.3ATR buffer",
{"vwap_exit_mode": "floor", "vwap_exit_buffer_atr": 0.3}, days,
)
# ════════<E29590><E29590><EFBFBD>═══════════════════════════════════════════════════<E29590><E29590>══════════
# B. Score-Based Position Sizing — top picks get larger positions
# ═════════════<E29590><E29590>═══════════════════════════════════<E29590><E29590>═════════════════════
results["V13b1_score_1.5x"] = run_experiment(
"V13b1: Score sizing 1.5x (top pick = 1.5× base risk)",
{"score_sizing_multiplier": 1.5}, days,
)
results["V13b2_score_2.0x"] = run_experiment(
"V13b2: Score sizing 2.0x (top pick = 2× base risk)",
{"score_sizing_multiplier": 2.0}, days,
)
results["V13b3_score_3.0x"] = run_experiment(
"V13b3: Score sizing 3.0x (top pick = 3× base risk)",
{"score_sizing_multiplier": 3.0}, days,
)
# ════<E29590><E29590><EFBFBD>════════════<E29590><E29590>══════════════════════════════<E29590><E29590>══════════════════════
# C. Confirmation Bar — require follow-through after breakout
# ═════════════════<E29590><E29590><EFBFBD>═════════════════════════════════════════════════════
results["V13c_confirm"] = run_experiment(
"V13c: Require confirmation bar (next bar must close above entry)",
{"require_confirmation_bar": True}, days,
)
# ═<><E29590><EFBFBD>════════════<E29590><E29590>════════════════════════════════════════════════════════
# D. Gap Fill Protection — exit when catalyst is rejected
# ════<E29590><E29590>════════════════<E29590><E29590><EFBFBD>═════════════════════════════════════════════════
results["V13d_gap_fill"] = run_experiment(
"V13d: Gap fill exit (close < prev_close → exit immediately)",
{"exit_on_gap_fill": True}, days,
)
# ══════════════════════════<E29590><E29590>════════════════════════════════════════════
# E. Max Hold Time — capture morning momentum, avoid afternoon fade
# ══════════════<E29590><E29590><EFBFBD>════════════════════════════════<E29590><E29590>═══════════════════════
results["V13e1_hold_60"] = run_experiment(
"V13e1: Max hold 60min (1 hour)",
{"max_hold_minutes": 60}, days,
)
results["V13e2_hold_90"] = run_experiment(
"V13e2: Max hold 90min (1.5 hours)",
{"max_hold_minutes": 90}, days,
)
results["V13e3_hold_120"] = run_experiment(
"V13e3: Max hold 120min (2 hours)",
{"max_hold_minutes": 120}, days,
)
results["V13e4_hold_180"] = run_experiment(
"V13e4: Max hold 180min (3 hours)",
{"max_hold_minutes": 180}, days,
)
# ═══════════════════════════════════════════════════════════════<E29590><E29590><EFBFBD>═══════
# F. Combinations — combine promising features
# ══════════════════<E29590><E29590><EFBFBD>═══════════════════════════<E29590><E29590><EFBFBD>════════════════════════
# F1: VWAP exit + score sizing
results["V13f1_vwap_score"] = run_experiment(
"V13f1: VWAP exit after 1R + score sizing 2x",
{"vwap_exit_mode": "exit", "vwap_exit_after_r": 1.0, "score_sizing_multiplier": 2.0},
days,
)
# F2: VWAP exit + confirmation bar
results["V13f2_vwap_confirm"] = run_experiment(
"V13f2: VWAP exit after 1R + confirmation bar",
{"vwap_exit_mode": "exit", "vwap_exit_after_r": 1.0, "require_confirmation_bar": True},
days,
)
# F3: VWAP exit + gap fill + max hold 120
results["V13f3_vwap_gap_hold"] = run_experiment(
"V13f3: VWAP exit after 1R + gap fill + max hold 120",
{
"vwap_exit_mode": "exit", "vwap_exit_after_r": 1.0,
"exit_on_gap_fill": True, "max_hold_minutes": 120,
},
days,
)
# F4: Score sizing + confirmation + gap fill
results["V13f4_score_confirm_gap"] = run_experiment(
"V13f4: Score 2x + confirmation + gap fill",
{"score_sizing_multiplier": 2.0, "require_confirmation_bar": True, "exit_on_gap_fill": True},
days,
)
# F5: Kitchen sink — all features
results["V13f5_kitchen_sink"] = run_experiment(
"V13f5: All features combined",
{
"vwap_exit_mode": "exit", "vwap_exit_after_r": 1.0, "vwap_exit_buffer_atr": 0.3,
"score_sizing_multiplier": 2.0,
"require_confirmation_bar": True,
"exit_on_gap_fill": True,
"max_hold_minutes": 120,
},
days,
)
# <20><>══════════════════════════════════════════════════════════════════════
# Summary table
# ════════════<E29590><E29590><EFBFBD>═══════════════════<E29590><E29590><EFBFBD>══════════════════════════════════════
print(f"\n\n{'='*95}")
print(" V13 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()