โ† ะะฐะทะฐะด
""" Step 4: Backtest core โ€” single combo โ†’ PnL. Tests one parameter combination against cached kline data. Usage: import pickle from backtest_core import run_backtest with open("data_cache_7d.pkl", "rb") as f: cache = pickle.load(f) params = { "z_entry": 2.0, "z_max": 3.5, "natr_min": 0.75, "natr_max": 2.5, "chop_min": 50, "tp_pct": 2.0, "sl_pct": 1.0, } result = run_backtest(cache["symbols"], params) # result = {"trades": 42, "wins": 28, "losses": 14, "pnl": 12.5, "wr": 66.7, "pf": 2.1} """ import numpy as np def run_backtest(symbols_data: dict, params: dict) -> dict: """ Run backtest for ONE parameter combination across all symbols. Entry logic (per bar): - |Z| > z_entry AND |Z| < z_max - NATR >= natr_min AND NATR <= natr_max - CHOP >= chop_min - Z < 0 โ†’ LONG, Z > 0 โ†’ SHORT Exit: TP% or SL% from entry price (whichever hits first on subsequent bars). Uses HIGH/LOW of each bar to check SL/TP (realistic: intra-bar wicks). Returns dict with trade stats. """ z_entry = params["z_entry"] z_max = params["z_max"] natr_min = params["natr_min"] natr_max = params["natr_max"] chop_min = params["chop_min"] tp_pct = params["tp_pct"] sl_pct = params["sl_pct"] total_wins = 0 total_losses = 0 total_pnl = 0.0 gross_win = 0.0 gross_loss = 0.0 all_trades = [] for sym, d in symbols_data.items(): z = d["z"] natr = d["natr"] chop = d["chop"] c = d["c"] h = d["h"] l = d["l"] n = len(c) i = 0 while i < n - 1: # Check entry conditions at bar i zi = z[i] ni = natr[i] ci_chop = chop[i] if abs(zi) <= z_entry or abs(zi) >= z_max: i += 1 continue if ni < natr_min or ni > natr_max: i += 1 continue if ci_chop < chop_min: i += 1 continue # Determine side if zi < 0: side = "LONG" else: side = "SHORT" entry_price = c[i] if entry_price <= 0: i += 1 continue # Calculate TP/SL prices if side == "LONG": tp_price = entry_price * (1 + tp_pct / 100) sl_price = entry_price * (1 - sl_pct / 100) else: tp_price = entry_price * (1 - tp_pct / 100) sl_price = entry_price * (1 + sl_pct / 100) # Scan forward for exit exit_pnl = None exit_bar = i + 1 for j in range(i + 1, n): if side == "LONG": # Check SL first (worst case โ€” wick hits both) if l[j] <= sl_price: exit_pnl = -sl_pct exit_bar = j break if h[j] >= tp_price: exit_pnl = tp_pct exit_bar = j break else: # SHORT if h[j] >= sl_price: exit_pnl = -sl_pct exit_bar = j break if l[j] <= tp_price: exit_pnl = tp_pct exit_bar = j break if exit_pnl is None: # No exit found โ€” trade still open at end of data, skip break # Record trade if exit_pnl > 0: total_wins += 1 gross_win += exit_pnl else: total_losses += 1 gross_loss += abs(exit_pnl) total_pnl += exit_pnl # Cooldown: skip 6 bars (30 min on 5m) after exit to avoid re-entry i = exit_bar + 6 continue # end while for this symbol total_trades = total_wins + total_losses wr = (total_wins / total_trades * 100) if total_trades > 0 else 0 pf = (gross_win / gross_loss) if gross_loss > 0 else (999 if gross_win > 0 else 0) return { "trades": total_trades, "wins": total_wins, "losses": total_losses, "pnl": round(total_pnl, 2), "wr": round(wr, 1), "pf": round(pf, 2), "gross_win": round(gross_win, 2), "gross_loss": round(gross_loss, 2), }