Self-tuning loop: per-setup report + nightly re-sweep + live learned-params

(a) learn_params.per_setup_report: permanent per-setup P&L/earliness breakdown
    in the harness (forces all-setups so the ORB-vs-rest comparison is visible).
(b) nightly_tune.py + nightly_tune.sh: backfill latest bars, sweep ORB params
    over accumulated gapper sessions, write the best PROFITABLE config to
    data/learned_params.json. run_live_paper reads it on boot and applies the
    tuned ready_score / min_bars (explicit env still wins); won't overwrite with
    a losing or thin-sample config. Cron-scheduled after the close.

Verified: 75/75 tests; boot logs the learned params and applies them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-16 22:45:03 -04:00
1 parent 92985938e1
commit c7bfb3512a
4 files changed
+175 -1

No files matched your search

+41
View File
@@ -20,6 +20,8 @@ the live loop accumulate them) for a robust optimum.
from __future__ import annotations
import itertools
import os
from collections import defaultdict
import pandas as pd
from dotenv import load_dotenv
@@ -99,6 +101,43 @@ def run_combo(sessions, *, warmup, min_bars, eval_every, ready, gap_min, rvol_mi
}
def per_setup_report(sessions, *, warmup=6, eval_every=1, ready=65, min_bars=3,
gap_min=0.05, rvol_min=2.0):
"""Break trades down by setup type — shows which setups actually make money.
Always runs with ALL setups enabled (ignores STRATEGY_SETUPS) so the
comparison is visible even when the live config is restricted to ORB-only.
"""
saved = os.environ.pop("STRATEGY_SETUPS", None)
try:
eng = BacktestEngine(
Config(), warmup_bars=warmup, eval_every=eval_every, ready_score_pct=ready,
min_bars=min_bars,
criteria=SetupCriteria(gap_pct_min=gap_min, relative_volume_min=rvol_min),
)
by = defaultdict(lambda: {"n": 0, "win": 0, "R": 0.0, "pnl": 0.0, "em": []})
for symbol, _sess, bars, pc, adv in sessions:
for t in eng.run(bars, symbol, previous_close=pc, avg_daily_volume=adv).trades:
b = by[t.setup_type or "?"]
b["n"] += 1
b["win"] += 1 if (t.realized_pnl or 0) > 0 else 0
b["R"] += t.r_multiple or 0.0
b["pnl"] += t.realized_pnl or 0.0
b["em"].append(_entry_min(bars, t.entry_time))
finally:
if saved is not None:
os.environ["STRATEGY_SETUPS"] = saved
print("\n=== per-setup breakdown (which setups actually make money) ===")
print(f"{'setup':24s} {'trades':>6} {'win%':>5} {'avgR':>6} {'pnl':>9} {'entry_min':>9}")
for st, b in sorted(by.items(), key=lambda x: x[1]["pnl"], reverse=True):
em = [e for e in b["em"] if e == e]
em_avg = (sum(em) / len(em)) if em else float("nan")
print(f"{st:24s} {b['n']:>6} {b['win']/max(1,b['n'])*100:>4.0f}% "
f"{b['R']/max(1,b['n']):>+6.2f} {b['pnl']:>9.0f} {em_avg:>9.1f}")
print(" -> keep only the profitable setups via STRATEGY_SETUPS in .env")
def main():
con = open_research_db("market")
sessions = load_sessions(con)
@@ -145,6 +184,8 @@ def main():
print(f" pnl {base['pnl']:.0f}, win {base['win']*100:.0f}%, R {base['avgR']:.2f}, "
f"{base['trades']} trades, entry@{base['entry_min']:.1f}min")
per_setup_report(sessions)
if __name__ == "__main__":
main()