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.
216 lines
7.4 KiB
Python
216 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""V11 experiment runner: structural improvements over V10 champion.
|
|
|
|
Tests VIX regime filter, entropy scoring, and atr_ratio scoring for gainers_leader.
|
|
All experiments use V10 (orb_gainers_v10.yaml) as baseline.
|
|
|
|
Usage:
|
|
python -u scripts/v11_experiments.py [--days N]
|
|
"""
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import yaml
|
|
from pathlib import Path
|
|
|
|
BASE_CONFIG = "configs/intraday/strategies/orb_gainers_v10.yaml"
|
|
DAYS = 400
|
|
|
|
|
|
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
|
|
|
|
# Write temp config
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".yaml", prefix="v11_", 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 '(baseline)'}")
|
|
print(f"{'='*70}", flush=True)
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
|
except subprocess.TimeoutExpired:
|
|
print(f" TIMEOUT after 600s")
|
|
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 = {}
|
|
|
|
# V10 baseline (no overrides)
|
|
results["V10_baseline"] = run_experiment("V10 Baseline (control)", {}, days)
|
|
|
|
# === VIX experiments ===
|
|
results["V11a_max_vix_30"] = run_experiment(
|
|
"V11a: max_vix=30 (skip VIX>30 days)", {"max_vix": 30}, days
|
|
)
|
|
results["V11a2_max_vix_25"] = run_experiment(
|
|
"V11a2: max_vix=25 (more aggressive)", {"max_vix": 25}, days
|
|
)
|
|
results["V11b_vix_scale"] = run_experiment(
|
|
"V11b: VIX size scaling 15→30, min=0.5",
|
|
{"vix_size_scale_low": 15, "vix_size_scale_high": 30, "vix_size_scale_min": 0.5},
|
|
days,
|
|
)
|
|
results["V11b2_vix_scale_wide"] = run_experiment(
|
|
"V11b2: VIX size scaling 20→35, min=0.5",
|
|
{"vix_size_scale_low": 20, "vix_size_scale_high": 35, "vix_size_scale_min": 0.5},
|
|
days,
|
|
)
|
|
|
|
# === Entropy experiments (now unlocked for gainers_leader) ===
|
|
results["V11c_entropy_neg10"] = run_experiment(
|
|
"V11c: weight_entropy=-0.10 (prefer lower entropy)",
|
|
{"weight_entropy": -0.10}, days,
|
|
)
|
|
results["V11c2_entropy_neg05"] = run_experiment(
|
|
"V11c2: weight_entropy=-0.05",
|
|
{"weight_entropy": -0.05}, days,
|
|
)
|
|
results["V11c3_entropy_pos10"] = run_experiment(
|
|
"V11c3: weight_entropy=+0.10 (prefer higher entropy)",
|
|
{"weight_entropy": 0.10}, days,
|
|
)
|
|
|
|
# === ATR ratio experiments (now unlocked for gainers_leader) ===
|
|
results["V11d_atr_ratio_pos10"] = run_experiment(
|
|
"V11d: weight_atr_ratio=+0.10 (prefer expanding vol)",
|
|
{"weight_atr_ratio": 0.10}, days,
|
|
)
|
|
results["V11d2_atr_ratio_neg10"] = run_experiment(
|
|
"V11d2: weight_atr_ratio=-0.10 (prefer compressed vol)",
|
|
{"weight_atr_ratio": -0.10}, days,
|
|
)
|
|
|
|
# === Combinations ===
|
|
results["V11e_vix30_entropy"] = run_experiment(
|
|
"V11e: max_vix=30 + entropy=-0.10",
|
|
{"max_vix": 30, "weight_entropy": -0.10}, days,
|
|
)
|
|
results["V11f_vix30_atr_ratio"] = run_experiment(
|
|
"V11f: max_vix=30 + atr_ratio=+0.10",
|
|
{"max_vix": 30, "weight_atr_ratio": 0.10}, days,
|
|
)
|
|
results["V11g_combined"] = run_experiment(
|
|
"V11g: VIX scale 20→35 + entropy=-0.05 + atr_ratio=+0.10",
|
|
{
|
|
"vix_size_scale_low": 20, "vix_size_scale_high": 35, "vix_size_scale_min": 0.5,
|
|
"weight_entropy": -0.05, "weight_atr_ratio": 0.10,
|
|
},
|
|
days,
|
|
)
|
|
|
|
# === Summary table ===
|
|
print(f"\n\n{'='*90}")
|
|
print(" V11 EXPERIMENT RESULTS SUMMARY")
|
|
print(f"{'='*90}")
|
|
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("V10_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 == "V10_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()
|