Warrior_EA/research/fx_tom.py
AnimateDread 6192393511 research(fx): forex and metals - thirteen registered families, nothing passed
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>
2026-09-23 13:24:57 -04:00

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]}")