πŸ“Ÿ @backtest-kit/cli

Zero-boilerplate CLI for backtest-kit. Point it at a strategy file, pick a mode, and it handles exchange connectivity, candle caching, the web dashboard, Telegram alerts, and graceful shutdown for you β€” no setup code.

screenshot

Ask DeepWiki npm TypeScript

πŸ“š Docs Β· 🌟 Reference implementation Β· πŸ™ GitHub

New here? The fastest real setup is to clone the reference implementation β€” a working news-sentiment AI trading system with LLM forecasting, multi-timeframe data, and a documented February 2026 backtest. Start there, not from scratch.


# Scaffold a project (boilerplate stays inside the CLI; docs auto-fetched)
npx @backtest-kit/cli --init --output backtest-kit-project
cd backtest-kit-project && npm install && npm start -- --help

The whole onboarding is: write a strategy file that registers schemas via backtest-kit, point the CLI at it, choose a flag.

npx @backtest-kit/cli --backtest ./content/feb_2026.strategy/index.ts --symbol BTCUSDT
The strategy entry point (the CLI is only the runner)
// src/index.mjs β€” registers schemas via backtest-kit; @backtest-kit/cli just runs it
import { addStrategySchema, addExchangeSchema, addFrameSchema } from 'backtest-kit';
import ccxt from 'ccxt';

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),
});

addFrameSchema({ frameName: 'feb-2024', interval: '1m',
startDate: new Date('2024-02-01'), endDate: new Date('2024-02-29') });

addStrategySchema({ strategyName: 'my-strategy', interval: '15m',
getSignal: async (symbol) => null }); // return a signal or null

Wire it into package.json once and the positional path never changes:

{
"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" }
}
npm run backtest -- --symbol BTCUSDT --ui --telegram   # add integrations with flags

@backtest-kit/cli does two things well with one tool.

1. The lightest runner for a solo quant on day one. Write a strategy, point the CLI at it, you're trading. No DI container to learn, no scaffold to fight, no infra to copy-paste. The day you have an idea you can backtest it; the week you have an edge you can paper-trade it; the month you have a P&L you can run it live β€” same CLI, different flag.

2. A monorepo-grade runner for when the business takes off. The moment you start making money is the worst moment to rewrite your stack. So the CLI is monorepo-ready from day one even if you don't use it that way at first: per-strategy .env, per-strategy broker modules, folder-based import aliases, isolated dump dirs. The tool you backtested your first idea with is the tool that runs a desk of strategies in production β€” no rewrite, no language switch, only more files.


Every invocation is one mode (a primary flag) + a positional strategy/entry path + optional modifiers. --ui and --telegram are integrations that attach to any trading mode.

Mode Flag What it does
Backtest --backtest Run a strategy on historical candle data (uses a FrameSchema)
Paper --paper Live prices, no real orders β€” identical code path to live
Live --live Real trades via exchange API
Walker --walker A/B-compare multiple strategies on the same history, ranked report
Main --main Run a custom entry point with the full environment prepared, no trading harness
Pine --pine Run a local .pine indicator against exchange data
Editor --editor Open the visual Pine Script editor in the browser
Candle Dump --dump Fetch & save raw OHLCV candles to a file
PnL Debug --pnldebug Simulate per-minute PnL for a given entry price & direction
Broker Debug --brokerdebug Fire a single broker commit against the live adapter
Flush --flush Delete report/log/markdown/agent folders from a strategy dump dir
Init --init Scaffold a new project
Docker --docker Scaffold a self-contained Docker workspace
modifiers --ui Β· --telegram Β· --entry Web dashboard Β· Telegram alerts Β· fan out one strategy across many symbols
Complete core flag reference
Flag Type Description
--backtest boolean Run historical backtest (default false)
--walker boolean Run Walker A/B comparison (default false)
--paper boolean Paper trading β€” live prices, no orders (default false)
--live boolean Run live trading (default false)
--main boolean Custom entry point, no trading harness (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)
--noFlush boolean Skip removing report/log/markdown/agent folders before run (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 (default "1m, 15m, 30m, 4h")
--brokerdebug boolean Fire a single broker commit against the live adapter (default false)
--commit string Commit type for --brokerdebug (default "signal-open")

Positional argument (required): path to your strategy entry point file β€” set once in package.json scripts. Tool-specific flags (--pine, --dump, --pnldebug, --docker, …) are documented in their sections below.


The four modes that actually run strategies share one engine and one set of guarantees β€” only the clock and the order routing differ.

How each behaves

Backtest (--backtest) β€” runs against historical candles via a registered FrameSchema. Before running, the CLI removes the report, log, markdown, and agent folders from the strategy's dump/ dir, then warms the candle cache for every interval in --cacheInterval; subsequent runs reuse the cache with no API calls. --noCache skips warming, --noFlush keeps output folders.

{ "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" } }

Paper (--paper) β€” connects to the live exchange but places no real orders. Identical code path to live β€” the safe way to validate a strategy.

{ "scripts": { "paper": "npx @backtest-kit/cli --paper --symbol BTCUSDT ./src/index.mjs" } }

Live (--live) β€” deploys a real bot. Requires exchange API keys in .env. Combine with --ui --telegram for a monitored deployment.

{ "scripts": { "start": "npx @backtest-kit/cli --live --ui --telegram --symbol BTCUSDT ./src/index.mjs" } }

Runs the same historical period against multiple strategy files and prints a ranked report. Use it to pick the best variant before deploying.

npx @backtest-kit/cli --walker --symbol BTCUSDT --noCache --markdown --output feb_2026_comparison \
./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
# β†’ ./dump/feb_2026_comparison.md
Walker flags, output modes & behavior

Each positional argument is a separate strategy entry point. Before loading them the CLI removes the report/log/markdown/agent folders from each entry point's dump/ (skip with --noFlush). All files load without changing process.cwd() β€” .env is read from the working directory only. After loading, addWalkerSchema is called automatically using the exchange and frame registered by the strategy files. If no frame is registered, the CLI falls back to the last 31 days from Date.now() with a warning.

Flag Type Description
--walker boolean Enable Walker comparison
--symbol string Trading pair (default "BTCUSDT")
--cacheInterval string Intervals to pre-cache (default "1m, 15m, 30m, 4h")
--noCache boolean Skip candle cache warming
--noFlush boolean Skip removing output folders before the run
--verbose boolean Log each candle fetch and strategy progress
--output string Output file base name (default walker_{SYMBOL}_{TIMESTAMP})
--json boolean Save Walker.getData() as JSON to ./dump/<output>.json and exit
--markdown boolean Save Walker.getReport() as ./dump/<output>.md and exit

Output: no flag β†’ print Markdown report to stdout; --json / --markdown β†’ save and exit. Module hook: ./modules/walker.module loads automatically before the comparison (.ts/.mjs/.cjs tried in order).

Runs a single entry point with the full CLI environment prepared (.env, config/setup.config, config/loader.config, ./modules/main.module, cwd changed to the entry-point folder, graceful shutdown wired) β€” but never starts a trading harness. Use it to bootstrap the environment for a quick action, e.g. calling a 3rd-party API with automatic .env import.

Main behavior & flags

Unlike the trading modes it does not call Backtest/Live/Walker.background, pick a symbol, warm the cache, or resolve a strategy/exchange/frame β€” the entry point decides what to run. Exactly one positional entry point is required (Entry point is required otherwise). process.cwd() changes to the entry-point directory and its local .env overrides the root .env.

Although the CLI starts nothing itself, any Backtest/Live/Walker run your entry point launches is still managed: the process exits once listenDone* reports completion, the first Ctrl+C stops every active run via *.list()/*.stop(), a second force-quits. ./modules/main.module loads automatically before the entry point.

Flag Type Description
--main boolean Enable Main mode
--noFlush boolean Skip removing output folders before the run
{ "scripts": { "main": "npx @backtest-kit/cli --main ./tools/fetch_fear_and_greed.ts" } }

Power-user modifier β€” skip unless needed. The standard flow runs one symbol from --symbol. Use --entry to fan one strategy out across many symbols at once, or to drive *.background() from a UI / DB / API.

--entry is a modifier β€” combine it with exactly one of --backtest/--live/--paper/--walker, plus one positional entry file. The CLI does only the boilerplate (Setup, providers, the matching ./modules/<mode>.module, SIGINT that stops every active run, shutdown() once listenDone* reports all runs complete); you pick the symbol set, warm cache, and call *.background().

Example β€” backtest one strategy across five symbols
// src/multi-symbol.mjs
import { addExchangeSchema, addFrameSchema, addStrategySchema, Backtest, warmCandles } from "backtest-kit";
import ccxt from "ccxt";

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: (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 () => null });

// Decide the symbol set yourself β€” UI, database, API, or a list.
for (const symbol of ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT"]) {
// optional: await warmCandles({ exchangeName: "binance", interval: "1m", symbol, from, to });
Backtest.background(symbol, { strategyName: "my-strategy", exchangeName: "binance", frameName: "feb-2026" });
}
npx @backtest-kit/cli --backtest --entry ./src/multi-symbol.mjs

The same shape works for --live --entry / --paper --entry (call Live.background() per symbol with your broker adapter).


Five utilities that don't run a strategy. They share one convention, explained once here and referenced below.

The <mode>.module convention. By default the CLI auto-registers CCXT Binance. To use a different exchange (custom API keys, rate limits, a non-spot market), drop a modules/<mode>.module.ts that calls addExchangeSchema from backtest-kit. The CLI loads it automatically before running, trying .ts/.mjs/.cjs; it's searched next to the target file first, then in the project root. .env is loaded root-first then the target-file dir (override), so API keys stay out of code.

The shared <mode>.module.ts shape (pine / editor / dump / pnldebug / brokerdebug)
// modules/pine.module.ts  (same shape for editor/dump/pnldebug.module; brokerdebug registers a Broker instead)
import { addExchangeSchema } from "backtest-kit";
import ccxt from "ccxt";

addExchangeSchema({
exchangeName: "my-exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.bybit({
apiKey: process.env.BYBIT_API_KEY, secret: process.env.BYBIT_API_SECRET, enableRateLimit: true,
});
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: (s, p) => p.toFixed(2), formatQuantity: (s, q) => q.toFixed(8),
});
# .env (loaded root-first, then next to the target file)
BYBIT_API_KEY=xxx
BYBIT_API_SECRET=yyy

Executes any local .pine file against a real exchange and prints results as a Markdown table β€” no TradingView account. Reads every plot() that uses display=display.data_window as an output column (others ignored); column names come straight from the plot names.

npx @backtest-kit/cli --pine ./math/impulse_trend_15m.pine --symbol BTCUSDT --timeframe 15m --limit 180 --when "2025-09-24T12:00:00.000Z"
Pine flags, requirements & output
Flag Type Description
--pine boolean Enable PineScript mode
--symbol string Trading pair (default "BTCUSDT")
--timeframe string Candle interval (default "15m")
--limit string Candles to fetch (default 250)
--when string End date β€” ISO 8601 or Unix ms (default now)
--exchange string Exchange (default: first registered, falls back to CCXT Binance)
--output string Output base name (default: .pine file name)
--json boolean Write plots as JSON array to <pine-dir>/dump/{output}.json
--jsonl boolean Write plots as JSONL to <pine-dir>/dump/{output}.jsonl
--markdown boolean Write Markdown table to <pine-dir>/dump/{output}.md

--limit must cover indicator warmup bars β€” rows before warmup show N/A. Positional: path to the .pine file. Exchange via pine.module (see convention above). Required plot form:

//@version=5
indicator("MyIndicator", overlay=true)
plot(close,    "Close",    display=display.data_window)
plot(position, "Position", display=display.data_window)

Output (stdout, or --markdown/--json/--jsonl to <pine-dir>/dump/):

| Close | Position | timestamp |
| --- | --- | --- |
| 112871.28 | -1.0000 | 2025-09-22T15:00:00.000Z |
| 112736.00 | 0.0000 | 2025-09-22T18:30:00.000Z |
| 112653.90 | 1.0000 | 2025-09-22T22:15:00.000Z |

pine

A browser-based Pine Script editor (powered by @backtest-kit/ui) with a live chart that updates on β–Ά Run.

npx @backtest-kit/cli --editor   # β†’ http://localhost:60050?pine=1 opens automatically
Editor behavior & exchange

The CLI loads ./modules/editor.module if present (register your exchange, same as pine.module), starts the @backtest-kit/ui server on CC_WWWROOT_PORT (default 60050), and opens the editor in your browser. Ctrl+C stops it. Env: CC_WWWROOT_HOST (default 0.0.0.0), CC_WWWROOT_PORT (default 60050).

Fetch raw OHLCV candles from any registered exchange and save them β€” no strategy file required. dump/ is created in the current working directory.

npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 500 --when "2026-02-28T00:00:00.000Z" --jsonl --output feb2026_btc
# β†’ ./dump/feb2026_btc.jsonl
Dump flags
Flag Type Description
--dump boolean Enable candle dump
--symbol string Trading pair (default "BTCUSDT")
--timeframe string Candle interval (default "15m")
--limit string Candles to fetch (default 250)
--when string End date β€” ISO 8601 or Unix ms (default now)
--exchange string Exchange (default: first registered, falls back to CCXT Binance)
--output string Output base name (default {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
--json boolean Write candles as JSON array to ./dump/{output}.json
--jsonl boolean Write candles as JSONL to ./dump/{output}.jsonl

Exchange via dump.module (see convention above), searched in the current working directory. No flag β†’ print to stdout.

Simulate a hypothetical position minute by minute β€” running PnL, peak profit, max drawdown per candle β€” without placing trades or loading a strategy.

npx @backtest-kit/cli --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
PnL Debug flags, columns & sample output
Flag Type Description
--pnldebug boolean Enable PnL debug
--priceopen number Entry price (required)
--direction string long or short (default long)
--when string Start timestamp β€” ISO 8601 or Unix ms (default now)
--minutes string Number of 1m candles to simulate (default 60)
--symbol string Trading pair (default "BTCUSDT")
--exchange string Exchange (default: first registered, falls back to CCXT Binance)
--output string Output base name (default {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
--json / --jsonl / --markdown boolean Save to ./dump/<output>.{json,jsonl,md}

Columns: min (1-based offset), timestamp, close, pnl% (signed, vs entry), peak% (highest so far, β‰₯0), drawdown% (lowest so far, ≀0). Exchange via pnldebug.module (convention above).

Symbol: BTCUSDT | Direction: short | PriceOpen: 64069.50 | From: 2025-02-25T00:00:00.000Z | Minutes: 120
min | timestamp | close | pnl% | peak% | drawdown%
1 | 2025-02-25T00:01:00.000Z | 64020.10 | +0.08% | +0.08% | 0.00%
2 | 2025-02-25T00:02:00.000Z | 64105.30 | -0.06% | +0.08% | -0.06%
120 | 2025-02-25T02:00:00.000Z | 63200.00 | +1.36% | +1.36% | -0.06%

Fire a single broker commit against your live adapter without a full strategy β€” verify your brokerdebug.module wires exchange calls correctly before waiting hours for a real signal.

npx @backtest-kit/cli --brokerdebug --commit signal-open --symbol BTCUSDT
Broker Debug flags, commit types & how it works
Flag Type Description
--brokerdebug boolean Enable broker debug
--commit string Commit type to fire (default "signal-open")
--symbol string Trading pair (default "BTCUSDT")
--exchange string Exchange (default: first registered)

--commit values → hook: signal-open→onSignalOpenCommit, signal-close→onSignalCloseCommit, partial-profit→onPartialProfitCommit, partial-loss→onPartialLossCommit, average-buy→onAverageBuyCommit, trailing-stop→onTrailingStopCommit, trailing-take→onTrailingTakeCommit, breakeven→onBreakevenCommit.

The CLI loads ./modules/brokerdebug.module, fetches the last candle for --symbol, derives a synthetic payload from currentPrice (TP = +2%, SL = βˆ’2%), and calls the selected hook once; exits 0 on success. The module registers a Broker adapter (Broker.useBrokerAdapter(...) + Broker.enable()), not an exchange.

Delete generated output folders from one or more strategy dump dirs without touching cached candle data.

npx @backtest-kit/cli --flush ./content/feb_2026.strategy/modules/backtest.module.ts ./content/mar_2026.strategy/modules/backtest.module.ts
What flush removes

For each positional entry point the CLI resolves its directory and removes from <entry-dir>/dump/: report (backtest .jsonl), log (log.jsonl), markdown (exported reports), agent (agent outlines). Candle cache (dump/data/) and AI forecast outlines (dump/outline/) are not removed.


Bootstraps a ready-to-use project with an example strategy, an example Pine indicator, an AI-agent CLAUDE.md, and documentation fetched automatically. The target dir must not exist or be empty.

npx @backtest-kit/cli --init --output my-trading-bot   # β†’ ./my-trading-bot/
Generated structure & automatic docs fetch
backtest-kit-project/
β”œβ”€β”€ package.json # pre-configured with all backtest-kit deps
β”œβ”€β”€ CLAUDE.md # AI-agent guide for writing strategies
β”œβ”€β”€ content/feb_2026.strategy.ts # example strategy entry point
β”œβ”€β”€ math/feb_2026.pine # example PineScript indicator
β”œβ”€β”€ modules/{dump,pine}.module.ts # exchange schemas for --dump / --pine
β”œβ”€β”€ report/feb_2026.md # example research report
β”œβ”€β”€ docs/{...}.md + docs/lib/ # guides + fetched library READMEs
└── scripts/fetch_docs.mjs # downloads library READMEs into docs/lib/

After scaffolding the CLI runs scripts/fetch_docs.mjs, downloading the latest READMEs for backtest-kit, @backtest-kit/graph, @backtest-kit/pinets, @backtest-kit/cli, garch, volume-anomaly, agent-swarm-kit, functools-kit into docs/lib/. Re-run anytime with node ./scripts/fetch_docs.mjs or npm run sync:lib.

Scaffolds a self-contained Docker workspace with docker-compose.yaml and a strategy entry point, for 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
Two launch modes & environment variables

1. command: in docker-compose.yaml β€” pin mode and flags directly; the entrypoint forwards all args to the CLI unchanged:

command: [--live, --symbol, TRXUSDT, --strategy, feb_2026_strategy, --exchange, ccxt-exchange, ./content/feb_2026/feb_2026.strategy.ts, --ui]

2. Inline env vars β€” MODE + STRATEGY_FILE on the command line, no file edits:

Variable Required Default Description
MODE yes β€” backtest | live | paper | walker
STRATEGY_FILE yes β€” Path to entry point (relative to working_dir)
SYMBOL no BTCUSDT Trading pair
STRATEGY / EXCHANGE / FRAME no first registered Names
UI / TELEGRAM / VERBOSE / NO_CACHE / NO_FLUSH / ENTRY no β€” Any non-empty value enables the matching flag

When the CLI loads an entry point it changes the working directory to that file's location, so every relative path (dump/, modules/, template/) resolves inside that strategy's folder. Each strategy gets its own .env, broker modules, templates, and dump dir β€” so the same tool scales from one strategy to a desk of them.

How it works + isolated resources

ResolveService runs, before executing your entry point:

process.chdir(path.dirname(entryPoint))                         // cwd β†’ strategy directory
dotenv.config({ path: rootDir + '/.env' }) // root .env first
dotenv.config({ path: strategyDir + '/.env', override: true }) // strategy .env overrides
monorepo/
β”œβ”€β”€ package.json # root scripts (one per strategy)
β”œβ”€β”€ .env # shared API keys
└── strategies/
β”œβ”€β”€ oct_2025/
β”‚ β”œβ”€β”€ index.mjs # registers exchange/frame/strategy schemas
β”‚ β”œβ”€β”€ .env # overrides root .env for this strategy
β”‚ β”œβ”€β”€ modules/{live,paper,backtest}.module.mjs # broker adapters (optional)
β”‚ β”œβ”€β”€ template/ # custom Mustache templates (optional)
β”‚ └── dump/ # auto-created: candle cache + reports
└── dec_2025/ …
Resource Path (relative to strategy dir) Isolated
Candle cache ./dump/data/candle/ βœ… per-strategy
Backtest reports ./dump/ βœ… per-strategy
Broker module (live/paper/backtest) ./modules/{live,paper,backtest}.module.mjs βœ… per-strategy
Config module (walker) ./modules/walker.module.mjs βœ… loaded once
Telegram templates ./template/*.mustache βœ… per-strategy
Environment variables ./.env (overrides root) βœ… per-strategy

Each run produces its own dump/ β€” easy to compare results across periods, by inspection or by pointing an AI agent at a specific folder.

Every top-level folder in process.cwd() automatically becomes a bare import alias inside any strategy file β€” no config, just create the folder. Extract shared utilities, indicators, or AI-agent logic into named folders and reuse them across strategies without relative-path hell.

Resolution table, structure & tsconfig
Import Resolves to
import { fn } from "utils" <cwd>/utils/index.ts (or .js/.mjs/.cjs)
import { calcRSI } from "math/rsi" <cwd>/math/rsi.ts
import { research } from "logic" <cwd>/logic/index.ts
import { X } from "logic/contract/ResearchResponse.contract" <cwd>/logic/contract/ResearchResponse.contract.ts

Both barrel and deep-subpath imports are supported. Add a matching paths entry to tsconfig.json so the editor resolves them:

{
"compilerOptions": {
"moduleResolution": "bundler",
"paths": { "logic": ["./logic/index.ts"], "logic/*": ["./logic/*"], "math": ["./math/index.ts"], "math/*": ["./math/*"], "utils": ["./utils/index.ts"], "utils/*": ["./utils/*"] }
},
"include": ["./logic", "./math", "./utils", "./content", "./modules"]
}

The CLI auto-detects the format and loads it with the right runtime β€” no flags. .ts via tsx tsImport() (handles ESM↔CJS cross-imports, no tsc step), .mjs via native import() (top-level await, ESM), .cjs via native require() (legacy/dual-package). Add tsx to deps for .ts strategies.


Mode-specific module files register a Broker adapter via side-effect import before the strategy starts. From then 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 (atomic rollback, retried next tick). No manual wiring; in backtest mode no adapter is called at all.

Mode flag 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()
--walker ./modules/walker.module.mjs Walker.background()
--main ./modules/main.module.mjs the custom entry point
--brokerdebug ./modules/brokerdebug.module.mjs the broker commit test

Resolved relative to cwd (the strategy dir); .mjs/.cjs/.ts tried automatically. A missing module is a soft warning, not an error.

Adapter example & hook reference
// 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 Partial profit
onPartialLossCommit BrokerPartialLossPayload Partial loss
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. TypeScript: implement Partial<IBroker> with typed payloads (BrokerSignalOpenPayload, etc.).


Loaded from {projectRoot}/config/. The three runtime configs load in order β€” setup.config β†’ loader.config β†’ alias.config β€” before any strategy or module code. The UI/Telegram configs resolve strategy dir β†’ project root β†’ package default (first match wins) and accept .ts/.cjs/.mjs/.js.

Loaded once before any persistence call. When present, the CLI skips its own default adapter registration β€” your config takes full ownership of the persistence layer.

MongoDB + Redis via @backtest-kit/mongo

setup() registers all 15 persistence adapters in one call, reading connection params from env (or passed explicitly):

// config/setup.config.ts
import { setup } from '@backtest-kit/mongo';
setup(); // or setup({ CC_MONGO_CONNECTION_STRING, CC_REDIS_HOST, CC_REDIS_PORT, CC_REDIS_PASSWORD })
CC_MONGO_CONNECTION_STRING=mongodb://localhost:27017/backtest-kit
CC_REDIS_HOST=127.0.0.1
CC_REDIS_PORT=6379

No strategy-code changes β€” adapters are wired transparently before the first persistence call.

Loaded after setup.config, before strategy/module code. Unlike setup.config (side-effect import), it exports a function the CLI awaits β€” use it to wait for an async dependency before the run starts.

When to use it, export styles & examples

Use it to: wire microfrontends in a monorepo (pre-load sibling packages, hydrate a shared DI container); wait for a DB connection so the backtest fails fast instead of mid-run; warm caches / external APIs (instruments, calendar, fee tables); run schema migrations before signals flow.

Exactly one export style β€” never both (if both present, default wins):

// config/loader.config.ts β€” default export (preferred)
export default async () => { await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!); await redis.ping(); };
// β€” or named export
export const loader = async () => { /* … */ };

@backtest-kit/mongo's setup() registers adapters synchronously but doesn't block on the connection; gate the run on a real connection here. To stitch microfrontends: import "@my-org/brokers"; import "@my-org/signals"; (the @my-org alias is declared in alias.config).

Override any Node module import without touching strategy code. Loaded once on the first import and applied globally β€” e.g. replace a heavy dependency with a stub for backtesting, or swap an external API for a mock in CI.

Formats & async factory
// config/alias.config.ts β€” named export
export const ccxt = require("./stubs/ccxt.stub.cjs");
// config/alias.config.cjs β€” default export
module.exports = { ccxt: require("./stubs/ccxt.stub.cjs") };

It may also export an async factory the CLI awaits before strategy code runs β€” handy for ESM-only modules that require() would throw on:

// async factory (default export); or `export const loader = async () => ({...})`
export default async () => ({ nanoid: await import("nanoid"), "p-limit": await import("p-limit") });

Both styles supported, never both at once (default wins). When strategy code calls require("ccxt"), the loader checks the alias table first β€” no monkey-patching of node_modules. Applies to all modules in the process (not per-strategy).

symbol.config β€” restrict/reorder the UI symbol list

By default the UI shows all exchange symbols. Override with a config/symbol.config (resolution: strategy dir β†’ project root β†’ package default):

// config/symbol.config.ts
export const symbol_list = [
{ icon: "/icon/btc.png", logo: "/icon/128/btc.png", symbol: "BTCUSDT", displayName: "Bitcoin", color: "#F7931A", priority: 50, description: "Bitcoin β€” the first and most popular cryptocurrency" },
{ icon: "/icon/eth.png", logo: "/icon/128/eth.png", symbol: "ETHUSDT", displayName: "Ethereum", color: "#6F42C1", priority: 50, description: "Ethereum β€” a blockchain platform for smart contracts" },
];
notification.config β€” which notification categories the UI shows

Defaults (override per strategy):

Key Default Description
signal true Signal lifecycle: opened, scheduled, closed, cancelled
risk true Risk manager rejections
info true Informational messages on an active signal
breakeven true Breakeven level reached
common_error true Non-fatal runtime errors
critical_error true Fatal errors that terminate the session
validation_error true Config / input validation errors
strategy_commit true All committed actions (partial close, DCA, trailing, …)
partial_loss false Partial loss level reached (before commit)
partial_profit false Partial profit level reached (before commit)
signal_sync false Live order fill / exit confirmations from exchange sync
// config/notification.config.ts
export default { signal: true, risk: true, info: true, breakeven: true, common_error: true, critical_error: true, validation_error: true, strategy_commit: true, partial_loss: false, partial_profit: false, signal_sync: false };
Override Mustache rendering with get*Markdown methods

By default messages render from Mustache templates (template/*.mustache). Export an object with any subset of get*Markdown methods (each gets the event payload, returns Promise<string>); unimplemented ones fall back to the template.

// config/telegram.config.ts
import { IStrategyTickResultOpened, IStrategyTickResultClosed, RiskContract } from "backtest-kit";
export default {
async getOpenedMarkdown(e: IStrategyTickResultOpened) { return `**Opened** ${e.symbol} at ${e.priceOpen}`; },
async getClosedMarkdown(e: IStrategyTickResultClosed) { return `**Closed** ${e.symbol} at ${e.priceClosed}`; },
async getRiskMarkdown(e: RiskContract) { return `**Risk rejected** ${e.symbol}`; },
};

Methods β†’ event types: getOpenedMarkdown/getClosedMarkdown/getScheduledMarkdown/getCancelledMarkdown (IStrategyTickResult*), getRiskMarkdown (RiskContract), getPartialProfitMarkdown/getPartialLossMarkdown/getBreakevenMarkdown/getTrailingTakeMarkdown/getTrailingStopMarkdown/getAverageBuyMarkdown (the matching *Commit), getSignalOpenMarkdown/getSignalCloseMarkdown (SignalOpen/CloseContract), getCancelScheduledMarkdown/getClosePendingMarkdown (*Commit), getSignalInfoMarkdown (SignalInfoContract).


Starts the @backtest-kit/ui server at http://localhost:60050 (host/port via CC_WWWROOT_HOST / CC_WWWROOT_PORT). Restrict the symbol list with symbol.config and notification categories with notification.config (above).

Sends formatted HTML messages with 1m / 15m / 1h price charts for every position event β€” opened, closed, scheduled, cancelled, risk rejection, partial profit/loss, trailing stop/take, breakeven. Requires CC_TELEGRAM_TOKEN and CC_TELEGRAM_CHANNEL. Customize per-event rendering with telegram.config (above).


Use the CLI as a library β€” call run() from your own script, no child process or flag parsing.

import { run } from '@backtest-kit/cli';

await run('backtest', { entryPoint: './src/index.mjs', symbol: 'ETHUSDT', frame: 'feb-2024', cacheInterval: ['1m','15m','1h'], verbose: true });
await run('paper', { entryPoint: './src/index.mjs', symbol: 'BTCUSDT' });
await run('live', { entryPoint: './src/index.mjs', symbol: 'BTCUSDT', verbose: true });
Payload fields (call once per process)

run() can be called only once per process β€” a second call throws "Should be called only once". mode: "backtest" | "paper" | "live".

Backtest: entryPoint, symbol ("BTCUSDT"), strategy (first registered), exchange (first registered), frame (first registered), cacheInterval (["1m","15m","30m","1h","4h"]), noCache (false), noFlush (false), verbose (false).

Paper / Live: entryPoint, symbol ("BTCUSDT"), strategy (first registered), exchange (first registered), verbose (false).


CC_TELEGRAM_TOKEN=your_bot_token_here     # required for --telegram (from @BotFather)
CC_TELEGRAM_CHANNEL=-100123456789         # required for --telegram (channel/chat ID)
CC_WWWROOT_HOST=0.0.0.0                    # UI bind address (default 0.0.0.0)
CC_WWWROOT_PORT=60050                      # UI port (default 60050)
CC_QUICKCHART_HOST=                        # optional self-hosted QuickChart URL
Default behaviors (when a schema isn't registered)
Component Default Note
Exchange CCXT Binance (default_exchange) warns; does not support order book in backtest β€” register a custom exchange with snapshot storage if your strategy calls getOrderBook() in backtest
Frame February 2024 (default_frame) warns
Symbol BTCUSDT β€”
Cache intervals 1m, 15m, 30m, 4h used if --cacheInterval not given; skip with --noCache

Instead of writing infrastructure for every project β€” manual logger/storage/notification setup, CLI arg parsing, exchange registration, cache warming, Telegram bot, SIGINT handling, run wiring β€” the whole thing is one script:

{ "scripts": { "backtest": "npx @backtest-kit/cli --backtest --ui --telegram ./src/index.mjs" } }

Zero to running backtest in seconds Β· automatic candle-cache warming with retry Β· production web dashboard out of the box Β· Telegram alerts with charts (no chart code) Β· graceful SIGINT shutdown (no hanging processes) Β· pluggable logger β€” override the built-in one with setLogger() from your strategy module Β· works with any backtest-kit strategy as-is Β· broker hooks via side-effect modules (no CLI internals to touch).

Fork / PR on GitHub.

MIT Β© tripolskypetr