605faf5310
Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
215 lines
8.2 KiB
Python
215 lines
8.2 KiB
Python
"""
|
|
Random-search optimizer for USDCHF Playbook.
|
|
|
|
Targets: net_profit > 0, trades >= min_trades, stable PF.
|
|
|
|
Usage:
|
|
python run_optimize.py --trials 3000 --min-trades 150
|
|
python run_optimize.py --profile balanced --trials 5000
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import random
|
|
import sys
|
|
from dataclasses import asdict
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import MetaTrader5 as mt5
|
|
import pandas as pd
|
|
|
|
ROOT = Path(__file__).resolve().parents[3]
|
|
sys.path.insert(0, str(ROOT / "backtesting" / "MT5"))
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from cluster_audit.backtest_core import CostModel, load_bars, resolve_symbol # noqa: E402
|
|
from strategy_core import PlaybookParams, build_market, pip_size, simulate # noqa: E402
|
|
|
|
LAB = Path(__file__).resolve().parent
|
|
EA_SET = LAB / "USDCHF_Playbook.set"
|
|
|
|
|
|
def sample_params(rng: random.Random, high_freq: bool) -> PlaybookParams:
|
|
if high_freq:
|
|
return PlaybookParams(
|
|
daily_ema_period=rng.choice([34, 50, 100]),
|
|
use_daily_bias=rng.choice([True, True, False]),
|
|
htf_zone_bars=rng.choice([12, 16, 20]),
|
|
min_break_body_ratio=round(rng.uniform(0.45, 0.65), 2),
|
|
use_double_trap=rng.choice([True, True, False]),
|
|
ny_chaos_start=rng.choice([11, 12, 13]),
|
|
ny_chaos_end=rng.choice([14, 15, 16]),
|
|
momentum_start=rng.choice([14, 15, 16]),
|
|
momentum_end=rng.choice([1, 2, 3]),
|
|
ltf_fast_ema=rng.randint(5, 12),
|
|
ltf_slow_ema=rng.choice([18, 21, 26, 34]),
|
|
entry_mode=rng.choice([0, 1, 1, 2]),
|
|
atr_sl_mult=round(rng.uniform(1.4, 2.4), 2),
|
|
atr_tp_mult=round(rng.uniform(3.0, 6.0), 2),
|
|
use_trailing=rng.choice([True, False]),
|
|
trail_atr_mult=round(rng.uniform(0.9, 1.6), 2),
|
|
max_bars_in_trade=rng.choice([64, 96, 128]),
|
|
use_compression_filter=rng.choice([True, False]),
|
|
compress_atr_ratio=round(rng.uniform(0.55, 0.80), 2),
|
|
cooldown_bars=rng.choice([2, 3, 4]),
|
|
)
|
|
return PlaybookParams(
|
|
daily_ema_period=rng.choice([50, 100]),
|
|
htf_zone_bars=rng.choice([16, 20, 24]),
|
|
min_break_body_ratio=round(rng.uniform(0.50, 0.70), 2),
|
|
use_double_trap=rng.choice([True, False]),
|
|
ny_chaos_start=rng.choice([12, 13]),
|
|
ny_chaos_end=rng.choice([14, 15, 16]),
|
|
momentum_start=rng.choice([15, 16]),
|
|
momentum_end=rng.choice([2, 3]),
|
|
entry_mode=rng.choice([1, 1, 2]),
|
|
atr_sl_mult=round(rng.uniform(1.6, 2.2), 2),
|
|
atr_tp_mult=round(rng.uniform(3.5, 5.5), 2),
|
|
max_bars_in_trade=rng.choice([80, 96, 120]),
|
|
cooldown_bars=rng.choice([4, 6, 8]),
|
|
)
|
|
|
|
|
|
def write_set(p: PlaybookParams, path: Path) -> None:
|
|
lines = [
|
|
"; USDCHF Playbook optimized",
|
|
"Timeframe=15",
|
|
f"UseDailyBias={'true' if p.use_daily_bias else 'false'}",
|
|
f"DailyEmaPeriod={p.daily_ema_period}",
|
|
f"HtfZoneBars={p.htf_zone_bars}",
|
|
f"MinBreakBodyRatio={p.min_break_body_ratio}",
|
|
f"UseDoubleTrap={'true' if p.use_double_trap else 'false'}",
|
|
f"NyChaosStartHour={p.ny_chaos_start}",
|
|
f"NyChaosEndHour={p.ny_chaos_end}",
|
|
f"MomentumStartHour={p.momentum_start}",
|
|
f"MomentumEndHour={p.momentum_end}",
|
|
f"LtfFastEma={p.ltf_fast_ema}",
|
|
f"LtfSlowEma={p.ltf_slow_ema}",
|
|
f"EntryMode={p.entry_mode}",
|
|
f"AtrPeriod={p.atr_period}",
|
|
f"AtrSlMult={p.atr_sl_mult}",
|
|
f"AtrTpMult={p.atr_tp_mult}",
|
|
f"UseTrailing={'true' if p.use_trailing else 'false'}",
|
|
f"TrailAtrMult={p.trail_atr_mult}",
|
|
f"MaxBarsInTrade={p.max_bars_in_trade}",
|
|
f"ExtendHoldMomentum={'true' if p.extend_hold_in_momentum else 'false'}",
|
|
f"CooldownBars={p.cooldown_bars}",
|
|
f"UseCompressionFilter={'true' if p.use_compression_filter else 'false'}",
|
|
f"CompressAtrRatio={p.compress_atr_ratio}",
|
|
f"LotSize={p.lot_size}",
|
|
]
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
def score_result(r, min_trades: int, profile: str) -> float:
|
|
if r.total_trades < min_trades:
|
|
return float("-inf")
|
|
if r.net_profit <= 0:
|
|
return float("-inf")
|
|
if r.profit_factor < 1.02:
|
|
return float("-inf")
|
|
base = r.net_profit
|
|
if profile == "high-freq":
|
|
return base + r.total_trades * 2.0
|
|
if profile == "balanced":
|
|
return base + r.total_trades * 5.0 - r.max_drawdown_pct * 50.0
|
|
return base - r.max_drawdown_pct * 80.0
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--symbol", default="USDCHF")
|
|
ap.add_argument("--start", default="2022-01-01")
|
|
ap.add_argument("--end", default="2026-01-01")
|
|
ap.add_argument("--trials", type=int, default=3000)
|
|
ap.add_argument("--min-trades", type=int, default=120)
|
|
ap.add_argument("--max-trades", type=int, default=800)
|
|
ap.add_argument("--profile", choices=["profit", "high-freq", "balanced"], default="balanced")
|
|
ap.add_argument("--seed", type=int, default=42)
|
|
args = ap.parse_args()
|
|
rng = random.Random(args.seed)
|
|
|
|
if not mt5.initialize():
|
|
raise SystemExit("MT5 init failed")
|
|
try:
|
|
sym = resolve_symbol(args.symbol)
|
|
df = load_bars(sym, mt5.TIMEFRAME_M15, datetime.fromisoformat(args.start), datetime.fromisoformat(args.end))
|
|
costs = CostModel.for_symbol(sym)
|
|
pip = pip_size(sym)
|
|
point = float(mt5.symbol_info(sym).point)
|
|
print(f"{sym} M15 bars={len(df)} trials={args.trials} profile={args.profile}", flush=True)
|
|
|
|
best_sc = float("-inf")
|
|
best = None
|
|
best_p = None
|
|
rows = []
|
|
high_freq = args.profile == "high-freq"
|
|
|
|
for n in range(1, args.trials + 1):
|
|
p = sample_params(rng, high_freq)
|
|
md = build_market(df, p)
|
|
r = simulate(md, sym, p, costs, pip, point)
|
|
sc = score_result(r, args.min_trades, args.profile)
|
|
if args.max_trades and r.total_trades > args.max_trades:
|
|
sc = float("-inf")
|
|
rows.append({"trial": n, "score": sc, "net": r.net_profit, "trades": r.total_trades, "pf": r.profit_factor, **asdict(p)})
|
|
if sc > best_sc:
|
|
best_sc, best, best_p = sc, r, p
|
|
print(
|
|
f" NEW BEST {n}: net=${r.net_profit:,.0f} trades={r.total_trades} "
|
|
f"PF={r.profit_factor:.2f} DD={r.max_drawdown_pct:.1f}%",
|
|
flush=True,
|
|
)
|
|
if n % 500 == 0:
|
|
b = best
|
|
print(f" ... {n}/{args.trials} best_net=${b.net_profit if b else 0:,.0f}", flush=True)
|
|
|
|
pd.DataFrame(rows).sort_values("score", ascending=False).to_csv(LAB / "optimize_trials.csv", index=False)
|
|
assert best and best_p
|
|
|
|
with open(LAB / "best_params.json", "w", encoding="utf-8") as f:
|
|
json.dump(
|
|
{
|
|
"metrics": {
|
|
"net_profit": best.net_profit,
|
|
"total_trades": best.total_trades,
|
|
"profit_factor": best.profit_factor,
|
|
"win_rate": best.win_rate,
|
|
"max_drawdown_pct": best.max_drawdown_pct,
|
|
"sharpe": best.sharpe,
|
|
},
|
|
"params": asdict(best_p),
|
|
},
|
|
f,
|
|
indent=2,
|
|
)
|
|
write_set(best_p, EA_SET)
|
|
|
|
trows = [
|
|
{
|
|
"side": t["side"],
|
|
"open_time": df.index[t["open_i"]],
|
|
"close_time": df.index[t["close_i"]],
|
|
"profit": t["profit"],
|
|
"exit_reason": t["exit_reason"],
|
|
}
|
|
for t in best.trades
|
|
]
|
|
pd.DataFrame(trows).to_csv(LAB / "best_trades.csv", index=False)
|
|
|
|
print(
|
|
f"\nBEST: net=${best.net_profit:,.2f} trades={best.total_trades} "
|
|
f"PF={best.profit_factor:.2f} WR={best.win_rate:.1f}% MaxDD={best.max_drawdown_pct:.1f}%",
|
|
flush=True,
|
|
)
|
|
print(f"Saved {EA_SET.name} and best_params.json", flush=True)
|
|
finally:
|
|
mt5.shutdown()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|