63a829cc46
主要内容: - 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)
155 lines
6.4 KiB
Python
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())
|