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.
fithia2/_tune_v7356_sweep.py

129 lines
4.9 KiB
Python

"""Sweep v7.356 parameters to reduce MDD and improve SQS.
Base: v7.356_composed_gld_compound. SQS risk component=50, MDD=14.5% — bottleneck.
Sweep strategy:
- Phase 1: single-param sweeps (max_pos_value, risk_off_scaler, stop_atr)
- Phase 2: top-2 from phase 1 combined
"""
import json
import subprocess
import shutil
from pathlib import Path
BASE = "configs/experiments/return_max_long_v7.356_composed_gld_compound.json"
SWEEP_DIR = Path("configs/experiments/_tune_v7356_sweep")
SWEEP_DIR.mkdir(exist_ok=True)
SNAPSHOT = "midlarge-liquid-long-v1_bucketfix_full_audit_canonical_ftb_fix_v2"
def make_config(name: str, risk_overrides: dict, exec_overrides: dict = None) -> str:
with open(BASE) as f:
cfg = json.load(f)
cfg["experiment_name"] = name
cfg["overrides"]["risk"].update(risk_overrides)
if exec_overrides:
cfg["overrides"]["execution"].update(exec_overrides)
path = SWEEP_DIR / f"{name}.json"
path.write_text(json.dumps(cfg, indent=2))
return str(path)
def run_backtest(config_path: str) -> dict:
cmd = [
"python3", "-m", "apps.backtester.run",
"--manifest", config_path,
"--start", "2022-01-01",
"--split", "all",
"--initial-equity", "10000",
"--snapshot-id", SNAPSHOT,
]
# Clean runtime cache to ensure fresh load
cache = Path(f"data/parquet/{SNAPSHOT}/.runtime_cache")
if cache.exists():
shutil.rmtree(cache)
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
tail = proc.stdout.split("\n")[-10:]
run_id = None
return_pct = None
sqs_total = None
sqs_parts = None
trades = None
for line in tail:
if "Run complete:" in line:
run_id = line.split("Run complete:")[1].strip()
elif "Total return:" in line:
return_pct = float(line.split(":")[1].strip().rstrip("%"))
elif "SQS:" in line:
parts = line.split("SQS:")[1].strip()
sqs_total = float(parts.split(" ")[0])
sqs_parts = parts
elif "Trades:" in line:
trades = int(line.split(":")[1].strip())
# Get MDD from metrics
mdd = sharpe = pf = None
if run_id:
m_path = Path(f"runs/{run_id}/metrics/metrics_summary.json")
if m_path.exists():
m = json.loads(m_path.read_text())
mdd = m["max_drawdown_pct"]
sharpe = m["sharpe_ratio"]
pf = m["profit_factor"]
return {
"return": return_pct,
"trades": trades,
"mdd": mdd,
"sharpe": sharpe,
"pf": pf,
"sqs": sqs_total,
"sqs_parts": sqs_parts,
"run_id": run_id,
}
SWEEPS = [
# (name, risk overrides, execution overrides)
("v7.356_T01_pos20", {"max_position_value_pct": 20}, None),
("v7.356_T02_pos15", {"max_position_value_pct": 15}, None),
("v7.356_T03_ro_07", {"macro_regime_risk_off_size_scaler": 0.7}, None),
("v7.356_T04_ro_05", {"macro_regime_risk_off_size_scaler": 0.5}, None),
("v7.356_T05_stop_25", {"stop_atr_multiplier": 2.5}, None),
("v7.356_T06_stop_35", {"stop_atr_multiplier": 3.5}, None),
("v7.356_T07_sec3", {"max_positions_per_sector": 3}, None),
("v7.356_T08_sec4", {"max_positions_per_sector": 4}, None),
]
def main():
baseline = {"return": 2236.1, "mdd": 14.50, "sharpe": 2.72, "pf": 4.38, "sqs": 87.1, "trades": 293}
print(f"BASELINE: ret={baseline['return']}% MDD={baseline['mdd']}% Sharpe={baseline['sharpe']} SQS={baseline['sqs']} trades={baseline['trades']}")
print("=" * 100)
results = []
for name, risk_ov, exec_ov in SWEEPS:
path = make_config(name, risk_ov, exec_ov)
print(f"\n>>> {name}: risk={risk_ov} exec={exec_ov}")
try:
r = run_backtest(path)
r["name"] = name
d_ret = r["return"] - baseline["return"] if r["return"] else None
d_mdd = r["mdd"] - baseline["mdd"] if r["mdd"] else None
d_sqs = r["sqs"] - baseline["sqs"] if r["sqs"] else None
print(f" ret={r['return']:.0f}% (Δ{d_ret:+.0f}) MDD={r['mdd']:.2f}% (Δ{d_mdd:+.2f}) Sharpe={r['sharpe']:.2f} PF={r['pf']:.2f} SQS={r['sqs']:.1f}{d_sqs:+.1f}) trades={r['trades']}")
results.append(r)
except Exception as e:
print(f" FAILED: {e}")
# Print summary table
print("\n" + "=" * 100)
print("SUMMARY (sorted by SQS):")
results.sort(key=lambda x: x.get("sqs") or 0, reverse=True)
print(f"{'name':30s} {'return':>10s} {'MDD':>8s} {'Sharpe':>8s} {'PF':>6s} {'SQS':>6s} {'trades':>7s}")
print(f"{'BASELINE':30s} {baseline['return']:>9.0f}% {baseline['mdd']:>7.2f}% {baseline['sharpe']:>8.2f} {baseline['pf']:>6.2f} {baseline['sqs']:>6.1f} {baseline['trades']:>7d}")
for r in results:
print(f"{r['name']:30s} {r['return']:>9.0f}% {r['mdd']:>7.2f}% {r['sharpe']:>8.2f} {r['pf']:>6.2f} {r['sqs']:>6.1f} {r['trades']:>7d}")
if __name__ == "__main__":
main()