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.
111 lines
5.2 KiB
Python
111 lines
5.2 KiB
Python
"""Phase 3: engine-level + portfolio-level levers."""
|
|
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_ov=None, engine_scale=None, signal_ov=None, sleeve_change=None) -> str:
|
|
with open(BASE) as f:
|
|
cfg = json.load(f)
|
|
cfg["experiment_name"] = name
|
|
if risk_ov:
|
|
cfg["overrides"]["risk"].update(risk_ov)
|
|
if signal_ov:
|
|
cfg["overrides"]["signal"].update(signal_ov)
|
|
if engine_scale:
|
|
# Scale each engine's risk_budget_pct
|
|
for eng in cfg.get("strategy_engines", []):
|
|
if "engine_risk_budget_pct" in eng:
|
|
eng["engine_risk_budget_pct"] *= engine_scale
|
|
if sleeve_change:
|
|
for k, v in sleeve_change.items():
|
|
cfg["overrides"][k] = v
|
|
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]
|
|
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 = return_pct = sqs_total = 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:
|
|
sqs_total = float(line.split("SQS:")[1].strip().split(" ")[0])
|
|
elif "Trades:" in line:
|
|
trades = int(line.split(":")[1].strip())
|
|
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, "run_id": run_id}
|
|
|
|
|
|
SWEEPS = [
|
|
# Engine risk_budget scaling (top-N from prior session's analysis)
|
|
("T30_engine_x07", None, 0.7, None, None), # 30% less risk per engine
|
|
("T31_engine_x05", None, 0.5, None, None), # half risk per engine
|
|
("T32_engine_x13", None, 1.3, None, None), # 30% more risk per engine
|
|
# Macro risk-off A-tier only (more selective in bad regimes)
|
|
("T33_atier_only", {"macro_regime_risk_off_a_tier_only": True}, None, None, None),
|
|
("T34_atier_05x", {"macro_regime_risk_off_a_tier_only": True, "macro_regime_risk_off_size_scaler": 0.5}, None, None, None),
|
|
# Sleeve preset changes (parking variations)
|
|
("T35_parking_qqqm_low_dd", None, None, None, {"risk": {"cash_parking_preset": "qqqm_low_dd"}}),
|
|
# Combo: best from each axis
|
|
("T40_combo_engine07_ro05", {"macro_regime_risk_off_size_scaler": 0.5}, 0.7, None, None),
|
|
("T41_combo_engine07_atier", {"macro_regime_risk_off_a_tier_only": True, "macro_regime_risk_off_size_scaler": 0.5}, 0.7, None, None),
|
|
("T42_combo_engine05_ro05", {"macro_regime_risk_off_size_scaler": 0.5}, 0.5, None, 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("=" * 110)
|
|
results = []
|
|
for name, risk_ov, eng_sc, sig_ov, sleeve in SWEEPS:
|
|
path = make_config(name, risk_ov, eng_sc, sig_ov, sleeve)
|
|
print(f"\n>>> {name}: risk={risk_ov} engine_x={eng_sc} signal={sig_ov} sleeve={sleeve}")
|
|
try:
|
|
r = run_backtest(path)
|
|
r["name"] = name
|
|
d_ret = (r["return"] or 0) - baseline["return"]
|
|
d_mdd = (r["mdd"] or 0) - baseline["mdd"]
|
|
d_sqs = (r["sqs"] or 0) - baseline["sqs"]
|
|
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("\n" + "=" * 110)
|
|
print("SUMMARY (sorted by SQS, then return):")
|
|
results.sort(key=lambda x: (x.get("sqs") or 0, x.get("return") or 0), reverse=True)
|
|
print(f"{'name':22s} {'return':>10s} {'MDD':>8s} {'Sharpe':>8s} {'PF':>6s} {'SQS':>7s} {'trades':>7s}")
|
|
print(f"{'BASELINE':22s} {baseline['return']:>9.0f}% {baseline['mdd']:>7.2f}% {baseline['sharpe']:>8.2f} {baseline['pf']:>6.2f} {baseline['sqs']:>7.1f} {baseline['trades']:>7d}")
|
|
for r in results:
|
|
print(f"{r['name']:22s} {r['return']:>9.0f}% {r['mdd']:>7.2f}% {r['sharpe']:>8.2f} {r['pf']:>6.2f} {r['sqs']:>7.1f} {r['trades']:>7d}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|