forked from mnbvc188199/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>
65 lines
2.9 KiB
Python
65 lines
2.9 KiB
Python
"""
| |
Is the START of the month special for metals, or is it just drift?
| |
| |
Every trading-day-of-month position gets the same treatment: the 2-day window
| |
entered at the close of TDOM k-1 and exited at the close of TDOM k+1. If the
| |
first-2-days window is merely riding the 2016-26 metals rally, every position
| |
earns about the drift and TDOM 1-2 is not special. If it is a genuine turn-of-
| |
month effect, it should rank at or near the top of ~20 positions in BOTH IS and
| |
OOS, and beat the drift baseline by a margin a permutation test respects.
| |
"""
| |
| |
from __future__ import annotations
| |
| |
import sys
| |
import numpy as np
| |
| |
sys.path.insert(0, __file__.rsplit("\\", 1)[0] if "\\" in __file__ else ".")
| |
import fx_screen as fs # noqa: E402
| |
| |
SPLIT = np.datetime64("2016-01-01")
| |
| |
| |
def tdom(ts):
| |
"""1-based trading-day-of-month for each bar."""
| |
mon = ts.astype("datetime64[D]").astype("datetime64[M]")
| |
out = np.ones(len(ts), int)
| |
for i in range(1, len(ts)):
| |
out[i] = 1 if mon[i] != mon[i - 1] else out[i - 1] + 1
| |
return out
| |
| |
| |
def profile(sym, width=2):
| |
d = fs.load(sym, "D1")
| |
ts, c = d["ts"], d["c"]
| |
k = tdom(ts)
| |
r = np.full(len(c), np.nan)
| |
r[width:] = (c[width:] - c[:-width]) / c[:-width] # window ENDING at bar i
| |
end_k = k # TDOM of the exit bar
| |
res = {}
| |
for pos in range(width, 21): # exit on TDOM pos -> window TDOM pos-1..pos
| |
m = (end_k == pos) & np.isfinite(r)
| |
res[pos] = (r[m & (ts < SPLIT)], r[m & (ts >= SPLIT)])
| |
return res, np.nanmean(r[ts < SPLIT]), np.nanmean(r[ts >= SPLIT])
| |
| |
| |
if __name__ == "__main__":
| |
rng = np.random.default_rng(0)
| |
for sym in sys.argv[1:] or ["XAUUSD", "XAGUSD", "XPTUSD", "XPDUSD", "XAUEUR"]:
| |
res, dis, doos = profile(sym)
| |
is_m = {p: v[0].mean() * 1e4 for p, v in res.items()}
| |
oo_m = {p: v[1].mean() * 1e4 for p, v in res.items()}
| |
rank_is = sorted(is_m, key=is_m.get, reverse=True).index(2) + 1
| |
rank_oo = sorted(oo_m, key=oo_m.get, reverse=True).index(2) + 1
| |
#--- permutation: how often does a RANDOM set of month-windows of the same
| |
#--- size beat the TDOM-1..2 excess over drift, OOS
| |
oos_all = np.concatenate([v[1] for v in res.values()])
| |
tom = res[2][1]
| |
exc = tom.mean() - oos_all.mean()
| |
perm = np.mean([rng.choice(oos_all, len(tom), replace=False).mean() - oos_all.mean() >= exc
| |
for _ in range(5000)])
| |
print(f"{sym}: TDOM1-2 window IS {is_m[2]:+6.1f} bp (rank {rank_is}/19, all-window mean {dis * 1e4:+.1f})"
| |
f" OOS {oo_m[2]:+6.1f} bp (rank {rank_oo}/19, all-window mean {doos * 1e4:+.1f})"
| |
f" OOS excess {exc * 1e4:+.1f} bp, permutation p = {perm:.4f}")
| |
top = sorted(oo_m.items(), key=lambda x: -x[1])[:4]
| |
print(f" OOS top windows (exit TDOM: bp): {[(p, round(v, 1)) for p, v in top]}")
|