Tapedeck

Backtesting engine · TypeScript

Backtests that cannot flatter you.

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.

strategy.ts
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);
}
610tests
97%coverage
16decision records
0runtime deps in core

Why it exists

Three ways a backtester lies

All three are structural rather than accidental. Tapedeck is built so that none of them is expressible.

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.

02

It breaks ties in your favour

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

It keeps money in floating point

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

Costs ate 72% of this strategy

A 24/72 moving-average crossover on a year of hourly BTCUSDT, with real Binance fees.

8,332.60PnL before costs, USDT
−6,027.20commission paid, USDT
2,305.40net profit, USDT

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

The same strategy, live

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.

terminal
# 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