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 — the demo is the engine, not a recording of it. That is what a core with no runtime dependencies buys, and it is the reason this is TypeScript.
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);
}
$ tapedeck run strategy.ts --data btcusdt-1h.tape
Modelling caveats
! 139 order(s) had latency shorter than one bar, which
cannot be honoured on bar data and was ignored rather
than invented. Feed tick data for exact latency.
Result
net profit 2,305.40 USDT
total return 2.31%
costs ate 72.3%
The caveats print above the result. In the terminal, in the report and on this site, every time.
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.
Two exchanges
The same 24/72 crossover, the same year of Bitcoin, the same position size — run once against Binance's published fee schedule and once against Coinbase Exchange's. Both figures come out of the run below, not out of a paragraph.
Binance spot · BTCUSDT
1,997.31 USDT
Coinbase Exchange · BTC-USD
-20,687.80 USD
Entry tier on both, taken from each exchange's own table with the date it was read. The tapes are two different books, so this is the cost of a venue on top of its market rather than fees in a vacuum — which is exactly why the engine will not price one exchange's tape with another's fees. Open the Coinbase run.
Paper trading
A strategy runs unchanged in a backtest and against a live exchange feed. Both modes share one synchronous kernel and differ only in who fills the event queue, a file or a socket.
The feed is live; the broker is the simulator. So the hard half of live trading is not here: no order reaches a venue, and there is no acknowledgement, no rejection, no exchange-side partial fill, no rate limit and no reconciliation against a real account. The provider has no concept of a credential, by design — but absent is absent, and this is a live feed rather than live execution.
# 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
Where this sits
Older and larger tools already do this, and some of them run real money. NautilusTrader has a Rust core and adapters for real venues; LEAN brings data and brokerages with it; VectorBT sweeps parameters orders of magnitude faster; backtrader has years of community behind it. If you need to trade real money this quarter, use one of those.
Every modelling assumption is a reported number rather than a footnote: ambiguous bars, latency too short to honour, and what an aggregation left out, printed above the result. That is the subject here, not a feature.
No install, no account, no container. The core has no runtime dependencies and imports nothing from Node, so the page you are on runs the same engine the command line does.
Six packages, an ADR for every decision that could have gone the other way, and a test suite where six tests changed the engine rather than confirming it — the last of them a lookahead bug found by repairing a fixture that could not fail.