← Back
"""
Step 3: Sweep from cached data.
Usage:
  python3 backtests/sweep_from_cache.py --rr 2   (R:R = 1:2, TP=2×SL)
  python3 backtests/sweep_from_cache.py --rr 3   (R:R = 1:3)
  python3 backtests/sweep_from_cache.py --rr 4
  python3 backtests/sweep_from_cache.py --rr 5

Output: backtests/sweep_rr{N}_results.json
"""

import sys, os, json, pickle, argparse
import numpy as np
from datetime import datetime
from itertools import product

VWAP_PERIOD = 50
MAKER_FEE = 0.0002
TAKER_FEE = 0.00055

# Fixed filters per Rick's request
# Z > 1.8, NATR > 0.5, CHOP > 45

GRID = {
    "z_entry":  [1.9, 2.0, 2.2, 2.5, 2.8, 3.0],
    "z_max":    [3.0, 3.5, 4.0, 5.0],
    "natr_min": [0.5, 0.75, 1.0],
    "natr_max": [2.0, 2.5, 3.0, 3.5],
    "chop_min": [45, 50, 55],
}

# SL values to try — TP = SL × R:R
SL_VALUES = [0.3, 0.5, 0.75, 1.0, 1.5, 2.0]


def run_one(data_cache, cfg):
    all_trades = []
    for sym, d in data_cache.items():
        h=d["h"]; l=d["l"]; c=d["c"]; z_arr=d["z"]; natr_arr=d["natr"]; chop_arr=d["chop"]
        n=len(c); active=None; cooldown=0

        for i in range(VWAP_PERIOD, n):
            z=z_arr[i]; natr=natr_arr[i]; chop=chop_arr[i]

            if active:
                sl_hit=tp_hit=z_tp=False
                if active["side"]=="LONG":
                    if l[i]<=active["sl"]: sl_hit=True; cp=active["sl"]
                    if h[i]>=active["tp"]: tp_hit=True; cp2=active["tp"]
                    if z>=-0.3 and c[i]>active["ep"]: z_tp=True; cp3=c[i]
                else:
                    if h[i]>=active["sl"]: sl_hit=True; cp=active["sl"]
                    if l[i]<=active["tp"]: tp_hit=True; cp2=active["tp"]
                    if z<=0.3 and c[i]<active["ep"]: z_tp=True; cp3=c[i]

                if sl_hit:
                    all_trades.append({"sym":sym,"pnl":_pnl(active,cp,"SL"),"reason":"SL"})
                    active=None; cooldown=i+12
                elif tp_hit:
                    all_trades.append({"sym":sym,"pnl":_pnl(active,cp2,"TP"),"reason":"TP"})
                    active=None; cooldown=i+12
                elif z_tp:
                    all_trades.append({"sym":sym,"pnl":_pnl(active,cp3,"Z-TP"),"reason":"Z-TP"})
                    active=None; cooldown=i+12
                continue

            if i<cooldown: continue
            if natr<cfg["natr_min"] or natr>cfg["natr_max"]: continue
            if chop<cfg["chop_min"]: continue
            if abs(z)>cfg["z_max"]: continue

            if z<-cfg["z_entry"]:
                ep=c[i]; active={"side":"LONG","ep":ep,"qty":(5.0*3)/ep,
                    "tp":ep*(1+cfg["tp_pct"]/100),"sl":ep*(1-cfg["sl_pct"]/100)}
            elif z>cfg["z_entry"]:
                ep=c[i]; active={"side":"SHORT","ep":ep,"qty":(5.0*3)/ep,
                    "tp":ep*(1-cfg["tp_pct"]/100),"sl":ep*(1+cfg["sl_pct"]/100)}

        if active:
            all_trades.append({"sym":sym,"pnl":_pnl(active,c[-1],"END"),"reason":"END"})

    if not all_trades or len(all_trades)<5:
        return None

    wins=[t for t in all_trades if t["pnl"]>0]
    total_pnl=sum(t["pnl"] for t in all_trades)
    gp=sum(t["pnl"] for t in wins)
    gl=abs(sum(t["pnl"] for t in all_trades if t["pnl"]<=0))
    pf=gp/gl if gl>0 else 999

    return {
        "trades":len(all_trades),
        "wr":round(len(wins)/len(all_trades)*100,1),
        "pnl":round(total_pnl,2),
        "pf":round(pf,2),
        "tp_count":sum(1 for t in all_trades if t["reason"]=="TP"),
        "sl_count":sum(1 for t in all_trades if t["reason"]=="SL"),
        "ztp_count":sum(1 for t in all_trades if t["reason"]=="Z-TP"),
        "avg_pnl":round(total_pnl/len(all_trades),4),
    }


def _pnl(trade, close_price, reason):
    if trade["side"]=="LONG": pnl=trade["qty"]*(close_price-trade["ep"])
    else: pnl=trade["qty"]*(trade["ep"]-close_price)
    ef=trade["qty"]*trade["ep"]*TAKER_FEE
    xf=trade["qty"]*close_price*(MAKER_FEE if reason=="TP" else TAKER_FEE)
    return pnl-ef-xf


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--rr", type=int, required=True, help="R:R ratio (2=1:2, 3=1:3, etc)")
    parser.add_argument("--cache", default="backtests/data_cache_7d.pkl")
    args = parser.parse_args()

    rr = args.rr
    print(f"Loading cached data from {args.cache}...")
    with open(args.cache, "rb") as f:
        raw = pickle.load(f)
    data_cache = raw["symbols"]
    print(f"  {len(data_cache)} symbols, {raw['days']}d data from {raw['date']}")

    # Build combos
    keys = list(GRID.keys())
    base_combos = list(product(*[GRID[k] for k in keys]))

    combos = []
    for vals in base_combos:
        base_cfg = dict(zip(keys, vals))
        if base_cfg["z_entry"] >= base_cfg["z_max"]: continue
        for sl in SL_VALUES:
            tp = sl * rr
            cfg = {**base_cfg, "sl_pct": sl, "tp_pct": tp}
            combos.append(cfg)

    print(f"Sweeping {len(combos)} combos for R:R 1:{rr}...")

    results = []
    for idx, cfg in enumerate(combos):
        res = run_one(data_cache, cfg)
        if res and res["pnl"] > 0:  # only save profitable
            res["config"] = cfg
            res["rr"] = rr
            results.append(res)
        if (idx+1) % 500 == 0:
            print(f"  {idx+1}/{len(combos)} — {len(results)} profitable so far")

    results.sort(key=lambda x: x["pnl"], reverse=True)

    out_path = os.path.join(os.path.dirname(__file__), f"sweep_rr{rr}_results.json")
    with open(out_path, "w") as f:
        json.dump({
            "date": datetime.now().isoformat(),
            "rr": rr,
            "total_combos": len(combos),
            "profitable_combos": len(results),
            "top_15": results[:15],
        }, f, indent=2)

    print(f"\n{'='*90}")
    print(f"R:R 1:{rr} — {len(results)} profitable / {len(combos)} total")
    print(f"{'='*90}")

    if results:
        print(f"{'#':>3} {'PnL':>8} {'WR%':>6} {'PF':>6} {'Trd':>5} | Z    Zmax TP%   SL%   CHOP NATR")
        print("-"*90)
        for i, r in enumerate(results[:15]):
            c = r["config"]
            print(f"{i+1:>3} ${r['pnl']:>7.2f} {r['wr']:>5.1f}% {r['pf']:>5.2f} {r['trades']:>5} | "
                  f"{c['z_entry']:>4.1f} {c['z_max']:>4.1f} {c['tp_pct']:>5.2f} {c['sl_pct']:>5.2f} "
                  f"{c['chop_min']:>4} {c['natr_min']:.2f}-{c['natr_max']:.1f}")
    else:
        print("❌ No profitable combos found for this R:R")

    print(f"\nSaved to {out_path}")


if __name__ == "__main__":
    main()

📜 Git History

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