Zero-boilerplate CLI for launching backtests, paper trading, and live trading. Run any backtest-kit strategy from the command line — no setup code required.

Point the CLI at your strategy file, choose a mode, and it handles exchange connectivity, candle caching, UI dashboard, and Telegram notifications for you.
📚 Backtest Kit Docs | 🌟 GitHub
npx @backtest-kit/cli --backtest ./strategy.mjs — no boilerplate needed@backtest-kit/ui with a single --ui flag--telegramlive.module.mjs, paper.module.mjs, or backtest.module.mjs to register a Broker adapter. No manual wiring needed.setLogger() from your strategy module@backtest-kit/cli wraps the backtest-kit engine and resolves all scaffolding automatically:
| Mode | Command Line Args | Description |
|---|---|---|
| Backtest | --backtest |
Run strategy on historical candle data |
| Paper | --paper |
Live prices, no real orders |
| Live | --live |
Real trades via exchange API |
| UI Dashboard | --ui |
Web dashboard at http://localhost:60050 |
| Telegram | --telegram |
Trade notifications with price charts |
Add @backtest-kit/cli to your project and wire it up in package.json scripts:
npm install @backtest-kit/cli
{
"scripts": {
"backtest": "npx @backtest-kit/cli --backtest ./src/index.mjs",
"paper": "npx @backtest-kit/cli --paper ./src/index.mjs",
"start": "npx @backtest-kit/cli --live ./src/index.mjs"
},
"dependencies": {
"@backtest-kit/cli": "latest",
"backtest-kit": "latest",
"ccxt": "latest"
}
}
Or run once without installing:
npx @backtest-kit/cli --backtest ./src/index.mjs
Create your strategy entry point (src/index.mjs). The file registers schemas via backtest-kit — @backtest-kit/cli is only the runner:
// src/index.mjs
import { addStrategySchema, addExchangeSchema, addFrameSchema } from 'backtest-kit';
import ccxt from 'ccxt';
// Register exchange
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),
});
// Register frame (backtest only)
addFrameSchema({
frameName: 'feb-2024',
interval: '1m',
startDate: new Date('2024-02-01'),
endDate: new Date('2024-02-29'),
});
// Register strategy
addStrategySchema({
strategyName: 'my-strategy',
interval: '15m',
getSignal: async (symbol) => {
// return signal or null
return null;
},
});
Run a backtest:
npm run backtest -- --symbol BTCUSDT
Run with UI dashboard and Telegram:
npm run backtest -- --symbol BTCUSDT --ui --telegram
Run live trading:
npm start -- --symbol BTCUSDT --ui
| Command Line Args | Type | Description |
|---|---|---|
--backtest |
boolean | Run historical backtest (default: false) |
--paper |
boolean | Paper trading (live prices, no orders) (default: false) |
--live |
boolean | Run live trading (default: false) |
--ui |
boolean | Start web UI dashboard (default: false) |
--telegram |
boolean | Enable Telegram notifications (default: false) |
--verbose |
boolean | Log each candle fetch (default: false) |
--noCache |
boolean | Skip candle cache warming before backtest (default: false) |
--symbol |
string | Trading pair (default: "BTCUSDT") |
--strategy |
string | Strategy name (default: first registered) |
--exchange |
string | Exchange name (default: first registered) |
--frame |
string | Backtest frame name (default: first registered) |
--cacheInterval |
string | Intervals to pre-cache before backtest (default: "1m, 15m, 30m, 4h") |
Positional argument (required): path to your strategy entry point file (set once in package.json scripts).
{
"scripts": {
"backtest": "npx @backtest-kit/cli --backtest ./src/index.mjs"
}
}
Runs the strategy against historical candle data using a registered FrameSchema.
{
"scripts": {
"backtest": "npx @backtest-kit/cli --backtest --symbol ETHUSDT --strategy my-strategy --exchange binance --frame feb-2024 --cacheInterval \"1m, 15m, 1h, 4h\" ./src/index.mjs"
}
}
npm run backtest
Before running, the CLI warms the candle cache for every interval in --cacheInterval. On the next run, cached data is used directly — no API calls needed. Pass --noCache to skip this step entirely.
Connects to the live exchange but does not place real orders. Identical code path to live — safe for strategy validation.
{
"scripts": {
"paper": "npx @backtest-kit/cli --paper --symbol BTCUSDT ./src/index.mjs"
}
}
npm run paper
Deploys a real trading bot. Requires exchange API keys configured in your .env or environment.
{
"scripts": {
"start": "npx @backtest-kit/cli --live --ui --telegram --symbol BTCUSDT ./src/index.mjs"
}
}
npm start
@backtest-kit/cli works out of the box in a monorepo where each strategy lives in its own subdirectory. When the CLI loads your entry point file, it automatically changes the working directory to the file's location — so all relative paths (dump/, modules/, template/) resolve inside that strategy's folder, not the project root.
Internally, ResolveService does the following before executing your entry point:
process.chdir(path.dirname(entryPoint)) // cwd → strategy directory
dotenv.config({ path: rootDir + '/.env' }) // load root .env first
dotenv.config({ path: strategyDir + '/.env', override: true }) // strategy .env overrides
Everything that follows — candle cache warming, report generation, module loading, template resolution — uses the new cwd automatically.
monorepo/
├── package.json # root scripts (one per strategy)
├── .env # shared API keys (exchange, Telegram, etc.)
└── strategies/
├── oct_2025/
│ ├── index.mjs # entry point — registers exchange/frame/strategy schemas
│ ├── .env # overrides root .env for this strategy
│ ├── modules (optional)
│ | ├── live.module.mjs # broker adapter for --live mode (optional)
│ | ├── paper.module.mjs # broker adapter for --paper mode (optional)
│ | ├── backtest.module.mjs # broker adapter for --backtest mode (optional)
│ ├── template/ # custom Mustache templates (optional)
│ └── dump/ # auto-created: candle cache + backtest reports
└── dec_2025/
├── index.mjs
├── .env
└── dump/
package.json{
"scripts": {
"backtest:oct": "npx @backtest-kit/cli --backtest ./strategies/oct_2025/index.mjs",
"backtest:dec": "npx @backtest-kit/cli --backtest ./strategies/dec_2025/index.mjs"
},
"dependencies": {
"@backtest-kit/cli": "latest",
"backtest-kit": "latest",
"ccxt": "latest"
}
}
npm run backtest:oct
npm run backtest:dec
| Resource | Path (relative to strategy dir) | Isolated |
|---|---|---|
| Candle cache | ./dump/data/candle/ |
✅ per-strategy |
| Backtest reports | ./dump/ |
✅ per-strategy |
| Broker module (live) | ./modules/live.module.mjs |
✅ per-strategy |
| Broker module (paper) | ./modules/paper.module.mjs |
✅ per-strategy |
| Broker module (backtest) | ./modules/backtest.module.mjs |
✅ per-strategy |
| Telegram templates | ./template/*.mustache |
✅ per-strategy |
| Environment variables | ./.env (overrides root) |
✅ per-strategy |
Each strategy run produces its own dump/ directory, making it straightforward to compare results across time periods — both by inspection and by pointing an AI agent at a specific strategy folder.
--ui)Starts @backtest-kit/ui server. Access the interactive dashboard at:
http://localhost:60050
Customize host/port via environment variables CC_WWWROOT_HOST and CC_WWWROOT_PORT.
--telegram)Sends formatted HTML messages with 1m / 15m / 1h price charts to your Telegram channel for every position event: opened, closed, scheduled, cancelled, risk rejection, partial profit/loss, trailing stop/take, and breakeven.
Requires CC_TELEGRAM_TOKEN and CC_TELEGRAM_CHANNEL in your environment.
The CLI supports mode-specific module files that are loaded as side-effect imports before the strategy starts. Each file is expected to call Broker.useBrokerAdapter() from backtest-kit to register a broker adapter.
| Mode | Module file | Loaded before |
|---|---|---|
--live |
./modules/live.module.mjs |
Live.background() |
--paper |
./modules/paper.module.mjs |
Live.background() (paper) |
--backtest |
./modules/backtest.module.mjs |
Backtest.background() |
File is resolved relative to
cwd(the strategy directory). All of.mjs,.cjs,.tsextensions are tried automatically. Missing module is a soft warning — not an error.
The module file is a side-effect import. When the CLI loads it, your code runs and registers the adapter. From that point on, backtest-kit intercepts every trade-mutating call through the adapter before updating internal state — if the adapter throws, the position state is never changed.
// live.module.mjs
import { Broker } from 'backtest-kit';
import { myExchange } from './exchange.mjs';
class MyBroker {
async onSignalOpenCommit({ symbol, priceOpen, direction }) {
await myExchange.openPosition(symbol, direction, priceOpen);
}
async onSignalCloseCommit({ symbol, priceClosed }) {
await myExchange.closePosition(symbol, priceClosed);
}
async onPartialProfitCommit({ symbol, cost, currentPrice }) {
await myExchange.createOrder({
symbol,
side: 'sell',
quantity: cost / currentPrice,
});
}
async onAverageBuyCommit({ symbol, cost, currentPrice }) {
await myExchange.createOrder({
symbol,
side: 'buy',
quantity: cost / currentPrice,
});
}
}
Broker.useBrokerAdapter(MyBroker);
Broker.enable();
| Method | Payload type | Triggered on |
|---|---|---|
onSignalOpenCommit |
BrokerSignalOpenPayload |
Position activation |
onSignalCloseCommit |
BrokerSignalClosePayload |
SL / TP / manual close |
onPartialProfitCommit |
BrokerPartialProfitPayload |
PP |
onPartialLossCommit |
BrokerPartialLossPayload |
PL |
onTrailingStopCommit |
BrokerTrailingStopPayload |
SL adjustment |
onTrailingTakeCommit |
BrokerTrailingTakePayload |
TP adjustment |
onBreakevenCommit |
BrokerBreakevenPayload |
SL moved to entry |
onAverageBuyCommit |
BrokerAverageBuyPayload |
DCA entry |
All methods are optional. Unimplemented hooks are silently skipped. In backtest mode all broker calls are skipped automatically — no adapter code runs during backtests.
import { Broker, IBroker, BrokerSignalOpenPayload, BrokerSignalClosePayload } from 'backtest-kit';
class MyBroker implements Partial<IBroker> {
async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
// place open order on exchange
}
async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
// place close order on exchange
}
}
Broker.useBrokerAdapter(MyBroker);
Broker.enable();
@backtest-kit/cli automatically detects the format of your strategy file and loads it with the appropriate runtime — no flags or configuration required.
| Format | Extension | Runtime | Use Case |
|---|---|---|---|
| TypeScript | .ts |
tsx via tsImport() |
TypeScript strategies with cross-imports (ESM ↔ CJS) |
| ES Module | .mjs |
Native import() |
Modern JavaScript with top-level await and ESM syntax |
| CommonJS | .cjs |
Native require() |
Legacy or dual-package strategies |
.ts)Run TypeScript strategy files directly — no tsc compilation step needed. Powered by tsx, which handles cross-format imports transparently:
{
"scripts": {
"backtest": "npx @backtest-kit/cli --backtest ./src/index.ts"
},
"dependencies": {
"@backtest-kit/cli": "latest",
"backtest-kit": "latest",
"tsx": "latest"
}
}
.mjs)Standard ESM format. Supports top-level await, named exports, and import syntax:
{
"scripts": {
"backtest": "npx @backtest-kit/cli --backtest ./src/index.mjs"
}
}
.cjs)For projects that compile to or use CommonJS. Loaded via require():
{
"scripts": {
"backtest": "npx @backtest-kit/cli --backtest ./dist/index.cjs"
}
}
Create a .env file in your project root:
# Telegram notifications (required for --telegram)
CC_TELEGRAM_TOKEN=your_bot_token_here
CC_TELEGRAM_CHANNEL=-100123456789
# Web UI server (optional, defaults shown)
CC_WWWROOT_HOST=0.0.0.0
CC_WWWROOT_PORT=60050
# Custom QuickChart service URL (optional)
CC_QUICKCHART_HOST=
| Variable | Default | Description |
|---|---|---|
CC_TELEGRAM_TOKEN |
— | Telegram bot token (from @BotFather) |
CC_TELEGRAM_CHANNEL |
— | Telegram channel or chat ID |
CC_WWWROOT_HOST |
0.0.0.0 |
UI server bind address |
CC_WWWROOT_PORT |
60050 |
UI server port |
CC_QUICKCHART_HOST |
— | Self-hosted QuickChart instance URL |
When your strategy module does not register an exchange, frame, or strategy name, the CLI falls back to built-in defaults and prints a console warning:
| Component | Default | Warning |
|---|---|---|
| Exchange | CCXT Binance (default_exchange) |
Warning: The default exchange schema is set to CCXT Binance... |
| Frame | February 2024 (default_frame) |
Warning: The default frame schema is set to February 2024... |
| Symbol | BTCUSDT |
— |
| Cache intervals | 1m, 15m, 30m, 4h |
Used if --cacheInterval not provided; skip entirely with --noCache |
Note: The default exchange schema does not support order book fetching in backtest mode. If your strategy calls
getOrderBook()during backtest, you must register a custom exchange schema with your own snapshot storage.
In addition to the CLI, @backtest-kit/cli can be used as a library — call run() directly from your own script without spawning a child process or parsing CLI flags.
run(mode, args)import { run } from '@backtest-kit/cli';
await run(mode, args);
| Parameter | Description |
|---|---|
mode |
"backtest" | "paper" | "live" — Execution mode |
args |
Mode-specific options (all optional — same defaults as CLI) |
run() can be called only once per process. A second call throws "Should be called only once".
Backtest (mode: "backtest"):
| Field | Type | Description |
|---|---|---|
entryPoint |
string |
Path to strategy entry point file |
symbol |
string |
Trading pair (default: "BTCUSDT") |
strategy |
string |
Strategy name (default: first registered) |
exchange |
string |
Exchange name (default: first registered) |
frame |
string |
Frame name (default: first registered) |
cacheInterval |
CandleInterval[] |
Intervals to pre-cache (default: ["1m","15m","30m","1h","4h"]) |
noCache |
boolean |
Skip candle cache warming (default: false) |
verbose |
boolean |
Log each candle fetch (default: false) |
Paper and Live (mode: "paper" / mode: "live"):
| Field | Type | Description |
|---|---|---|
entryPoint |
string |
Path to strategy entry point file |
symbol |
string |
Trading pair (default: "BTCUSDT") |
strategy |
string |
Strategy name (default: first registered) |
exchange |
string |
Exchange name (default: first registered) |
verbose |
boolean |
Log each candle fetch (default: false) |
Backtest:
import { run } from '@backtest-kit/cli';
await run('backtest', {
entryPoint: './src/index.mjs',
symbol: 'ETHUSDT',
frame: 'feb-2024',
cacheInterval: ['1m', '15m', '1h'],
verbose: true,
});
Paper trading:
import { run } from '@backtest-kit/cli';
await run('paper', {
entryPoint: './src/index.mjs',
symbol: 'BTCUSDT',
});
Live trading:
import { run } from '@backtest-kit/cli';
await run('live', {
entryPoint: './src/index.mjs',
symbol: 'BTCUSDT',
verbose: true,
});
Instead of writing infrastructure code for every project:
❌ Without @backtest-kit/cli (manual setup)
// index.ts
import { setLogger, setConfig, Storage, Notification, Report, Markdown } from 'backtest-kit';
import { serve } from '@backtest-kit/ui';
setLogger({ log: console.log, ... });
Storage.enable();
Notification.enable();
Report.enable();
Markdown.disable();
// ... parse CLI args manually
// ... register exchange schema
// ... warm candle cache
// ... set up Telegram bot
// ... handle SIGINT gracefully
// ... load and run backtest
✅ With @backtest-kit/cli (one script)
{ "scripts": { "backtest": "npx @backtest-kit/cli --backtest --ui --telegram ./src/index.mjs" } }
npm run backtest
Benefits:
backtest-kit strategy file as-isFork/PR on GitHub.
MIT © tripolskypetr