01
It acts on what it did not know
A bar arrives, the strategy reads its close, and the engine fills the resulting order at that same close.
An order submitted while processing a bar can never match against that bar.
Backtesting engine · TypeScript
An event-driven backtesting and paper-trading engine for TypeScript. No lookahead, money in fixed-point integers, and every assumption the run had to make printed above the result instead of under it.
8,760 hourly bars replay in about 50 ms, in your tab, on the same kernel the CLI runs. Nothing is uploaded.
onBar(bar, ctx) {
const fast = fastSma.value;
const slow = slowSma.value;
if (fast === null || slow === null) return;
const side = fast > slow ? 1 : fast < slow ? -1 : previous;
if (side === previous) return;
previous = side;
// Order the difference to a target position.
// Positions are the truth; flags drift.
rebalance(ctx, side > 0 ? qty : -qty);
}
Why it exists
All three are structural rather than accidental. Tapedeck is built so that none of them is expressible.
01
A bar arrives, the strategy reads its close, and the engine fills the resulting order at that same close.
An order submitted while processing a bar can never match against that bar.
02
When a stop and a target both sit inside one bar's range, the engine quietly picks one, and it is rarely the stop.
The pessimistic branch wins, and the bar is counted in
stats.ambiguousBars.
03
Across a few hundred thousand fills, the reported PnL stops reconciling with the sum of the trades.
Fixed-point integers end to end, with a cost basis rather than an average price.
What that buys you
A 24/72 moving-average crossover on a year of hourly BTCUSDT, with real Binance fees.
A backtester that skipped fees would have reported a strategy three and a half times better than the one that exists. Set the costs to none in the demo and watch it happen.
Paper trading
A strategy runs unchanged in backtest and in paper trading against a live exchange feed. Not a compatibility layer: both modes share one synchronous kernel, and the only real difference is who fills the event queue, a file or a socket.
# the committed year of hourly BTCUSDT, start to finish
$ tapedeck run strategy.ts --data btcusdt-1h.tape
# the same file, against the live Binance socket
$ tapedeck paper strategy.ts --venue binance --symbol BTCUSDT