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