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

255 lines
8.6 KiB
Python

"""
RSIScalpingAdaptive XAUUSD — monthly walk-forward validation (Python).
Mirrors the in-EA optimizer: each calendar month, grid-search the prior month,
pick the best score, then forward-test that month with the selected params.
Usage:
python run_walk_forward.py
python run_walk_forward.py --symbol XAUUSD --start 2023-01-01 --end 2026-01-01
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict, dataclass
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"))
from cluster_audit.backtest_core import CostModel, load_bars, resolve_symbol # noqa: E402
from run_backtest import StrategyParams, run_backtest # noqa: E402
STRATEGY_ID = "RSIScalpingAdaptiveXAUUSD"
@dataclass
class SearchGrid:
rsi_period: tuple[int, int, int] = (12, 18, 2)
rsi_overbought: tuple[float, float, float] = (65.0, 77.0, 3.0)
rsi_oversold: tuple[float, float, float] = (50.0, 63.0, 3.0)
rsi_target_buy: tuple[float, float, float] = (75.0, 86.0, 3.0)
rsi_target_sell: tuple[float, float, float] = (50.0, 63.0, 3.0)
bars_to_wait: tuple[int, int, int] = (1, 4, 1)
min_trades: int = 8
max_combos: int = 600
weight_sharpe: float = 0.35
weight_net: float = 0.25
weight_pf: float = 0.15
weight_dd: float = 0.10
def _frange(start: float, stop: float, step: float) -> list[float]:
out: list[float] = []
v = start
while v <= stop + 1e-9:
out.append(round(v, 6))
v += step
return out
def _irange(start: int, stop: int, step: int) -> list[int]:
return list(range(start, stop + 1, step))
def score_report(report, min_trades: int, grid: SearchGrid) -> float:
if report.total_trades < min_trades or report.net_profit <= 0 or report.profit_factor < 1.05:
return float("-inf")
pf = min(report.profit_factor, 4.0) / 4.0
return (
report.sharpe * grid.weight_sharpe
+ (report.net_profit / 2000.0) * grid.weight_net
+ pf * grid.weight_pf
- report.max_drawdown_pct * grid.weight_dd
)
def is_valid(p: StrategyParams) -> bool:
return p.rsi_target_buy > p.rsi_oversold and p.rsi_target_sell < p.rsi_overbought
def iter_params(fallback: StrategyParams, grid: SearchGrid):
yield fallback
tested = 0
for rp in _irange(*grid.rsi_period):
for ob in _frange(*grid.rsi_overbought):
for os in _frange(*grid.rsi_oversold):
for tb in _frange(*grid.rsi_target_buy):
for ts in _frange(*grid.rsi_target_sell):
for bw in _irange(*grid.bars_to_wait):
if tested >= grid.max_combos:
return
p = StrategyParams(
rsi_period=rp,
rsi_overbought=ob,
rsi_oversold=os,
rsi_target_buy=tb,
rsi_target_sell=ts,
bars_to_wait=bw,
lot_size=fallback.lot_size,
initial_balance=fallback.initial_balance,
)
if is_valid(p):
tested += 1
yield p
def month_starts(start: datetime, end: datetime) -> list[pd.Timestamp]:
idx = pd.date_range(start=start, end=end, freq="MS")
return list(idx)
def previous_month_bounds(ts: pd.Timestamp) -> tuple[datetime, datetime]:
prev_end = ts - pd.Timedelta(seconds=1)
prev_start = prev_end.replace(day=1)
return prev_start.to_pydatetime(), prev_end.to_pydatetime()
def month_bounds(ts: pd.Timestamp) -> tuple[datetime, datetime]:
start = ts.to_pydatetime()
end = (ts + pd.offsets.MonthBegin(1) - pd.Timedelta(seconds=1)).to_pydatetime()
return start, end
def optimize_month(
df_all: pd.DataFrame,
symbol: str,
costs: CostModel,
opt_start: datetime,
opt_end: datetime,
fallback: StrategyParams,
grid: SearchGrid,
):
df = df_all.loc[(df_all.index >= opt_start) & (df_all.index <= opt_end)]
if len(df) < 80:
return fallback, None, 0
best_p = fallback
best_r = None
best_score = float("-inf")
combos = 0
for p in iter_params(fallback, grid):
report = run_backtest(df, symbol, p, costs, f"{opt_start.date()}_{opt_end.date()}", "H1")
combos += 1
sc = score_report(report, grid.min_trades, grid)
if sc > best_score:
best_score = sc
best_p = p
best_r = report
return best_p, best_r, combos
def forward_month(
df_all: pd.DataFrame,
symbol: str,
costs: CostModel,
fwd_start: datetime,
fwd_end: datetime,
params: StrategyParams,
):
df = df_all.loc[(df_all.index >= fwd_start) & (df_all.index <= fwd_end)]
if len(df) < 20:
return None
return run_backtest(df, symbol, params, costs, f"{fwd_start.date()}_{fwd_end.date()}", "H1")
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=f"{STRATEGY_ID} walk-forward")
p.add_argument("--symbol", default="XAUUSD")
p.add_argument("--start", default="2023-01-01")
p.add_argument("--end", default="2026-01-01")
p.add_argument("--balance", type=float, default=10_000.0)
p.add_argument("--lot", type=float, default=0.1)
return p.parse_args()
def main() -> None:
args = parse_args()
out_dir = Path(__file__).resolve().parent
fallback = StrategyParams(lot_size=args.lot, initial_balance=args.balance)
grid = SearchGrid()
if not mt5.initialize():
raise SystemExit("MetaTrader5 initialize() failed")
try:
symbol = resolve_symbol(args.symbol)
start = datetime.fromisoformat(args.start)
end = datetime.fromisoformat(args.end)
warmup = start - pd.Timedelta(days=45)
print(f"Loading {symbol} H1 bars from {warmup.date()} to {end.date()} ...")
df_all = load_bars(symbol, mt5.TIMEFRAME_H1, warmup.to_pydatetime(), end)
costs = CostModel.for_symbol(symbol)
rows = []
cumulative = 0.0
for month_ts in month_starts(start, end):
if month_ts.to_pydatetime() >= end:
break
opt_start, opt_end = previous_month_bounds(month_ts)
fwd_start, fwd_end = month_bounds(month_ts)
if fwd_start >= end:
continue
best_p, opt_report, combos = optimize_month(
df_all, symbol, costs, opt_start, opt_end, fallback, grid
)
fwd_report = forward_month(df_all, symbol, costs, fwd_start, fwd_end, best_p)
if fwd_report is None:
continue
cumulative += fwd_report.net_profit
rows.append(
{
"month": str(month_ts.date())[:7],
"opt_window": f"{opt_start.date()}..{opt_end.date()}",
"combos_tested": combos,
"selected": asdict(best_p),
"opt_net": opt_report.net_profit if opt_report else 0.0,
"opt_sharpe": opt_report.sharpe if opt_report else 0.0,
"fwd_net": fwd_report.net_profit,
"fwd_trades": fwd_report.total_trades,
"fwd_sharpe": fwd_report.sharpe,
"fwd_pf": fwd_report.profit_factor,
"fwd_dd_pct": fwd_report.max_drawdown_pct,
"cumulative_net": cumulative,
}
)
print(
f"{rows[-1]['month']} | opt ${rows[-1]['opt_net']:,.0f} "
f"-> fwd ${rows[-1]['fwd_net']:,.0f} | cum ${cumulative:,.0f} | "
f"RSI={best_p.rsi_period} OB={best_p.rsi_overbought} OS={best_p.rsi_oversold}"
)
summary = {
"strategy": STRATEGY_ID,
"symbol": symbol,
"start": args.start,
"end": args.end,
"months": len(rows),
"cumulative_net": cumulative,
"rows": rows,
}
out_path = out_dir / "walk_forward_report.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(summary, f, indent=2, ensure_ascii=False)
pd.DataFrame(rows).to_csv(out_dir / "walk_forward_monthly.csv", index=False)
print(f"\nWalk-forward cumulative net: ${cumulative:,.2f} over {len(rows)} months")
print(f"Saved {out_path}")
finally:
mt5.shutdown()
if __name__ == "__main__":
main()