Files
zhutoutoutousan 605faf5310 Prepare source-only public release for develop.
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>
2026-07-02 15:03:43 +02:00

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