code wiki / (root) / nx_fin_backtest.nx

nx_fin_backtest.nx source

↩ module page · 103 lines · 5421 B

1// nx_fin_backtest.nx -- R2 of the trading stack: the TRUTH ENGINE. Given OHLCV bars (from nx_fin_marketdata) + 2// a pre-computed long/flat SIGNAL per bar (produced by an R3 strategy from data up to that bar -> NO look-ahead 3// by construction), simulate the trades point-in-time with realistic COST (slippage_bps + commission) and a 4// LIQUIDITY cap (md_capacity of the name's ADV), mark-to-market every bar, and emit the HONEST risk metrics: 5// expectancy, win-rate, profit-factor, MAX DRAWDOWN, return. This is the machine that proves or KILLS an edge 6// before a dollar is risked -- the antidote to the "20-30%/trade, no losses" fantasy. Pure, deterministic, 7// i64 fixed-point (cents / basis-points), NO floats. Composes R1 (md_capacity, MD_FIELDS). Single-responsibility: 8// it SIMULATES a signal; it does not GENERATE one (that is R3) and does not SIZE a portfolio (that is R4). 9// license_tier: ORIGINAL 10import "nx_syscalls.nx" 11import "nx_fin_marketdata.nx" 12const BT_MAGIC_10000: i64 = 10000 13const BT_MAGIC_1000000000: i64 = 1000000000 14 15// out[] metric slots 16const BT_TRADES: i64 = 0 17const BT_WINS: i64 = 1 18const BT_LOSSES: i64 = 2 19const BT_GROSS_WIN: i64 = 3 // cents 20const BT_GROSS_LOSS:i64 = 4 // cents (positive) 21const BT_NET: i64 = 5 // cents 22const BT_EXPECTANCY:i64 = 6 // cents per trade 23const BT_WINRATE: i64 = 7 // basis points 24const BT_PF: i64 = 8 // profit factor, basis points (10000 = 1.0x) 25const BT_MAXDD: i64 = 9 // basis points (peak-to-trough of marked equity) 26const BT_RET: i64 = 10 // basis points (total return on start equity) 27const BT_FINAL_EQ: i64 = 11 // cents 28 29// shares to buy: all available cash, capped by the ADV participation limit (the SCALING WALL). 30// adv_cents<=0 disables the cap (cash-bound). Composes R1's md_capacity. 31func bt_position_size(cash_cents: i64, price_cents: i64, adv_cents: i64, participation_bps: i64) -> i64 { 32 if price_cents <= 0 { return 0 } 33 var shares: i64 = cash_cents / price_cents 34 if adv_cents > 0 { 35 let cap_shares: i64 = md_capacity(adv_cents, participation_bps) / price_cents 36 if cap_shares < shares { shares = cap_shares } 37 } 38 return shares 39} 40 41// Run the backtest. signals[i] in {0,1} = desired LONG/flat during bar i (decided from data <= i). Returns 42// n_trades; fills out[0..11] with the metrics above. 43func bt_run(bars: *i64, nbars: i64, signals: *i64, start_equity: i64, 44 slippage_bps: i64, commission_cents: i64, adv_cents: i64, participation_bps: i64, out: *i64) -> i64 { 45 var cash: i64 = start_equity 46 var pos: i64 = 0 47 var entry: i64 = 0 48 var peak: i64 = start_equity 49 var maxdd: i64 = 0 50 var ntr: i64 = 0; var nw: i64 = 0; var nl: i64 = 0 51 var gwin: i64 = 0; var gloss: i64 = 0 52 var i: i64 = 0 53 while i < nbars { 54 let close: i64 = bars[i*MD_FIELDS+4] 55 let sig: i64 = signals[i] 56 if sig == 1 { 57 if pos == 0 { // ENTER long at close + slippage 58 let buy: i64 = close + (close*slippage_bps)/BT_MAGIC_10000 59 let shares: i64 = bt_position_size(cash, buy, adv_cents, participation_bps) 60 if shares > 0 { 61 cash = cash - shares*buy - commission_cents 62 pos = shares; entry = buy 63 } 64 } 65 } else { 66 if pos > 0 { // EXIT at close - slippage 67 let sell: i64 = close - (close*slippage_bps)/BT_MAGIC_10000 68 let proceeds: i64 = pos*sell - commission_cents 69 let pnl: i64 = proceeds - pos*entry 70 cash = cash + proceeds 71 ntr = ntr + 1 72 if pnl >= 0 { nw = nw + 1; gwin = gwin + pnl } else { nl = nl + 1; gloss = gloss + (0 - pnl) } 73 pos = 0; entry = 0 74 } 75 } 76 var equity: i64 = cash 77 if pos > 0 { equity = cash + pos*close } // mark-to-market (unrealized counts for DD) 78 if equity > peak { peak = equity } 79 if peak > 0 { let dd: i64 = (peak - equity)*BT_MAGIC_10000/peak; if dd > maxdd { maxdd = dd } } 80 i = i + 1 81 } 82 if pos > 0 { // close any open position at the last bar 83 let close: i64 = bars[(nbars-1)*MD_FIELDS+4] 84 let sell: i64 = close - (close*slippage_bps)/BT_MAGIC_10000 85 let proceeds: i64 = pos*sell - commission_cents 86 let pnl: i64 = proceeds - pos*entry 87 cash = cash + proceeds 88 ntr = ntr + 1 89 if pnl >= 0 { nw = nw + 1; gwin = gwin + pnl } else { nl = nl + 1; gloss = gloss + (0 - pnl) } 90 pos = 0 91 } 92 let net: i64 = gwin - gloss 93 var expc: i64 = 0; if ntr > 0 { expc = net / ntr } 94 var wr: i64 = 0; if ntr > 0 { wr = nw*BT_MAGIC_10000/ntr } 95 var pf: i64 = 0 96 if gloss > 0 { pf = gwin*BT_MAGIC_10000/gloss } else { if gwin > 0 { pf = BT_MAGIC_1000000000 } } // no losses -> "infinite" sentinel 97 var ret: i64 = 0; if start_equity > 0 { ret = (cash - start_equity)*BT_MAGIC_10000/start_equity } 98 out[BT_TRADES]=ntr; out[BT_WINS]=nw; out[BT_LOSSES]=nl 99 out[BT_GROSS_WIN]=gwin; out[BT_GROSS_LOSS]=gloss; out[BT_NET]=net 100 out[BT_EXPECTANCY]=expc; out[BT_WINRATE]=wr; out[BT_PF]=pf 101 out[BT_MAXDD]=maxdd; out[BT_RET]=ret; out[BT_FINAL_EQ]=cash 102 return ntr 103}