Backtest Kit
Engine for live-trading and backtesting strategies with clean architecture and real-time execution capabilities.
Install / Use
npx skills add tripolskypetr/backtest-kitInstalls into whichever agent you are using.
README
🧿 Backtest Kit
A TypeScript engine for backtesting and live-trading strategies — crypto, forex, DEX, spot or futures — where the code you test is the code you ship. See reference implementation

Most trading bots don't die because the strategy was wrong. They die because the backtest quietly read tomorrow's candle, because the process crashed mid-fill and opened the position twice, because the exchange rejected an order and the bot kept trading a ghost. The strategy was never the hard part — the infrastructure was.
backtest-kit is that infrastructure, closed off one failure at a time over a year of live trading and running real money in production at TheOneTrade. This page walks the failures that kill bots and shows how each one is designed out of the default path — not "discouraged," not "documented," but structurally unavailable unless you go out of your way to defeat the engine. Every claim opens into The Code / The Math / The Proof so you (or the model reading this for you) can check the work instead of trusting the pitch.
📚 API Reference · 🌟 Reference implementation · 📰 Article series
Start here
Three on-ramps, one engine. Casual keeps the boilerplate inside the CLI; Sidekick ejects every wire into your repo; Docker gives you a restart-safe box.
<details> <summary>The Code</summary># Casual — your repo holds only strategy files; docs auto-fetched into docs/lib/
npx @backtest-kit/cli --init --output backtest-kit-project
cd backtest-kit-project && npm install && npm start
# Full control — exchange/frames/risk/runner all editable in your project
npx -y @backtest-kit/sidekick my-trading-bot && cd my-trading-bot && npm start
# Docker — zero-downtime live trading
npx @backtest-kit/cli --docker && cd backtest-kit-docker
MODE=live SYMBOL=TRXUSDT STRATEGY_FILE=./content/feb_2026/feb_2026.strategy.ts docker-compose up -d
A whole strategy is three registrations and a run call. No bootstrap, no DI container to learn:
import ccxt from 'ccxt';
import { addExchangeSchema, addStrategySchema, addFrameSchema, Position,
Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';
addExchangeSchema({
exchangeName: 'binance',
getCandles: async (symbol, interval, since, limit) => {
const ex = new ccxt.binance();
const ohlcv = await ex.fetchOHLCV(symbol, interval, since.getTime(), limit);
return ohlcv.map(([timestamp, open, high, low, close, volume]) =>
({ timestamp, open, high, low, close, volume }));
},
formatPrice: (s, p) => p.toFixed(2), formatQuantity: (s, q) => q.toFixed(8),
});
addFrameSchema({ frameName: 'feb-2026', interval: '1m',
startDate: new Date('2026-02-01'), endDate: new Date('2026-02-28') });
addStrategySchema({
strategyName: 'my-strategy', interval: '15m',
getSignal: async (symbol, when, currentPrice) => ({
position: 'long',
...Position.bracket({ position: 'long', currentPrice, percentTakeProfit: 2, percentStopLoss: 1 }),
minuteEstimatedTime: 60 * 24, cost: 100,
}),
});
Backtest.background('BTCUSDT', { strategyName: 'my-strategy', exchangeName: 'binance', frameName: 'feb-2026' });
listenSignalBacktest(console.log);
listenDoneBacktest(async (e) => { await Backtest.dump(e.symbol, e.strategyName); });
</details>
The rakes — and where they went
What follows isn't a feature list. It's the set of mistakes that quietly drain accounts, each one paired with the design decision that took it off the table. If you've shipped a bot before, you've stepped on at least three of these.
1. Your backtest lied to you, and you'll only find out with real money
Look-ahead bias is the assassin of algo trading: a single line that touches a future candle, an indicator loaded without a timestamp filter, one forgotten <=. The backtest prints a beautiful equity curve that can never be reproduced live, and you deploy straight into a drawdown.
The usual defense is "be careful." Careful doesn't survive a 2,000-line strategy or a refactor at 1 a.m. So the cure here isn't discipline — it's removal of the failure surface. There is no timestamp parameter to forget. An ambient temporal context flows through every async call via Node's AsyncLocalStorage, and the data layer physically refuses to hand you a candle past "now." The pending (still-forming) candle is never returned, because its half-finished OHLC would poison every indicator.
The one rule this rests on: that context is live for the whole await chain of your getSignal and every listen* callback — including across Promise.all, which is where strategy code actually runs. It is not sorcery over execution you deliberately detach from that chain. A bare timer, an EventEmitter, a forked process, or the web dashboard reads engine state by identifier (signal id / symbol), not by inheriting the ambient clock — that explicit, id-based interop is exactly how the frontend talks to a running backtest. Inside the hooks the guarantee holds; step outside them on purpose and you address the engine deliberately rather than by accident.
Every request resolves "now" from the ambient context, aligns down to the interval boundary, and treats the pending candle as exclusive:
when = current execution-context time (AsyncLocalStorage)
stepMs = interval duration (1m → 60000)
alignedWhen = Math.floor(when / stepMs) * stepMs // round down to boundary
since = alignedWhen − limit * stepMs // go back `limit` candles
sinceis inclusive — first candle hastimestamp === since.alignedWhenis exclusive — the candle covering[alignedWhen, alignedWhen+stepMs)is still open and is never returned.- Range is the half-open
[since, alignedWhen); exactlylimitcandles return; timestamps aresince + i·stepMs.
getNextCandles() is backtest-only and throws in live mode — there is no future to look at when "now" is wall-clock. getRawCandles(limit?, sDate?, eDate?) supports flexible windows, all clamped to eDate ≤ when. Order books and aggregated trades use the same alignment (trades always to a 1-minute boundary). All boundaries are UTC: a 4h candle aligns to 00/04/08/12/16/20 UTC regardless of your local offset — so since values that look "uneven" in local time are exact in UTC. Because since is derived from the ambient when, multi-timeframe pulls inside one getSignal are automatically synchronized, and runtime and the persistent cache compute identical keys — deterministic, exact-timestamp retrieval.
getSignal: async (symbol) => {
// No timestamps anywhere. Context flows even through Promise.all —
// all four timeframes are pinned to the same tick automatically.
const [c1h, c15m, c5m, c1m] = await Promise.all([
getCandles(symbol, '1h', 24),
getCandles(symbol, '15m', 48),
getCandles(symbol, '5m', 60),
getCandles(symbol, '1m', 60),
]);
}
The bias you can't introduce by hand is the bias you'll never debug in production.
</details>2. "It worked in the backtest" means nothing if live runs different code
The standard path productionizes a strategy by rewriting it: the research notebook becomes a second, hand-built live system with its own order logic, its own bugs, its own divergence. Now you have two strategies that look identical and behave differently exactly when it matters.
Here there is one code path. The getSignal you backtested is the getSignal that trades. Backtest mode feeds it historical timestamps; live mode feeds it Date.now(). The business logic — entries, validation, scheduled activation, TP/SL/timeout, partial closes — is byte-for-byte the same in both. The only differences are infrastructural: where the data comes from, not what you do with it.
// Backtest — a historical frame drives the clock
Backtest.background('BTCUSDT', { strategyName, exchangeName, frameName });
// Live — wall-clock drives the clock; the strategy file is untouched
Live.background('BTCUSDT', { strategyName, exchangeName }); // keys via .env
listenSignalLive(async (e) => { if (e.action === 'closed') await Live.dump(e.symbol, e.strategyName); });
// Paper — live prices, no real orders, identical path. Validate here before risking capital.
And one engine, two ways to consume it — pick by use case, not by capability:
// Event-driven (production bots, monitoring)
Backtest.background('BTCUSDT', config);
listenSignalBacktest(e => {/* … */});
// Async iterator (research, scripts, LLM agents)
for await (const event of Backtest.run('BTCUSDT', config)) { /* signal | progress | done */ }
</details>
<details>
<summary>The Proof</summary>
This is the property the test suite exists to defend, and the line
Related Skills
node-connect
385.6kDiagnose OpenClaw Android, iOS, or macOS node pairing, QR/setup code, route, auth, and connection failures.
blender-python-addon
40.5kBlender Python add-on rules for operators, panels, properties, registration, testing, and API-safe scripting
flutter-development-guidelines-cursorrules-prompt-file
40.5kCursor rules for Flutter development with MVVM architecture, Riverpod state management, Material widgets, and Dart style guidelines.
commit-push-pr
140.7kCommit, push, and open a PR
