← Назад
""" 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()