forked from animatedread/Warrior_EA
Hypotheses were registered in FX_PLAN.md before each round. Trend, breakout, cross reversion, hour seasonality, month-end USD, carry-cross dip-buy, metals dip-buy and flight-to-safety all fail the bar. The weekend-gap fade looked like the best result of the project on bar data (OOS t 20, 28/28 pairs) and loses on real ticks (EURCHF PF 0.52, AUDNZD PF 0.53): the Sunday-open spread is as wide as the gap. WarriorGapFade is kept as the research artifact that proved it and is flagged DO NOT TRADE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
"""
|
|
Round 2 of the forex/metals search (FX_PLAN.md, registered before running).
|
|
|
|
python fx_round2.py X1|X2|X3|X4
|
|
|
|
X2 DEVIATION, recorded rather than hidden: broker SP500 H4 history starts in
|
|
2021, so the registered 2016 split is impossible on H4. X2 is therefore run
|
|
twice: H4 2021-2026 split at 2024 (the split every index result used), and D1
|
|
2008-2026 split at 2016 (the registered split, one timeframe up).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import numpy as np
|
|
|
|
sys.path.insert(0, __file__.rsplit("\\", 1)[0] if "\\" in __file__ else ".")
|
|
import backtest as bt # noqa: E402
|
|
import fx_screen as fs # noqa: E402
|
|
from run_screen import zscore_entries # noqa: E402
|
|
from vol_filter_test import vol_pctile # noqa: E402
|
|
|
|
RISK = ["AUDJPY", "NZDJPY", "CADJPY", "AUDUSD", "NZDUSD", "USDJPY"]
|
|
METALS = ["XAUUSD", "XAGUSD", "XPTUSD", "XPDUSD", "XAUEUR"]
|
|
|
|
|
|
def as_rows(trades):
|
|
return [dict(i=t["entry_i"], j=t["exit_i"], t=t["t"], side=1, ret=t["ret"],
|
|
gross=t["gross"], bars=t["bars"]) for t in trades]
|
|
|
|
|
|
def dipz(d, gated):
|
|
e, xma = zscore_entries(d, 20, -1.5, 0)
|
|
if gated:
|
|
e = e & (np.nan_to_num(vol_pctile(d), nan=-1) >= 0.50)
|
|
return as_rows(bt.simulate(d, e, side=1, exit_ma=xma, max_bars=10, stop_atr=3.0))
|
|
|
|
|
|
def run_dipz(syms, label):
|
|
for gated in (False, True):
|
|
rows, allt = [], []
|
|
for s in syms:
|
|
d = fs.load(s, "H4")
|
|
tr = dipz(d, gated)
|
|
allt.append(tr)
|
|
rows.append(fs.score(s, d, tr))
|
|
fs.pooled(allt, f"{label} gate={'on' if gated else 'off'}")
|
|
fs.report(f"{label} long-only z-dip H4, gate {'ON' if gated else 'off'}", rows, 2 * len(syms))
|
|
|
|
|
|
def x2(tf, split, spx_file):
|
|
"""SP500 dip signal -> long the risk cross, filled at the cross's next open."""
|
|
import run_screen as rs
|
|
old = bt.COMMON
|
|
spx = bt.load("SP500", spx_file)
|
|
e, _ = rs.zscore_entries(spx, 20, -1.5, 0)
|
|
if tf == "H4":
|
|
e = e & (np.nan_to_num(vol_pctile(spx), nan=-1) >= 0.50)
|
|
sig_t = set(spx["ts"][e].astype("datetime64[s]").tolist())
|
|
fs_split = fs.SPLIT
|
|
fs.SPLIT = np.datetime64(split)
|
|
rows, allt = [], []
|
|
for s in RISK:
|
|
d = fs.load(s, tf)
|
|
ent = np.array([t in sig_t for t in d["ts"].astype("datetime64[s]").tolist()])
|
|
m = bt.sma(d["c"], 20)
|
|
tr = as_rows(bt.simulate(d, ent, side=1, exit_ma=m, max_bars=10, stop_atr=3.0))
|
|
lo = np.datetime64(str(spx["ts"][0])[:10])
|
|
tr = [t for t in tr if t["t"] >= lo]
|
|
allt.append(tr)
|
|
rows.append(fs.score(s, d, tr))
|
|
fs.pooled(allt, f"X2 {tf}")
|
|
fs.report(f"X2 SP500-dip -> long risk FX, {tf}, split {split}", rows, len(RISK))
|
|
fs.SPLIT = fs_split
|
|
bt.COMMON = old
|
|
|
|
|
|
def x3(k):
|
|
"""Weekend gap fade on H1."""
|
|
rows, allt = [], []
|
|
for s in fs.ALL:
|
|
d = fs.load(s, "H1")
|
|
o, h, l, c, ts = d["o"], d["h"], d["l"], d["c"], d["ts"]
|
|
a = bt.atr(h, l, c, 14)
|
|
day = ts.astype("datetime64[D]")
|
|
gapi = np.where((ts[1:] - ts[:-1]) > np.timedelta64(36, "h"))[0] + 1
|
|
tr = []
|
|
for i in gapi:
|
|
if not np.isfinite(a[i - 1]) or a[i - 1] <= 0:
|
|
continue
|
|
gap = o[i] - c[i - 1]
|
|
if abs(gap) < k * a[i - 1]:
|
|
continue
|
|
side = -int(np.sign(gap))
|
|
entry, target = o[i], c[i - 1]
|
|
stop = entry - side * 3.0 * a[i - 1]
|
|
px, j = None, i
|
|
while j < len(c) and day[j] == day[i]:
|
|
if (side > 0 and l[j] <= stop) or (side < 0 and h[j] >= stop):
|
|
px = stop
|
|
break
|
|
if (side > 0 and h[j] >= target) or (side < 0 and l[j] <= target):
|
|
px = target
|
|
break
|
|
j += 1
|
|
if px is None:
|
|
j = j - 1
|
|
px = c[j]
|
|
tr.append(fs.trade(d, i, j, side, entry, px))
|
|
allt.append(tr)
|
|
rows.append(fs.score(s, d, tr))
|
|
fs.pooled(allt, f"X3 k={k}")
|
|
fs.report(f"X3 weekend gap fade, k={k}", rows, 2 * len(fs.ALL))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
w = sys.argv[1]
|
|
if w == "X1":
|
|
run_dipz(RISK, "X1 risk crosses")
|
|
elif w == "X4":
|
|
run_dipz(METALS, "X4 metals")
|
|
elif w == "X2":
|
|
x2("H4", "2024-01-01", "PERIOD_H4")
|
|
x2("D1", "2016-01-01", "PERIOD_D1")
|
|
elif w == "X3":
|
|
for k in (0.5, 1.0):
|
|
x3(k)
|