Files
mymt5opp/scripts/reeval_finalist_forward.py
T
gavindiaz 63a829cc46 phase 7-8 完成 + warmup 修复 + 产物结构化重组
主要内容:
- Phase 8 PROMOTE: finalist #1 (trial #324) registry 条目,自动生成
- Optuna objective warmup bug 修复 (shared/optimizer/objective.py)
- studies/ 目录按用途重组为 optuna/ + finalists/ + features/ 三层
- reports/ 加入 Optuna 中文 dashboard (5 主图 + 18 slice + 15 contour)
- 新增 PROJECT_GUIDE.md 项目说明文档
- 新增 build_registry_entry.py / build_optuna_dashboard.py / build_feature_datasets.py
- .gitignore: 允许提交 studies/*.db (Optuna DB) 和 reports/*.html (MT5 + dashboard)
2026-06-27 00:28:07 +08:00

155 lines
6.4 KiB
Python

"""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())