"""Re-evaluate finalist #1 on the MT5-forward-aligned window. MT5 forward mode (FromDate=2025.01.01, ToDate=2026.06.26, Forward=2026.01.01) produces two segments: IS = 2025-01-01 → 2026-01-01 (12 months) OOS = 2026-01-01 → 2026-06-26 (~6 months, data ends 2026-06-25 23:55) INDICATOR WARMUP (matches MT5 tester behaviour): MT5's Strategy Tester uses pre-test chart history to warm up indicators — EMA(160) on M5 needs ~14 hours of bars before it produces a value, but MT5's first 2025-01-02 trade fires at 01:55 because the indicator was already stable on 2024 data. The previous version of this script sliced bars to [IS_START, IS_END) BEFORE computing signals, so indicators didn't stabilise until ~14 hours into 2025-01-01 and the first Python trade landed at 15:50 — 14 hours late vs MT5. Fix: compute signals on the FULL bars (data starts 2024-06-26 → ~6 months of warmup, well beyond EMA(160)'s 14-hour requirement), then trim the bars + signal arrays + M1 to the evaluation window before running the engine. The engine therefore starts fresh at IS_START (initial_deposit, no open position) exactly like MT5's tester, but sees indicators that are already stable. Outputs the metrics dict that compare_finalist.py will pick up. """ from __future__ import annotations import json import sys from pathlib import Path PROJECT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(PROJECT)) import optuna import pandas as pd from shared.core.engine import SizingInputs from shared.core.metrics import compute_metrics from shared.data.loaders import load_bars from shared.optimizer.selector import select_diverse_topn from strategies.gold_scalper_pro.instruments import XAUUSD_REAL from strategies.gold_scalper_pro.scalper_engine import ( ScalperEngine, engine_kwargs_from_params, ) from strategies.gold_scalper_pro.search_space import FROZEN_BASELINE, SEARCH_SPACE from strategies.gold_scalper_pro.signals import build_signals # MT5-forward-aligned windows (ToDate is exclusive in MT5 tester's day boundary). IS_START = pd.Timestamp("2025-01-01 00:00:00") IS_END = pd.Timestamp("2026-01-01 00:00:00") # forward start OOS_START = pd.Timestamp("2026-01-01 00:00:00") OOS_END = pd.Timestamp("2026-06-26 00:00:00") # data ends 2026-06-25 23:55 INITIAL_DEPOSIT = 1000.0 def run_with_warmup(params, full_bars, full_m1, win_start, win_end): """Compute signals on FULL bars (with pre-window warmup) and run the engine on the trimmed [win_start, win_end) slice only. Mirrors MT5 Strategy Tester: indicator buffers are pre-warmed on history before the test start, but the engine/equity starts fresh at win_start. """ pack = build_signals(params, full_bars, XAUUSD_REAL) ts = pd.to_datetime(full_bars["timestamp"].to_numpy()) lo = int(ts.searchsorted(win_start, side="left")) hi = int(ts.searchsorted(win_end, side="left")) win_bars = full_bars.iloc[lo:hi].reset_index(drop=True) sig_long = pack.signals_long[lo:hi] sig_short = pack.signals_short[lo:hi] sl_p = pack.sl_prices[lo:hi] tp_p = pack.tp_prices[lo:hi] if full_m1 is not None and len(full_m1) > 0: m1_ts = pd.to_datetime(full_m1["timestamp"].to_numpy()) m1_lo = int(m1_ts.searchsorted(win_start, side="left")) m1_hi = int(m1_ts.searchsorted(win_end, side="left")) win_m1 = full_m1.iloc[m1_lo:m1_hi].reset_index(drop=True) else: win_m1 = None engine = ScalperEngine() result = engine.run( win_bars, sig_long, sig_short, sl_p, tp_p, XAUUSD_REAL, SizingInputs(), INITIAL_DEPOSIT, m1_bars=win_m1, **engine_kwargs_from_params(params), ) m = compute_metrics(result, periods_per_year=252 * 24 * 12) return { "net": round(m.net_profit, 2), "PF": round(m.profit_factor, 4), "trades": m.total_trades, "DD%": round(m.max_equity_dd_pct, 6), "sharpe": round(m.sharpe, 4), "win_rate": round(m.win_rate, 4), "first_trade_ts": ( result.trades[0].entry_time.isoformat() if result.trades else None ), } def main() -> int: db = PROJECT / "studies" / "optuna" / "gold_scalper_pro_is2025.db" study = optuna.load_study( study_name="gold_scalper_pro_is2025", storage=f"sqlite:///{db}", ) finalists = select_diverse_topn(study, n=3, ranges=SEARCH_SPACE) if not finalists: sys.exit("no finalists") print("loading bars (full history, used as indicator warmup) ...") full_m5 = load_bars(PROJECT / "data" / "XAUUSD_M5_2024-06-26_2026-06-26.parquet") full_m1 = load_bars(PROJECT / "data" / "XAUUSD_M1_2024-06-26_2026-06-26.parquet") print(f" full M5: {len(full_m5):,} bars full M1: {len(full_m1):,} bars") print(f" IS : {IS_START.date()} → {IS_END.date()} (12 months, MT5 forward-aligned)") print(f" OOS : {OOS_START.date()} → {OOS_END.date()} (~6 months, data ends 2026-06-25)") print(f" warmup window: {full_m5['timestamp'].min()} → {IS_START} " f"(~6 months, EMA(160) needs ~14h so this is plenty)") out = {"windows": {"IS": [str(IS_START), str(IS_END)], "OOS": [str(OOS_START), str(OOS_END)]}, "finalists": []} for i, t in enumerate(finalists, 1): merged = {**FROZEN_BASELINE, **t.params} print(f"\n--- finalist #{i} (trial #{t.number}) ---") is_m = run_with_warmup(merged, full_m5, full_m1, IS_START, IS_END) oos_m = run_with_warmup(merged, full_m5, full_m1, OOS_START, OOS_END) print(f" IS : net={is_m['net']:.2f} PF={is_m['PF']:.2f} " f"trades={is_m['trades']} DD%={is_m['DD%']:.2%} " f"first={is_m['first_trade_ts']}") print(f" OOS : net={oos_m['net']:.2f} PF={oos_m['PF']:.2f} " f"trades={oos_m['trades']} DD%={oos_m['DD%']:.2%} " f"first={oos_m['first_trade_ts']}") out["finalists"].append({ "index": i, "trial_number": t.number, "score": round(t.value, 2), "params": t.params, "merged_params": merged, "IS": is_m, "OOS": oos_m, }) out_path = PROJECT / "studies" / "finalists" / "gold_scalper_pro_is2025-2026.json" out_path.write_text(json.dumps(out, indent=2, default=str), encoding="utf-8") print(f"\n saved: {out_path}") return 0 if __name__ == "__main__": raise SystemExit(main())