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}