114 lines
4.5 KiB
Python
114 lines
4.5 KiB
Python
"""Diagnose the IS sizing mismatch: Python avg net/trade = $0.38 vs MT5 $2.97.
|
||||
|
|
|
|||
|
|
If gross P/L scales proportionally to MT5 (factor ~1/7.8) and trade count
|
|||
|
|
matches, it's pure sizing. If PF also shifts, the BE/trailing logic differs.
|
|||
|
|
"""
|
|||
|
|
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 pandas as pd
|
|||
|
|
|
|||
|
|
from shared.core.engine import SizingInputs
|
|||
|
|
from shared.core.metrics import compute_metrics
|
|||
|
|
from shared.data.loaders import load_bars
|
|||
|
|
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
|
|||
|
|
import optuna
|
|||
|
|
from shared.optimizer.selector import select_diverse_topn
|
|||
|
|
|
|||
|
|
|
|||
|
|
IS_START = pd.Timestamp("2025-01-01 00:00:00")
|
|||
|
|
IS_END = pd.Timestamp("2026-01-01 00:00:00")
|
|||
|
|
|
|||
|
|
|
|||
|
|
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)
|
|||
|
|
t = finalists[0]
|
|||
|
|
merged = {**FROZEN_BASELINE, **t.params}
|
|||
|
|
|
|||
|
|
print(f"finalist #1 (trial #{t.number})")
|
|||
|
|
print(f" InpRiskPercent = {merged['InpRiskPercent']}")
|
|||
|
|
print(f" InpAtrSLMult = {merged['InpAtrSLMult']}")
|
|||
|
|
print(f" InpAtrTPMult = {merged['InpAtrTPMult']}")
|
|||
|
|
|
|||
|
|
m5 = load_bars(PROJECT / "data" / "XAUUSD_M5_2024-06-26_2026-06-26.parquet")
|
|||
|
|
m1 = load_bars(PROJECT / "data" / "XAUUSD_M1_2024-06-26_2026-06-26.parquet")
|
|||
|
|
is_bars = m5[(m5["timestamp"] >= IS_START) & (m5["timestamp"] < IS_END)].reset_index(drop=True)
|
|||
|
|
is_m1 = m1[(m1["timestamp"] >= IS_START) & (m1["timestamp"] < IS_END)].reset_index(drop=True)
|
|||
|
|
print(f" IS bars: {len(is_bars):,} IS M1: {len(is_m1):,}")
|
|||
|
|
|
|||
|
|
pack = build_signals(merged, is_bars, XAUUSD_REAL)
|
|||
|
|
engine = ScalperEngine()
|
|||
|
|
result = engine.run(
|
|||
|
|
is_bars, pack.signals_long, pack.signals_short,
|
|||
|
|
pack.sl_prices, pack.tp_prices,
|
|||
|
|
XAUUSD_REAL, SizingInputs(), 1000.0,
|
|||
|
|
m1_bars=is_m1,
|
|||
|
|
**engine_kwargs_from_params(merged),
|
|||
|
|
)
|
|||
|
|
m = compute_metrics(result, periods_per_year=252 * 24 * 12)
|
|||
|
|
|
|||
|
|
gross_profit = sum(t.pnl for t in result.trades if t.pnl > 0)
|
|||
|
|
gross_loss = sum(t.pnl for t in result.trades if t.pnl < 0)
|
|||
|
|
print(f"\nPython IS:")
|
|||
|
|
print(f" trades = {m.total_trades}")
|
|||
|
|
print(f" gross profit = {gross_profit:.2f}")
|
|||
|
|
print(f" gross loss = {gross_loss:.2f}")
|
|||
|
|
print(f" net = {gross_profit + gross_loss:.2f}")
|
|||
|
|
print(f" PF = {gross_profit / -gross_loss:.4f}" if gross_loss < 0 else " PF = inf")
|
|||
|
|
print(f" avg net/trade = {(gross_profit + gross_loss) / m.total_trades:.4f}")
|
|||
|
|
|
|||
|
|
# Sample first 5 trades — check lot sizes & prices.
|
|||
|
|
print(f"\nFirst 5 trades:")
|
|||
|
|
print(f" {'time':<21} {'dir':<5} {'entry':>10} {'exit':>10} {'lots':>8} {'pnl':>9} {'reason'}")
|
|||
|
|
for t in result.trades[:5]:
|
|||
|
|
d = "LONG" if t.direction.name == "LONG" else "SHRT"
|
|||
|
|
print(f" {str(t.entry_time):<21} {d:<5} {t.entry_price:>10.2f} "
|
|||
|
|
f"{t.exit_price:>10.2f} {t.lots:>8.4f} {t.pnl:>9.4f} {t.exit_reason}")
|
|||
|
|
|
|||
|
|
# Distribution of lots.
|
|||
|
|
import numpy as np
|
|||
|
|
lots_arr = np.array([t.lots for t in result.trades])
|
|||
|
|
print(f"\n lots: min={lots_arr.min():.4f} max={lots_arr.max():.4f} "
|
|||
|
|
f"mean={lots_arr.mean():.4f} median={np.median(lots_arr):.4f}")
|
|||
|
|
print(f" lots unique count: {len(np.unique(lots_arr))}")
|
|||
|
|
print(f" lots histogram (top 5):")
|
|||
|
|
vals, counts = np.unique(lots_arr, return_counts=True)
|
|||
|
|
for v, c in sorted(zip(vals, counts), key=lambda x: -x[1])[:5]:
|
|||
|
|
print(f" {v:.4f} ×{c}")
|
|||
|
|
|
|||
|
|
# MT5 comparison.
|
|||
|
|
print(f"\nMT5 IS (from report):")
|
|||
|
|
print(f" gross profit = 23416.75")
|
|||
|
|
print(f" gross loss = -16429.41")
|
|||
|
|
print(f" net = 6987.34")
|
|||
|
|
print(f" PF = 1.43")
|
|||
|
|
print(f" trades = 2348")
|
|||
|
|
print(f" avg net/trade = {6987.34/2348:.4f}")
|
|||
|
|
|
|||
|
|
# Scaling check: if Python lots were 7.8x larger, would P/L match?
|
|||
|
|
py_gross = gross_profit
|
|||
|
|
mt5_gross = 23416.75
|
|||
|
|
print(f"\n scaling factor (MT5 gross profit / Python gross profit): "
|
|||
|
|
f"{mt5_gross/py_gross:.2f}x")
|
|||
|
|
|
|||
|
|
return 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
raise SystemExit(main())
|