- Implemented sqx_audit.py to audit StrategyQuant X trade lists, focusing on performance metrics and cost analysis. - Created sqx_portfolio.py to evaluate portfolio performance based on uncorrelated components and their impact on risk and return. - Developed swing.py to analyze cost ratios across different holding periods and assess swing trading structures. - Introduced test_management.py to investigate the effectiveness of exit rules on random entries and their impact on expectancy.
137 lines
6.5 KiB
Python
137 lines
6.5 KiB
Python
"""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}")
|