"""Cost is a RATIO, and the hourly tests were measuring it at its worst possible scale. The recurring verdict in this project - "the effect is real and the spread is bigger" - was established almost entirely on short holds. That is the regime where cost is guaranteed to dominate, because cost is roughly FIXED per round trip while the available move grows with the square root of holding time: cost / move ~ spread / (sigma * sqrt(T)) So a one-hour hold pays the same spread as a one-week hold and gets ~13x less movement to pay it with. Concluding "nothing survives cost" from hourly tests is close to circular. This measures the ratio directly across holding periods, then tests the swing structure that actually gets traded: enter at a weekday/session, exit at the Friday close so no weekend financing is paid. COSTS CHARGED IN FULL --------------------- spread the real bid/ask at entry and exit, from the M1 book commission 0.32 bp per side, the realistic ECN figure swap per night held, swept - MT5 keeps no swap history so it cannot be recovered from data. Closing at the Friday close avoids the 2-3 night weekend charge, which is the single largest avoidable financing item for a swing trader. A note on what CANNOT be done: swap direction cannot be predicted from history because the history does not exist. It can only be measured forward from the symbol spec. Treat it as a known constant per instrument and direction, never as something to forecast. """ import numpy as np, sys, datetime as dt sys.stdout.reconfigure(encoding='utf-8', errors='replace') import book, fills SYMS = ('EURUSD', 'USDJPY', 'XAUUSD', 'SP500') COMM_BP = 0.32 # per side SWAP_BP_DAY = 2.05 # ~7.5%/yr financing, per night, in bp of notional def scale_table(sym): """Mean absolute move vs round-trip cost, as a function of holding time.""" bk = fills.Book(sym) f = book.frame(sym, 'H1', bk) c = f.c sp_bp = float(np.median((bk.ac - bk.bc) / bk.bc)) * 1e4 rows = [] for hrs, label in ((1, '1 hour'), (4, '4 hours'), (24, '1 day'), (72, '3 days'), (120, '5 days'), (480, '20 days')): i = np.arange(300, len(c) - hrs) mv = np.abs(np.log(c[i + hrs] / c[i])) * 1e4 nights = hrs / 24.0 cost = sp_bp + 2 * COMM_BP + nights * SWAP_BP_DAY rows.append((label, mv.mean(), sp_bp, 2 * COMM_BP, nights * SWAP_BP_DAY, cost, mv.mean() / cost)) return sp_bp, rows def weekly(sym, entry_dow, entry_hour, swap_bp_day=SWAP_BP_DAY): """Enter at a given weekday/hour, exit at the FRIDAY close of the same week. Both directions are reported, because a swing edge must show up as a directional asymmetry - if long and short are mirror images the only thing being measured is cost. """ bk = fills.Book(sym) f = book.frame(sym, 'H1', bk) d = np.array([dt.datetime.fromtimestamp(x / 1000, dt.UTC) for x in f.t]) dow = np.array([x.weekday() for x in d]) hour = np.array([x.hour for x in d]) wk = np.array([x.isocalendar()[0] * 100 + x.isocalendar()[1] for x in d]) ent_mask = (dow == entry_dow) & (hour == entry_hour) & (np.arange(len(d)) > 300) ent_i = np.nonzero(ent_mask)[0] #--- Last bar of the WEEKDAY part of the week. NOT simply the last bar of the ISO week: #--- the broker week runs to Sunday ~23:00 (weekday 6), and an earlier version of this #--- took that as the "Friday close" - so every trade held straight through the weekend #--- and paid the 2-3 night weekend financing the exit was designed to avoid. It showed #--- up as 6.9 nights on a Monday->Friday SP500 trade, which is arithmetically impossible. #--- Restricting to weekday <= 4 (Mon-Fri) is what actually closes before the weekend. last_of_week = {} for k in range(len(d)): if dow[k] <= 4: last_of_week[wk[k]] = k out = [] for i in ent_i: j = last_of_week.get(wk[i], -1) if j <= i: continue out.append((i, j)) if len(out) < 100: return None I = np.array([a for a, _ in out]); J = np.array([b for _, b in out]) #--- last minute of the exit bar, from the INDEX - see book.Frame.last_i0 si, sj = f.i0[I], f.last_i0(J) nights = (bk.t[sj] // 86400000 - bk.t[si] // 86400000).astype(float) mid = 0.5 * (bk.bo[si] + bk.ao[si]) res = {} for dirn, tag in ((1, 'long'), (-1, 'short')): if dirn > 0: ent, exi = bk.ao[si], bk.bc[sj] else: ent, exi = bk.bo[si], bk.ac[sj] gross = (exi - ent) * dirn / mid * 1e4 net = gross - 2 * COMM_BP - nights * swap_bp_day res[tag] = (gross, net, nights) return res, len(I) if __name__ == '__main__': syms = [s for s in sys.argv[1:] if s in SYMS] or list(SYMS) print("=== 1. THE SCALE EFFECT: available movement vs round-trip cost ===") print(" cost is ~fixed per trade; movement grows with sqrt(time). This is why short") print(f" holds can never clear cost. swap modelled at {SWAP_BP_DAY} bp/night (~7.5%/yr).\n") for sym in syms: sp, rows = scale_table(sym) print(f" {sym} median spread {sp:.2f} bp") print(f" {'hold':>9}{'|move| bp':>11}{'spread':>9}{'comm':>7}{'swap':>8}" f"{'total cost':>12}{'move/cost':>11}") for lab, mv, s_, cm, sw, ct, ratio in rows: print(f" {lab:>9}{mv:>11.1f}{s_:>9.2f}{cm:>7.2f}{sw:>8.2f}{ct:>12.2f}{ratio:>11.1f}") print() print("=== 2. SWING STRUCTURE: enter at a weekday/hour, exit at the FRIDAY CLOSE ===") print(" no weekend financing. Both directions shown - a real edge is an ASYMMETRY,") print(" not just 'the move was bigger than the cost'.\n") print(f" {'sym':>7}{'entry':>16}{'n':>6}{'nights':>8}" f"{'LONG net bp':>13}{'t':>7}{'SHORT net bp':>14}{'t':>7}{'asym bp':>9}") for sym in syms: for dow, dname in ((0, 'Mon'), (2, 'Wed')): for h in (9, 15): r = weekly(sym, dow, h) if r is None: continue res, n = r gl, nl, nights = res['long'] gs, ns, _ = res['short'] se = lambda x: x.std(ddof=1) / np.sqrt(len(x)) asym = 0.5 * (nl.mean() - ns.mean()) print(f" {sym:>7}{dname + ' ' + str(h) + ':00':>16}{n:>6}{nights.mean():>8.1f}" f"{nl.mean():>+13.2f}{nl.mean()/se(nl):>+7.2f}" f"{ns.mean():>+14.2f}{ns.mean()/se(ns):>+7.2f}{asym:>+9.2f}")