Backtest components¶
const { BacktestRunner, SimulatedExecutor, BacktestResult, Engine, SignalBuilder } = require('@lrvx/lrvx');
BacktestRunner¶
Replays OHLCV data through a strategy. Emitted orders go to a SimulatedExecutor; statistics are returned at the end.
const bt = new lrvx.BacktestRunner(registry, feeRate, initialCapital);
bt.setStrategy(strategy);
const stats = bt.runCsv('/path/to/data.csv', 'BTCUSDT');
const stats = bt.runOhlcv(timestampsNs, closePrices, 'BTCUSDT');
| Method | Returns | Description |
|---|---|---|
setStrategy(strategy) |
void |
Attach a strategy |
runCsv(path, symbol) |
stats object | Replay a CSV file (timestamp, open, high, low, close, volume) |
runOhlcv(timestamps, closes, symbol) |
stats object | Replay raw arrays (timestamps as Float64Array, closes as Float64Array) |
runTape(path) |
stats object | Replay a .lrvx tape directory — the canonical recording written by lrvx tape record or scripts/backfill_to_tape.py |
runTapes(paths) |
stats object | Merge N .lrvx tape directories on read and replay the merged stream. Symbols are rekeyed by (metadata.exchange, name) so two venues stay distinct. runTapes([t]) equals runTape(t); throws on bad paths or overlapping book streams |
setExecutor(executor) |
void |
Replace the built-in SimulatedExecutor. null detaches |
addExecutionListener(listener) |
void |
Attach an order-lifecycle listener. Multiple listeners may be attached; each fires for every order event |
equityCurve() |
EquityCurve |
Equity curve from the most recent run |
trades() |
BacktestTrades |
Closed trades from the most recent run |
equityCurve() and trades() throw LrvxError(E_RUN_002) if no run has completed.
Stats object¶
| Key | Type | Description |
|---|---|---|
totalTrades |
number |
Round-trip trade count |
winningTrades |
number |
Winning trades |
losingTrades |
number |
Losing trades |
initialCapital |
number |
Starting capital |
finalCapital |
number |
Ending capital |
netPnl |
number |
Net P&L after fees |
totalPnl |
number |
Gross P&L |
totalFees |
number |
Total fees paid |
grossProfit |
number |
Sum of winning trades |
grossLoss |
number |
Sum of losing trades |
maxDrawdown |
number |
Max drawdown (absolute) |
maxDrawdownPct |
number |
Max drawdown (%) |
winRate |
number |
Winning trade ratio |
profitFactor |
number |
Gross profit / gross loss |
avgWin |
number |
Average winning trade |
avgLoss |
number |
Average losing trade |
sharpeRatio |
number |
Annualized Sharpe ratio |
sortinoRatio |
number |
Sortino ratio |
calmarRatio |
number |
Calmar ratio |
returnPct |
number |
Net return (%) |
SimulatedExecutor¶
Lower-level backtest executor. Use directly when you need more control over fill logic than BacktestRunner provides.
const exec = new lrvx.SimulatedExecutor();
exec.setDefaultSlippage('fixed_bps', 0, 0, 2.0, 0);
exec.setQueueModel('tob', 1);
| Method / Property | Description |
|---|---|
submitOrder(id, side, price, qty, type, symbol, opts?) |
Submit an order (side: "buy"/"sell") |
cancelOrder(orderId) |
Cancel an order |
cancelAll(symbol) |
Cancel all orders for a symbol |
onBar(symbol, closePrice) |
Feed a bar close |
onBarOhlc(symbol, open, high, low, close) |
Feed a full OHLC bar: the manual bar path (see below) |
beginBarCallbackWindow() / endBarCallbackWindow() |
Hold orders submitted between the two calls until the next bar's open |
reset() |
Drop fills and run-scoped state, keeping installed configuration |
onTrade(symbol, price, isBuy) |
Feed a trade |
advanceClock(timestampNs) |
Advance simulated time |
setDefaultSlippage(model, ticks, tickSize, bps, impactCoeff) |
Configure slippage. model is one of "none", "fixed_ticks", "fixed_bps", "volume_impact" |
setQueueModel(model, depth) |
Configure limit order queue. model is one of "none", "tob", "full", "pro_rata", "pro_rata_with_fifo", "top_pro_lmm", "pro_rata_with_priority" |
fillCount |
Number of fills (property) |
submitOrder's type is one of "market", "limit", "stop_market",
"stop_limit", "trailing_stop". The optional seventh argument carries
{ tif?: 'gtc' | 'ioc' | 'fok' | 'gtd' | 'post_only', reduceOnly?: boolean, expiresAtNs?: number }.
The manual bar path¶
onBar moves the market straight to a bar's close, so a caller driving
SimulatedExecutor by hand never sees the bar's open or its intrabar
extremes — a held order would match at a price that only exists because the
bar has already happened. onBarOhlc is what BacktestRunner.runBars uses
internally: it moves the market to the open first (releasing any order held
from a beginBarCallbackWindow/endBarCallbackWindow pair at that price),
then walks low -> high -> close.
const exec = new lrvx.SimulatedExecutor();
exec.advanceClock(60_000_000_000n);
exec.onBarOhlc(1, 50000.0, 50500.0, 49800.0, 50200.0);
exec.beginBarCallbackWindow();
exec.submitOrder(1, 'buy', 0.0, 1.0, 'market', 1);
exec.endBarCallbackWindow();
// The order above is held, not matched, and releases at the next
// onBarOhlc call's open.
exec.reset(); // drop fills before a second hand-driven run
BacktestResult¶
Computes statistics and equity curve from a SimulatedExecutor's fills.
const result = new lrvx.BacktestResult(initialCapital, feeRate);
result.ingestExecutor(exec);
const stats = result.stats();
| Method | Returns | Description |
|---|---|---|
recordFill(orderId, symbol, side, price, qty, timestampNs) |
void |
Record a single fill |
ingestExecutor(exec) |
void |
Drain all fills from a SimulatedExecutor |
stats() |
stats object | Same fields as BacktestRunner stats |
Engine¶
Bulk backtesting engine. Loads OHLCV data once, then runs strategies against it.
The constructor takes capital and fee rate, not a registry.
const engine = new lrvx.Engine(100_000, 0.0001);
engine.loadCsv('/path/to/btcusdt_1m.csv'); // symbol inferred from the filename
const signals = new lrvx.SignalBuilder();
signals.buy(tsNs, 1.0);
const stats = engine.run(signals);
| Method / Property | Returns | Description |
|---|---|---|
new Engine(initialCapital?, feeRate?) |
Defaults 100000 and 0.0001 (fee is a fraction) | |
loadCsv(path, symbol?) |
void |
Load OHLCV CSV. Without symbol the name is inferred from the file stem, upper-cased, with a _1m / _5m / _15m / _1h / _4h / _1d suffix stripped |
loadOhlcv(bars, symbol?) |
void |
bars is one object {ts, open, high, low, close, volume} of Float64Arrays, not positional arrays. Timestamps in s / ms / us are auto-scaled to ns |
resample(src, dst, interval) |
void |
Three strings, e.g. ('BTCUSDT', 'BTCUSDT_1h', '1h'). Interval is <count><s\|m\|h\|d>. Throws E_SYM_001 if src is not loaded |
run(signals) |
BacktestStats |
Takes a SignalBuilder only. Passing a Strategy aborts |
barCount(symbol?) |
number |
Bars loaded; defaults to the first loaded symbol |
ts(symbol?) |
BigInt64Array |
Bar open times in nanoseconds. Exact int64 — a double steps 256 ns at present-day magnitudes |
open(symbol?) |
Float64Array |
Open prices |
high(symbol?) |
Float64Array |
High prices |
low(symbol?) |
Float64Array |
Low prices |
close(symbol?) |
Float64Array |
Close prices |
volume(symbol?) |
Float64Array |
Volumes |
symbols |
string[] |
Registered symbol names (property) |
run() returns the full BacktestStats shape, not the narrower runner
shape — see Stats object. The key names are the same in
both; only the field set differs.
SignalBuilder¶
Builds a signal array to pass to engine.run().
Signals are keyed by timestamp and carry a quantity — not a bar index.
const signals = new lrvx.SignalBuilder();
signals.buy(tsNs, 1.0) // methods return `this`, so they chain
.limitSell(tsNs, 101.0, 1.0);
const stats = engine.run(signals);
| Method / Property | Description |
|---|---|
buy(tsNs, qty, symbol?) |
Long entry at tsNs. Returns this |
sell(tsNs, qty, symbol?) |
Short entry. Returns this |
limitBuy(tsNs, price, qty, symbol?) |
Limit long entry. Returns this |
limitSell(tsNs, price, qty, symbol?) |
Limit short entry. Returns this |
clear() |
Clear all signals |
length |
Signal count (read-only property) |
tsNs takes a JS number or a bigint, so an Engine.ts() element goes
straight in; values in s / ms / us are auto-scaled to ns.
| length | Number of signals (property) |