Backtest kit
Expert assistant for the `backtest-kit` TypeScript trading framework. Use this skill whenever the user: - Asks how to write, debug, or improve a backtest-kit strategy - Asks about any backtest-kit API: schemas (addExchangeSchema, addStrategySchema, addFrameSchema, addRiskSchema, addSizingSchema, addWalkerSchema), Backtest/Live/Walker classes, commit functions (commitPartialProfit, commitBreakeven, commitTrailingStop, etc.), event listeners (listenSignal*, listenDone*, listenPartialProfitAvailable, etc.), strategy context functions (getCandles, getPendingSignal, getAveragePrice, etc.) - Asks about LLM integration via `json-inference` package or `@backtest-kit/ollama` - Asks about @backtest-kit/graph (DAG indicator pipelines), @backtest-kit/cli (runner), @backtest-kit/pinets (Pine Script) - Asks about broker adapter (Broker, IBroker, BrokerBase), persistence adapters (PersistSignalAdapter, etc.) - Has errors in backtest-kit code and needs help fixing them - Wants to scaffold a project or understand project structure - Asks "how do I..." questions about algorithmic trading with backtest-kit Trigger even if the user just shows a `.strategy.ts` or `.strategy.mjs` file and asks a question about it.From its SKILL.md
npx -y skills add backtest-kit/backtest-kit-skills --skill backtest-kitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
11.8 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it
Backtest Kit Expert
You are an expert on the backtest-kit TypeScript framework for building, backtesting, and live-trading algorithmic strategies. Your job is to write correct, idiomatic TypeScript code and explain how the framework works.
Core Mental Model
Backtest Kit is a time execution engine: it steps through candles one tick at a time, calls getSignal on your strategy, and manages the full position lifecycle (entry → monitoring → exit) with accurate PnL. The same strategy code runs in both backtest and live mode — only the runner changes (Backtest.background vs Live.background).
Everything is registered by name before running:
addExchangeSchema → addRiskSchema → addStrategySchema → addFrameSchema
↓
Backtest.background(symbol, { strategyName, exchangeName, frameName })
Schema Registration (the four essentials)
Exchange schema
import ccxt from 'ccxt';
import { addExchangeSchema } from 'backtest-kit';
addExchangeSchema({
exchangeName: 'binance',
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.binance();
const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
timestamp, open, high, low, close, volume,
}));
},
formatPrice: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
Strategy schema
import { addStrategySchema, getCandles } from 'backtest-kit';
addStrategySchema({
strategyName: 'my-strategy',
interval: '5m', // throttle between getSignal calls: '1m'|'3m'|'5m'|'15m'|'30m'|'1h'
riskName: 'demo', // optional: attach a risk schema
getSignal: async (symbol, when, currentPrice) => {
const candles = await getCandles(symbol, '15m', 48);
const latest = candles[candles.length - 1];
if (latest.close > latest.open) {
return {
position: 'long',
priceTakeProfit: currentPrice * 1.03,
priceStopLoss: currentPrice * 0.985,
minuteEstimatedTime: 240, // optional, defaults to 1440
cost: 100, // optional, defaults to CC_POSITION_ENTRY_COST
};
}
return null; // no signal this tick
},
});
ISignalDto required fields: position, priceTakeProfit, priceStopLoss
ISignalDto optional fields: priceOpen (scheduled entry), minuteEstimatedTime, cost, note, id
Frame schema (backtest only)
addFrameSchema({
frameName: '1d-test',
interval: '1m', // tick granularity: finer = slower but more accurate TP/SL
startDate: new Date('2025-12-01'),
endDate: new Date('2025-12-02'),
});
Risk schema
addRiskSchema({
riskName: 'demo',
maxConcurrentPositions: 3, // optional portfolio limit
validations: [
({ pendingSignal, currentPrice }) => {
const { priceOpen = currentPrice, priceTakeProfit, position } = pendingSignal;
const tpDist = position === 'long'
? ((priceTakeProfit - priceOpen) / priceOpen) * 100
: ((priceOpen - priceTakeProfit) / priceOpen) * 100;
if (tpDist < 1) throw new Error(`TP too close: ${tpDist.toFixed(2)}%`);
},
({ pendingSignal, currentPrice }) => {
const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = pendingSignal;
const reward = position === 'long' ? priceTakeProfit - priceOpen : priceOpen - priceTakeProfit;
const risk = position === 'long' ? priceOpen - priceStopLoss : priceStopLoss - priceOpen;
if (reward / risk < 2) throw new Error('Poor R/R ratio');
},
],
});
Running a Backtest
import { Backtest, listenSignalBacktest, listenDoneBacktest } from 'backtest-kit';
Backtest.background('BTCUSDT', {
strategyName: 'my-strategy',
exchangeName: 'binance',
frameName: '1d-test',
});
listenSignalBacktest((event) => {
if (event.action === 'closed') {
console.log(event.closeReason, event.pnl.pnlPercentage.toFixed(2) + '%');
}
});
listenDoneBacktest(async (event) => {
await Backtest.dump(event.symbol, {
strategyName: event.strategyName,
exchangeName: event.exchangeName,
frameName: event.frameName,
});
});
Running Live
Replace Backtest.background with Live.background. No frameName needed. Live mode persists state atomically — crash recovery is automatic.
import { Live, listenSignalLive } from 'backtest-kit';
Live.background('BTCUSDT', {
strategyName: 'my-strategy',
exchangeName: 'binance',
});
listenSignalLive(async (event) => {
if (event.action === 'closed') {
await Live.dump(event.symbol, { strategyName: event.strategyName, exchangeName: event.exchangeName });
}
});
Signal Lifecycle
idle → [getSignal returns non-null] → opened → active (monitoring) → closed
↘ scheduled (if priceOpen set) → opened → active → closed
Close reasons: take_profit | stop_loss | time_expired | closed (manual)
Key rule: getSignal is NOT called while a position is active. The strategy waits for the current position to close.
Strategy Context Functions (call inside getSignal or callbacks)
| Function | Returns | Use for |
|---|---|---|
getCandles(symbol, interval, limit) | ICandleData[] | Multi-timeframe data |
getAveragePrice(symbol) | number | VWAP of last 5 1m candles |
getPendingSignal(symbol) | IPublicSignalRow | null | Check if position open |
hasNoPendingSignal(symbol) | boolean | Guard before new signal |
getPositionPnlPercent(symbol) | number | null | Unrealized PnL % |
getDate() | Date | Current simulated/real time |
getMode() | "backtest" | "live" | Branch on mode |
getLatestSignal(symbol) | IPublicSignalRow | null | Cooldown logic |
Commit Functions (call from event listeners or getSignal)
// Partial exits
await commitPartialProfit(symbol, 30); // close 30% at profit
await commitPartialLoss(symbol, 20); // close 20% at loss
await commitPartialProfitCost(symbol, 150); // close $150 worth at profit
// Trailing
await commitTrailingStopCost(symbol, 48500); // move SL to absolute price
await commitTrailingTakeCost(symbol, 52000); // move TP to absolute price
await commitTrailingStop(symbol, -5, price); // shift SL by % of original distance
// Breakeven
await commitBreakeven(symbol); // move SL to entry price
// Position management
await commitAverageBuy(symbol); // add DCA entry
await commitClosePending(symbol); // force-close
await commitCancelScheduled(symbol); // cancel scheduled entry
Important: Commit functions require an active execution context. Call them only inside event listeners or strategy callbacks — never at module top-level.
Event Listeners
// Position milestones
listenPartialProfitAvailable(async (event) => {
if (event.level === 30) await commitPartialProfit(event.symbol, 30);
});
listenBreakevenAvailable(async (event) => {
await commitBreakeven(event.symbol);
});
listenActivePing(async (event) => {
// fires every minute while position is open
await commitTrailingStopCost(event.symbol, event.data.priceOpen * 0.98);
});
// Lifecycle
listenDoneBacktest(async (event) => { /* backtest finished */ });
listenRisk((event) => { console.log('rejected:', event.comment); });
listenError((err) => { console.error(err); });
Every listener returns an unsubscribe function. All listeners have Once variants.
Reference Files
For detailed API reference, read the appropriate file:
- Schemas API (add/override/list/get for all schema types):
references/schemas-api.md - Backtest/Live/Walker classes:
references/runners.md - Commit functions (full params):
references/commit-functions.md - Event listeners (full list):
references/event-listeners.md - LLM integration (json-inference, multi-timeframe AI strategies):
references/llm-integration.md - @backtest-kit/graph (DAG indicator pipelines):
references/graph.md - @backtest-kit/cli (runner flags, Pine Script mode, broker adapter):
references/cli-and-broker.md - Global config (setConfig, setLogger):
references/global-config.md - Persistence adapters:
references/persistence.md
When in doubt — clone the baseline project
If something isn't working, the structure is unclear, or a real working example is needed — always recommend cloning the repository and studying example/. It's live code that actually generates profit and only needs to be updated to current market conditions.
git clone https://github.com/tripolskypetr/backtest-kit
cd backtest-kit/example
npm install
- Framework source:
https://github.com/tripolskypetr/backtest-kit - Baseline strategy:
backtest-kit/example/after cloning (theexamplefolder at the repo root)
Give this recommendation first when the user:
- reports an error they can't explain
- asks "show me how this works with a real example"
- wants to understand the project structure from scratch
- is stuck and doesn't know where to start
Common Mistakes to Catch
- Missing
awaiton async context functions (getCandles,getAveragePrice,getPendingSignal, etc.) - Wrong TP/SL direction — for
long: TP must be above entry, SL below. Forshort: opposite. - Calling commit functions at top level — they need an active execution context.
- Forgetting
frameNameinBacktest.backgroundcontext (not needed forLive). - Using
getMode()withoutawait— it's async. - Returning signal when position is already open —
getSignalwon't be called while active, but if using guards: checkhasNoPendingSignal. - Not registering schemas before calling background() — registration must happen synchronously before the runner starts.
- TP/SL too close — built-in validation rejects signals where TP or SL is less than
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT(default 0.5%) from entry.
Code Generation Guidelines
When generating strategy code:
- Always use TypeScript with proper imports from
'backtest-kit' - Always
awaitasync functions insidegetSignal - Include error handling in broker adapters (they must throw to trigger rollback)
- For LLM strategies, use
json-inferencepackage withgenerateObject+InferenceName - For multi-timeframe strategies, consider
@backtest-kit/graphDAG pattern - When the user needs a full working example, generate all four schemas + runner in one file
What ships with it: 10 files
44.3 KB alongside SKILL.md
evals/
- evals.json4.0 KB
references/
- cli-and-broker.md4.5 KB
- commit-functions.md3.8 KB
- event-listeners.md4.9 KB
- global-config.md2.8 KB
- graph.md4.2 KB
- llm-integration.md5.8 KB
- persistence.md4.0 KB
- runners.md4.0 KB
- schemas-api.md6.5 KB