← Back
"""
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),
    }

📜 Git History

dd32dfdchore: initial commit — version control setup5 weeks ago
Show last diff
Loading...