"""Audit a StrategyQuant X trade list on its own terms. The question is not "is the equity curve up" - it is up. The questions that decide whether an equity curve is an EDGE are: 1. Is the per-trade expectancy distinguishable from zero, on the OUT-OF-SAMPLE segment alone? SQX searches an enormous number of candidates and reports the survivors, so the in-sample numbers carry no information at all about the next trade. Only OOS does, and only if it was never used for selection. 2. What is the return relative to the risk actually taken, and to the obvious alternative (holding the instrument)? 3. Does it survive the cost that was not charged? SQX models spread; commission and financing have to be checked separately, and this project has already found commission ~= spread. 4. Would it survive the account rules it is meant to be traded under? Nothing here is a criticism of SQX. It is the same protocol every family in research/ was held to. """ import numpy as np, sys, datetime as dt sys.stdout.reconfigure(encoding='utf-8', errors='replace') PATH = ('C:/Users/admin/AppData/Local/Temp/2/claude/' 'c--Users-admin-Documents-Workspaces-Warrior-EA/' 'bac693d9-0f8d-41a4-ab36-865894af56f2/scratchpad/sqx_trades.csv') START_BALANCE = 5000.0 #--- the5ers-style limits the strategy would have to live inside DAILY_LIMIT_PCT, TOTAL_LIMIT_PCT = 4.0, 8.0 #--- cost not charged by the backtest, per side, in index points of an SP500 CFD COMMISSION_PT = 0.30 # ~ typical CFD commission per side expressed in points at 5-6k index SWAP_PT_NIGHT = 0.55 # ~7.5%/yr financing on a 6000 index = ~1.23 pt/night; long side only def load(): rows = [] with open(PATH, encoding='utf-8') as f: head = f.readline().strip().split(';') for line in f: p = line.strip().split(';') if len(p) < 15: continue rows.append(dict(zip(head, p))) for r in rows: r['OpenTime'] = dt.datetime.strptime(r['OpenTime'], '%Y.%m.%d %H:%M:%S') r['CloseTime'] = dt.datetime.strptime(r['CloseTime'], '%Y.%m.%d %H:%M:%S') for k in ('OpenPrice', 'Size', 'ClosePrice', 'PL', 'Balance', 'MAE', 'MFE'): r[k] = float(r[k]) rows.sort(key=lambda r: r['CloseTime']) return rows def tstat(x): x = np.asarray(x, float) if len(x) < 3: return 0.0 return float(x.mean() / max(x.std(ddof=1) / np.sqrt(len(x)), 1e-12)) def block_of(r): return r['Sample'] def segment_stats(rows, label): pl = np.array([r['PL'] for r in rows]) n = len(pl) if n == 0: return wins = pl[pl > 0] loss = pl[pl <= 0] yrs = (rows[-1]['CloseTime'] - rows[0]['OpenTime']).days / 365.25 print(f" {label:<12}{n:>5}{pl.sum():>11.0f}{pl.mean():>9.2f}{tstat(pl):>8.2f}" f"{100.0*len(wins)/n:>8.1f}%{wins.mean() if len(wins) else 0:>9.1f}" f"{loss.mean() if len(loss) else 0:>9.1f}{n/max(yrs,0.01):>8.1f}") def equity_path(rows, start=START_BALANCE): eq = [start] for r in rows: eq.append(eq[-1] + r['PL']) return np.array(eq) def drawdown(eq): peak = np.maximum.accumulate(eq) dd = (peak - eq) / peak * 100.0 return dd.max(), int(np.argmax(dd)) def daily_pl(rows): """P/L grouped by CLOSE date - the closest thing a trade list allows to a daily loss. NOTE this UNDERSTATES the real daily swing: it counts only realised P/L, while a prop daily limit is measured on EQUITY, which moves with open positions too. The MAE column shows how much worse the intraday equity path got.""" d = {} for r in rows: d.setdefault(r['CloseTime'].date(), 0.0) d[r['CloseTime'].date()] += r['PL'] return d if __name__ == '__main__': rows = load() print(f"=== SQX strategy 2.19.106 - {len(rows)} trades, " f"{rows[0]['OpenTime']:%Y-%m-%d} .. {rows[-1]['CloseTime']:%Y-%m-%d} ===\n") print("=== 1. PERFORMANCE BY SAMPLE BLOCK (only OOS carries information) ===") print(f" {'block':<12}{'n':>5}{'total':>11}{'mean':>9}{'t':>8}{'win%':>9}" f"{'avg win':>9}{'avg loss':>9}{'/yr':>8}") for blk in ('IST', 'ISV1', 'OOS1'): segment_stats([r for r in rows if block_of(r) == blk], blk) segment_stats(rows, 'ALL') print() print("=== 2. RETURN vs RISK vs THE OBVIOUS ALTERNATIVE ===") eq = equity_path(rows) yrs = (rows[-1]['CloseTime'] - rows[0]['OpenTime']).days / 365.25 total_ret = (eq[-1] / eq[0] - 1) * 100 cagr = ((eq[-1] / eq[0]) ** (1 / yrs) - 1) * 100 mdd, mdd_i = drawdown(eq) bh = (rows[-1]['ClosePrice'] / rows[0]['OpenPrice'] - 1) * 100 bh_cagr = ((rows[-1]['ClosePrice'] / rows[0]['OpenPrice']) ** (1 / yrs) - 1) * 100 print(f" span {yrs:.1f} years") print(f" balance {eq[0]:.0f} -> {eq[-1]:.0f}") print(f" total return {total_ret:+.1f}% CAGR {cagr:+.2f}%") print(f" max drawdown (closed) {mdd:.2f}% (worst at trade {mdd_i})") print(f" return / maxDD {total_ret/max(mdd,1e-9):.2f}") print(f" SP500 buy & hold {bh:+.1f}% CAGR {bh_cagr:+.2f}% <- same period, no skill") print() print("=== 3. THE COST THAT WAS NOT CHARGED ===") nights = np.array([max((r['CloseTime'].date() - r['OpenTime'].date()).days, 0) for r in rows]) size = np.array([r['Size'] for r in rows]) is_long = np.array([r['Type'] == 'Buy' for r in rows]) comm = 2.0 * COMMISSION_PT * size swap = nights * SWAP_PT_NIGHT * size * is_long # financing charged on longs pl = np.array([r['PL'] for r in rows]) print(f" commission @ {COMMISSION_PT} pt/side total {comm.sum():>9.0f} " f"({comm.mean():.2f}/trade)") print(f" financing @ {SWAP_PT_NIGHT} pt/night total {swap.sum():>9.0f} " f"(mean {nights.mean():.2f} nights, longs only)") net = pl - comm - swap print(f" gross profit {pl.sum():>9.0f} mean {pl.mean():+.2f} t {tstat(pl):+.2f}") print(f" NET of commission + financing {net.sum():>9.0f} mean {net.mean():+.2f} t {tstat(net):+.2f}") oos = np.array([block_of(r) == 'OOS1' for r in rows]) print(f" NET, OOS ONLY {net[oos].sum():>9.0f} mean {net[oos].mean():+.2f} " f"t {tstat(net[oos]):+.2f} (n={oos.sum()})") eqn = np.concatenate([[START_BALANCE], START_BALANCE + np.cumsum(net)]) mddn, _ = drawdown(eqn) cagrn = ((eqn[-1] / eqn[0]) ** (1 / yrs) - 1) * 100 print(f" net CAGR {cagrn:+.2f}% net maxDD {mddn:.2f}% net final {eqn[-1]:.0f}") print() print("=== 4. WOULD IT SURVIVE THE ACCOUNT RULES? ===") dl = daily_pl(rows) worst = sorted(dl.items(), key=lambda kv: kv[1])[:5] #--- daily loss as a % of the balance at the START of that day bal_by_date = {} run = START_BALANCE for r in rows: run += r['PL'] bal_by_date[r['CloseTime'].date()] = run print(f" worst realised days (limit {DAILY_LIMIT_PCT}% of ~5-7k = 200-280):") for d, v in worst: b = bal_by_date.get(d, START_BALANCE) print(f" {d} {v:+8.2f} = {100.0*v/max(b,1):+.2f}% of balance") print(f" max closed-equity drawdown {mdd:.2f}% vs {TOTAL_LIMIT_PCT}% limit -> " f"{'BREACH' if mdd > TOTAL_LIMIT_PCT else 'ok'}") #--- the intraday path is worse than the closed curve: add each trade's MAE at its worst point mae = np.array([r['MAE'] for r in rows]) eq_open = eq[:-1] + mae # equity if every trade sat at its own MAE peak = np.maximum.accumulate(eq[:-1]) dd_open = ((peak - eq_open) / peak * 100.0).max() print(f" max drawdown INCLUDING open-trade excursion {dd_open:.2f}% vs {TOTAL_LIMIT_PCT}% " f"-> {'BREACH' if dd_open > TOTAL_LIMIT_PCT else 'ok'}") print() print("=== 5. FILL-MODEL RED FLAGS (this project's own bug class) ===") zero = [r for r in rows if r['TimeInTrade'] == '0s'] print(f" trades opened AND closed inside one bar ('0s'): {len(zero)} of {len(rows)}") print(" every one is a loss:", all(r['PL'] < 0 for r in zero)) print(" -> entry is a LIMIT order and the SL is hit in the same H1 bar. Whether that is") print(" possible at all depends on the intrabar path, which an H1 backtest does not have.") worse = [r for r in rows if r['MAE'] < r['PL'] - 1e-9 and r['CloseType'] == 'SL'] print(f" SL trades whose MAE is WORSE than the realised loss: {len(worse)} of " f"{sum(1 for r in rows if r['CloseType']=='SL')}") ratio = np.array([r['MAE'] / r['PL'] for r in worse]) print(f" mean MAE/loss ratio {ratio.mean():.2f}x (max {ratio.max():.2f}x)") print(" -> price traded well beyond the stop but the fill was booked AT the stop.") print(" That is zero slippage on the exit, on a gapping index CFD.")