diff --git "a/documents/cli_readme.html" "b/documents/cli_readme.html" --- "a/documents/cli_readme.html" +++ "b/documents/cli_readme.html" @@ -11,6 +11,23 @@
+New to backtest-kit? The fastest way to get a real, production-ready setup is to clone the reference implementation — a fully working news-sentiment AI trading system with LLM forecasting, multi-timeframe data, and a documented February 2026 backtest. Start there instead of from scratch.
Minimal scaffold — all boilerplate stays inside @backtest-kit/cli:
+npx @backtest-kit/cli --init --output backtest-kit-project
cd backtest-kit-project
npm install
npm start -- --help
+
+
+@backtest-kit/cli is designed to do two things well — and the same tool covers both.
1. The lightest possible runner for a solo quant on day one.
+You write a strategy file, point the CLI at it, and you're trading. No DI container to learn, no project scaffold to fight, no infrastructure code to copy-paste. One developer, one strategy, one command:
+npx @backtest-kit/cli --init
npx @backtest-kit/cli --backtest ./content/feb_2026.strategy/index.ts
+
+
+That's the whole onboarding. The first day you have an idea, you can backtest it. The first week you have an edge, you can paper-trade it. The first month you have a P&L, you can run it live — same CLI, different flag.
+2. Built-in monorepo tooling for when the business takes off.
+The moment you start making money is the worst possible moment to rewrite your stack in another language. So the CLI is also a monorepo-grade runner from day one — even if you don't use it that way at first.
+monorepo/
├── content/
│ ├── feb_2026.strategy/
│ ├── feb_2026.strategy.ts # strategy production code
│ ├── feb_2026.test.ts # developer playground
├── packages/
│ ├── shared-broker/ # shared broker code
│ ├── shared-signals/ # common indicators (RSI, MACD)
+
+
+As a result: you used to backtest your first idea is the same tool you use to run a desk of strategies in production. No rewrite, no language switch, no framework migration when the business scales — only more files in the monorepo.
npx @backtest-kit/cli --backtest ./strategy.mjs — no boilerplate needed--brokerdebug--flushCreate 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;
},
});
+// 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:
@@ -201,6 +223,16 @@
string
Intervals to pre-cache before backtest (default: "1m, 15m, 30m, 4h")
--brokerdebugfalse)--commit--brokerdebug (default: "signal-open")Positional argument (required): path to your strategy entry point file (set once in package.json scripts).
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
+++For Poweruser — skip unless needed. The standard flow runs one symbol from
+--symbol. Use--entryonly to fan out one strategy across many symbols at once, or to drive*.background()from a UI / DB / API.
With --entry, the CLI does only the boilerplate — Setup, providers (--ui / --telegram), the matching ./modules/<mode>.module, SIGINT that stops every active run via *.list(), and shutdown() once listenDone* reports all your runs complete. Picking the symbol set, warming cache, and calling *.background() is on you.
The --entry flag is a modifier — combine it with exactly one of --backtest / --live / --paper / --walker. One positional: the path to your entry file.
npx @backtest-kit/cli --backtest --entry ./src/multi-symbol.mjs
+
+
+// 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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.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) => null,
});
// Decide the symbol set yourself — from a UI, database, API, or just a list.
const symbols = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT"];
for (const symbol of symbols) {
//
// Optional
//
// await warmCandles({
// exchangeName: "binance,
// from: new Date("2026-01-01T00:00:00Z"),
// to: new Date("2026-01-31T23:59:59Z"),
// interval: "1m",
// symbol,
// })
Backtest.background(symbol, {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "feb-2026",
});
}
+
+
+The same shape works for --live --entry / --paper --entry (call Live.background() per symbol with your broker adapter)
@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
+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.
-Project Structure
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/
+Project Structure
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/
Root 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"
}
}
@@ -401,7 +445,7 @@
-Project Structure
my-project/
├── utils/ ← import { formatDate } from "utils"
│ └── index.ts
├── math/ ← import { calcRSI } from "math/rsi"
│ └── rsi.ts
├── logic/ ← import { research } from "logic"
│ ├── index.ts ← barrel
│ └── contract/
│ └── ResearchResponse.contract.ts ← import { ... } from "logic/contract/ResearchResponse.contract"
└── content/
├── feb_2026.strategy.ts ← uses all three aliases freely
└── mar_2026.strategy.ts ← same aliases, no duplication
+Project Structure
my-project/
├── utils/ ← import { formatDate } from "utils"
│ └── index.ts
├── math/ ← import { calcRSI } from "math/rsi"
│ └── rsi.ts
├── logic/ ← import { research } from "logic"
│ ├── index.ts ← barrel
│ └── contract/
│ └── ResearchResponse.contract.ts ← import { ... } from "logic/contract/ResearchResponse.contract"
└── content/
├── feb_2026.strategy.ts ← uses all three aliases freely
└── mar_2026.strategy.ts ← same aliases, no duplication
This lets you extract shared utilities, math helpers, or AI agent logic (e.g. agent-swarm-kit workflows) into named folders and reuse them across every strategy in the project without relative path hell.
@@ -443,7 +487,7 @@
Supported file formats (.ts, .cjs, .mjs, .js tried automatically):
-// 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",
},
];
+// 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 Filter (notification.config)
Controls which notification categories are shown in the UI dashboard. Create a config/notification.config file in your strategy directory to override the defaults.
@@ -541,14 +585,14 @@
-// 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,
};
+// 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,
};
Telegram Notifications (--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.
Telegram Message Adapter (telegram.config)
By default messages are rendered from Mustache templates (template/*.mustache). To override rendering programmatically, create a config/telegram.config file and export an object with any subset of get*Markdown methods. Each method receives the event payload and must return a Promise<string> with the Markdown message body.
Resolution order is the same as other configs (strategy dir → project root → package default).
-// config/telegram.config.ts
import {
IStrategyTickResultOpened,
IStrategyTickResultClosed,
RiskContract,
} from "backtest-kit";
export default {
async getOpenedMarkdown(event: IStrategyTickResultOpened): Promise<string> {
return `**Opened** ${event.symbol} at ${event.priceOpen}`;
},
async getClosedMarkdown(event: IStrategyTickResultClosed): Promise<string> {
return `**Closed** ${event.symbol} at ${event.priceClosed}`;
},
async getRiskMarkdown(event: RiskContract): Promise<string> {
return `**Risk rejected** ${event.symbol}`;
},
};
+// config/telegram.config.ts
import {
IStrategyTickResultOpened,
IStrategyTickResultClosed,
RiskContract,
} from "backtest-kit";
export default {
async getOpenedMarkdown(event: IStrategyTickResultOpened): Promise<string> {
return `**Opened** ${event.symbol} at ${event.priceOpen}`;
},
async getClosedMarkdown(event: IStrategyTickResultClosed): Promise<string> {
return `**Closed** ${event.symbol} at ${event.priceClosed}`;
},
async getRiskMarkdown(event: RiskContract): Promise<string> {
return `**Risk rejected** ${event.symbol}`;
},
};
All methods are optional — unimplemented ones fall back to the Mustache template.
@@ -656,13 +700,18 @@
./modules/walker.module.mjs
Walker.background()
+
+--brokerdebug
+./modules/brokerdebug.module.mjs
+broker commit test
+
File is resolved relative to cwd (the strategy directory). All of .mjs, .cjs, .ts extensions are tried automatically. Missing module is a soft warning — not an error.
How It Works
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();
+// 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();
Available Broker Hooks
@@ -717,23 +766,73 @@
All methods are optional. Unimplemented hooks are silently skipped. In backtest mode all broker calls are skipped automatically — no adapter code runs during backtests.
-TypeScript
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();
+TypeScript
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();
+
+
+⚙️ Setup Hook (config/setup.config)
@backtest-kit/cli loads a {projectRoot}/config/setup.config file once before any module hooks or strategy code run. Use it to perform one-time initialization that must happen before the first persistence call — registering a custom storage backend, configuring a logger, seeding global state, or anything else the process needs before backtest-kit starts.
+Important: When setup.config is present, the CLI skips its own default adapter registration — it does not call usePersist() / useLocal() / useMemory() for any of the persistence slots. This means your config takes full ownership of the persistence layer: whatever adapters you register in {projectRoot}/config/setup.config are the ones backtest-kit uses, with no interference from the CLI defaults.
+Example: MongoDB + Redis persistence via @backtest-kit/mongo
The most common use-case is swapping the default file-based persistence for a production-grade backend. Install @backtest-kit/mongo and call setup() — it registers all 15 persistence adapters in one call and reads connection parameters from environment variables:
+npm install @backtest-kit/mongo
-🔀 Import Aliases (alias.module)
@backtest-kit/cli lets you override any nodejs module import — without touching the strategy code. Drop a config/alias.module file in your project root and export a mapping from module name to replacement module.
-The alias table is loaded once (on the first import call) from {projectRoot}/config/alias.module and applied globally to every subsequent module load via require/ import.
+// config/setup.config.ts
import { setup } from '@backtest-kit/mongo';
setup();
+
+
+# .env
+CC_MONGO_CONNECTION_STRING=mongodb://localhost:27017/backtest-kit
+CC_REDIS_HOST=127.0.0.1
+CC_REDIS_PORT=6379
+
+
+Or pass connection parameters explicitly:
+// config/setup.config.ts
import { setup } from '@backtest-kit/mongo';
setup({
CC_MONGO_CONNECTION_STRING: 'mongodb://mongo:27017/mydb',
CC_REDIS_HOST: 'redis',
CC_REDIS_PORT: 6379,
CC_REDIS_PASSWORD: 'secret',
});
+
+
+No changes to strategy code are needed — setup() wires up the adapters transparently before backtest-kit makes its first persistence call.
+🧩 Module Loader (config/loader.config)
@backtest-kit/cli loads a {projectRoot}/config/loader.config file after setup.config but before any strategy or module code runs. Unlike setup.config (which is loaded for its side effects), loader.config exports a function that the CLI explicitly awaits. Use it whenever you need to wait for an async dependency to be ready before the backtest starts.
+monorepo/
├── packages/
│ ├── shared-broker/ # shared broker code
│ ├── shared-signals/ # common indicators (RSI, MACD)
│ ├── shared-db/ # mongodb wiring
│ ├── strategy-momentum/ # strategy code
│ └── strategy-mean-reversion/
+
+
+When to use it:
+
+- Wire microfrontends in a monorepo — resolve and pre-load sibling packages, register cross-package services, or hydrate a shared DI container before strategies import from neighboring workspaces.
+- Wait for a database connection — open a Mongo/Postgres/Redis connection and verify it's reachable before the first persistence call, so the backtest fails fast instead of mid-run.
+- Warm up caches or external APIs — pre-fetch reference data (instruments list, calendar, fee tables) so the strategy's first tick doesn't pay the round-trip cost.
+- Run schema migrations — apply any pending migrations to the persistence backend before signals start flowing.
+
+loader.config supports exactly one of two export styles — never both at once. If both are present, the default export wins and the named loader is ignored.
+// config/loader.config.ts — default export (preferred, ESM style)
export default async () => {
await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!);
await redis.ping();
};
+
+
+// config/loader.config.ts — named export
export const loader = async () => {
await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!);
await redis.ping();
};
+
+
+Example: wait for MongoDB before running a backtest
@backtest-kit/mongo's setup() registers the adapters synchronously but doesn't block until the connection is established. If your backtest depends on data that must be present in Mongo before the first signal fires, use loader.config to gate the run on a real connection:
+// config/setup.config.ts
import { setup } from '@backtest-kit/mongo';
setup();
+
+
+// config/loader.config.ts
import mongoose from 'mongoose';
export default async () => {
await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!);
console.log('mongo connection verified, starting backtest');
};
+
+
+Example: stitch microfrontends in a monorepo
When backtest-kit strategies live in one workspace and shared services (broker adapters, signal feeds, dashboards) live in sibling workspaces, loader.config is the place to wire them together before the runner starts:
+// config/loader.config.ts
import "@my-org/brokers";
import "@my-org/signals";
+
+
+The @my-org alias should be declared in config/alias.config.
+🔀 Import Aliases (config/alias.config)
@backtest-kit/cli lets you override any nodejs module import — without touching the strategy code. Drop a config/alias.config file in your project root and export a mapping from module name to replacement module.
+The alias table is loaded once (on the first import call) from {projectRoot}/config/alias.config and applied globally to every subsequent module load via require/ import.
Use cases:
- Replace a heavy dependency with a lighter stub for backtesting
- Swap any external api for a mock during CI runs
-// config/alias.module.ts — named export
export const ccxt = require("./stubs/ccxt.stub.cjs");
+// config/alias.config.ts — named export
export const ccxt = require("./stubs/ccxt.stub.cjs");
-// config/alias.module.cjs — default export
module.exports = {
ccxt: require("./stubs/ccxt.stub.cjs"),
};
+// config/alias.config.cjs — default export
module.exports = {
ccxt: require("./stubs/ccxt.stub.cjs"),
};
-// config/alias.module.mjs — default export
import ccxtStub from "./stubs/ccxt.stub.mjs";
export default {
ccxt: ccxtStub,
};
+// config/alias.config.mjs — default export
import ccxtStub from "./stubs/ccxt.stub.mjs";
export default {
ccxt: ccxtStub,
};
How it works: when strategy code calls require("ccxt"), the loader checks IMPORT_ALIAS first. If a key matches, the mapped value is returned instead of the real module — no monkey-patching of node_modules needed.
@@ -851,15 +950,15 @@
Next to the .pine file — <pine-file-dir>/modules/pine.module.ts
Project root — <cwd>/modules/pine.module.ts
-my-project/
├── math/
│ ├── impulse_trend_15m.pine ← indicator
│ └── modules/
│ └── pine.module.ts ← loaded first (next to .pine file)
├── modules/
│ └── pine.module.ts ← fallback (project root)
└── package.json
+my-project/
├── math/
│ ├── impulse_trend_15m.pine ← indicator
│ └── modules/
│ └── pine.module.ts ← loaded first (next to .pine file)
├── modules/
│ └── pine.module.ts ← fallback (project root)
└── package.json
Inside pine.module.ts call addExchangeSchema from backtest-kit and give the exchange a name:
-// modules/pine.module.ts
import { addExchangeSchema } from "backtest-kit";
import ccxt from "ccxt";
addExchangeSchema({
exchangeName: "my-exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.bybit({ 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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
+// modules/pine.module.ts
import { addExchangeSchema } from "backtest-kit";
import ccxt from "ccxt";
addExchangeSchema({
exchangeName: "my-exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.bybit({ 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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
Environment variables (.env)
Before loading pine.module, the CLI loads .env files in the same order as for strategy modules — project root first, then the .pine file directory (overrides root):
-my-project/
├── math/
│ ├── .env ← loaded second (overrides root)
│ └── impulse_trend_15m.pine
├── .env ← loaded first
└── package.json
+my-project/
├── math/
│ ├── .env ← loaded second (overrides root)
│ └── impulse_trend_15m.pine
├── .env ← loaded first
└── package.json
Use this to store API keys without hardcoding them:
@@ -868,7 +967,7 @@ BYBIT_API_KEY=xxx
BYBIT_API_SECRET=yyy
-// modules/pine.module.ts
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,
});
// ...
},
});
+// modules/pine.module.ts
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,
});
// ...
},
});
Then run:
@@ -894,7 +993,7 @@ plot(position, "Position", display=display.data_window)
The column names in the output Markdown table are taken directly from those plot names — no manual schema definition needed.
Output
The CLI prints a Markdown table to stdout:
-# PineScript Technical Analysis Dump
**Signal ID**: CLI execution 2025-09-24T12:00:00.000Z
| Close | Position | timestamp |
| --- | --- | --- |
| 112871.28 | -1.0000 | 2025-09-22T15:00:00.000Z |
| 112666.69 | -1.0000 | 2025-09-22T15:15:00.000Z |
| 112736.00 | 0.0000 | 2025-09-22T18:30:00.000Z |
| 112653.90 | 1.0000 | 2025-09-22T22:15:00.000Z |
+# PineScript Technical Analysis Dump
**Signal ID**: CLI execution 2025-09-24T12:00:00.000Z
| Close | Position | timestamp |
| --- | --- | --- |
| 112871.28 | -1.0000 | 2025-09-22T15:00:00.000Z |
| 112666.69 | -1.0000 | 2025-09-22T15:15:00.000Z |
| 112736.00 | 0.0000 | 2025-09-22T18:30:00.000Z |
| 112653.90 | 1.0000 | 2025-09-22T22:15:00.000Z |
Save to ./math/dump/impulse_trend_15m.md (uses .pine file name automatically, dump is created next to the .pine file):
@@ -922,7 +1021,7 @@ plot(position, "Position", display=display.data_window)
Press Ctrl+C to stop the server.
Exchange via editor.module
Drop a modules/editor.module.ts next to your project to register the exchange that the editor's candle provider will use:
-// modules/editor.module.ts
import { addExchangeSchema } from "backtest-kit";
import ccxt from "ccxt";
addExchangeSchema({
exchangeName: "my-exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.bybit({ 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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
+// modules/editor.module.ts
import { addExchangeSchema } from "backtest-kit";
import ccxt from "ccxt";
addExchangeSchema({
exchangeName: "my-exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.bybit({ 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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
Environment Variables
Exchange via pnldebug.module
By default the CLI registers CCXT Binance automatically. To use a different exchange, create a modules/pnldebug.module.ts file in the current working directory — the CLI loads it automatically before fetching candles.
-// modules/pnldebug.module.ts
import { addExchangeSchema } from "backtest-kit";
import ccxt from "ccxt";
addExchangeSchema({
exchangeName: "my-exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.bybit({ 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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
+// modules/pnldebug.module.ts
import { addExchangeSchema } from "backtest-kit";
import ccxt from "ccxt";
addExchangeSchema({
exchangeName: "my-exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.bybit({ 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: (symbol, price) => price.toFixed(2),
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
});
Usage
Print to stdout (default table format):
@@ -1162,10 +1261,10 @@ plot(position, "Position", display=display.data_window)
npm run pnldebug
-Example stdout output
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%
+Example stdout output
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%
-🗑️ Flushing Strategy Output (--flush)
@backtest-kit/cli can delete generated output folders from one or more strategy dump directories without touching cached candle data.
+🐛 Broker Debug (--brokerdebug)
@backtest-kit/cli can fire a single broker commit against your live broker adapter without running a full strategy — useful for verifying that your brokerdebug.module correctly wires up exchange calls.
CLI Flags
@@ -1176,6 +1275,89 @@ plot(position, "Position", display=display.data_window)
+--brokerdebug
+boolean
+Enable broker debug mode
+
+
+--commit
+string
+Commit type to fire (default: "signal-open")
+
+
+--symbol
+string
+Trading pair (default: "BTCUSDT")
+
+
+--exchange
+string
+Exchange name (default: first registered)
+
+
+
+Available --commit values:
+
+
+
+Value
+Broker hook
+
+
+
+
+signal-open
+onSignalOpenCommit
+
+
+signal-close
+onSignalCloseCommit
+
+
+partial-profit
+onPartialProfitCommit
+
+
+partial-loss
+onPartialLossCommit
+
+
+average-buy
+onAverageBuyCommit
+
+
+trailing-stop
+onTrailingStopCommit
+
+
+trailing-take
+onTrailingTakeCommit
+
+
+breakeven
+onBreakevenCommit
+
+
+
+How It Works
The CLI loads ./modules/brokerdebug.module, fetches the last candle for --symbol / --timeframe, derives synthetic payload values from currentPrice (TP = +2%, SL = -2%), and calls the selected broker hook once. Exits with code 0 on success.
+Broker via brokerdebug.module
Create a modules/brokerdebug.module.ts file and register your broker adapter:
+// modules/brokerdebug.module.ts
import { Broker } from 'backtest-kit';
import { myExchange } from './exchange.mjs';
class MyBroker {
async onSignalOpenCommit({ symbol, priceOpen, position }) {
await myExchange.openPosition(symbol, position, priceOpen);
}
// ... other hooks
}
Broker.useBrokerAdapter(MyBroker);
Broker.enable();
+
+
+Usage
npx @backtest-kit/cli --brokerdebug --commit signal-open --symbol BTCUSDT
npx @backtest-kit/cli --brokerdebug --commit partial-profit --symbol ETHUSDT --timeframe 1h
+
+
+🗑️ Flushing Strategy Output (--flush)
@backtest-kit/cli can delete generated output folders from one or more strategy dump directories without touching cached candle data.
+CLI Flags
+
+
+Flag
+Type
+Description
+
+
+
+
--flush
boolean
Enable flush mode
@@ -1210,7 +1392,7 @@ plot(position, "Position", display=display.data_window)
Candle cache (dump/data/) and AI forecast outlines (dump/outline/) are not removed.
-Usage
Flush a single strategy:
+Usage
Flush a single strategy:
npx @backtest-kit/cli --flush ./content/feb_2026.strategy/modules/backtest.module.ts
@@ -1226,7 +1408,7 @@ plot(position, "Position", display=display.data_window)
🗂️ Scaffolding a New Project (--init)
@backtest-kit/cli can bootstrap a ready-to-use project directory with a pre-configured layout, example strategy files, and all documentation fetched automatically.
-CLI Flags
+CLI Flags
Flag
@@ -1247,7 +1429,7 @@ plot(position, "Position", display=display.data_window)
-Usage
npx @backtest-kit/cli --init
+Usage
npx @backtest-kit/cli --init
Creates ./backtest-kit-project/ in the current working directory.
@@ -1257,7 +1439,7 @@ plot(position, "Position", display=display.data_window)
Creates ./my-trading-bot/.
The target directory must not exist or must be empty — the command aborts if it contains any files.
-Generated Project Structure
backtest-kit-project/
├── package.json # pre-configured with all backtest-kit dependencies
├── .gitignore
├── CLAUDE.md # AI-agent guide for writing strategies
├── content/
│ └── feb_2026.strategy.ts # example strategy entry point
├── docs/
│ ├── lib/ # fetched automatically (see below)
│ ├── backtest_actions.md
│ ├── backtest_graph_pattern.md
│ ├── backtest_logging_jsonl.md
│ ├── backtest_pinets_usage.md
│ ├── backtest_risk_async.md
│ ├── backtest_strategy_structure.md
│ ├── pine_debug.md
│ └── pine_indicator_warmup.md
├── math/
│ └── feb_2026.pine # example PineScript indicator
├── modules/
│ ├── dump.module.ts # exchange schema for --dump mode
│ └── pine.module.ts # exchange schema for --pine mode
├── report/
│ └── feb_2026.md # example strategy research report
└── scripts/
└── fetch_docs.mjs # utility: downloads library READMEs into docs/lib/
+Generated Project Structure
backtest-kit-project/
├── package.json # pre-configured with all backtest-kit dependencies
├── .gitignore
├── CLAUDE.md # AI-agent guide for writing strategies
├── content/
│ └── feb_2026.strategy.ts # example strategy entry point
├── docs/
│ ├── lib/ # fetched automatically (see below)
│ ├── backtest_actions.md
│ ├── backtest_graph_pattern.md
│ ├── backtest_logging_jsonl.md
│ ├── backtest_pinets_usage.md
│ ├── backtest_risk_async.md
│ ├── backtest_strategy_structure.md
│ ├── pine_debug.md
│ └── pine_indicator_warmup.md
├── math/
│ └── feb_2026.pine # example PineScript indicator
├── modules/
│ ├── dump.module.ts # exchange schema for --dump mode
│ └── pine.module.ts # exchange schema for --pine mode
├── report/
│ └── feb_2026.md # example strategy research report
└── scripts/
└── fetch_docs.mjs # utility: downloads library READMEs into docs/lib/
Automatic Documentation Fetch
After scaffolding, the CLI immediately runs scripts/fetch_docs.mjs inside the new project, which downloads the latest README files for all bundled libraries into docs/lib/:
@@ -1311,6 +1493,130 @@ plot(position, "Position", display=display.data_window)
npm run sync:lib
+🐳 Running in Docker (--docker)
CLI can create a ready-to-use Docker workspace: self-contained directory with docker-compose.yaml and a strategy entry point.
+CLI Flags
+
+
+Flag
+Type
+Description
+
+
+
+
+--docker
+boolean
+Scaffold a Docker workspace
+
+
+--output
+string
+Target directory name (default: backtest-kit-docker)
+
+
+
+Usage
npx @backtest-kit/cli --docker
+
+
+Creates ./backtest-kit-docker/ in the current working directory.
+Override the directory name with --output:
+npx @backtest-kit/cli --docker --output my-docker-workspace
+
+
+The target directory must not exist or must be empty — the command aborts if it contains any files.
+Two Launch Modes
The Docker image entrypoint supports two ways to run a strategy:
+1. command: in docker-compose.yaml
Pin mode and flags directly in the compose file. The entrypoint forwards all arguments 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 environment variables
Pass MODE and STRATEGY_FILE on the command line — no file edits needed:
+MODE=live SYMBOL=TRXUSDT STRATEGY_FILE=./content/feb_2026/feb_2026.strategy.ts docker-compose up -d
+
+
+
+
+
+Variable
+Required
+Default
+Description
+
+
+
+
+MODE
+yes
+—
+backtest | live | paper | walker
+
+
+STRATEGY_FILE
+yes
+—
+Path to strategy entry point (relative to working_dir)
+
+
+SYMBOL
+no
+BTCUSDT
+Trading pair
+
+
+STRATEGY
+no
+first registered
+Strategy name
+
+
+EXCHANGE
+no
+first registered
+Exchange name
+
+
+FRAME
+no
+first registered
+Frame name (backtest only)
+
+
+UI
+no
+—
+Any non-empty value enables --ui
+
+
+TELEGRAM
+no
+—
+Any non-empty value enables --telegram
+
+
+VERBOSE
+no
+—
+Any non-empty value enables --verbose
+
+
+NO_CACHE
+no
+—
+Any non-empty value enables --noCache
+
+
+NO_FLUSH
+no
+—
+Any non-empty value enables --noFlush
+
+
+ENTRY
+no
+—
+Any non-empty value enables multiple symbols from userspace
+
+
+
🌍 Environment Variables
Create a .env file in your project root:
# Telegram notifications (required for --telegram)
CC_TELEGRAM_TOKEN=your_bot_token_here
@@ -1396,7 +1702,7 @@ CC_QUICKCHART_HOST=
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.
🔧 Programmatic API
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);
+run(mode, args)
import { run } from '@backtest-kit/cli';
await run(mode, args);
@@ -1513,20 +1819,20 @@ CC_QUICKCHART_HOST=
Examples
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,
});
+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',
});
+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,
});
+import { run } from '@backtest-kit/cli';
await run('live', {
entryPoint: './src/index.mjs',
symbol: 'BTCUSDT',
verbose: true,
});
💡 Why Use @backtest-kit/cli?
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
+// 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)
@@ -1548,7 +1854,7 @@ CC_QUICKCHART_HOST=
🤝 Contribute
Fork/PR on GitHub.
📜 License
MIT © tripolskypetr
-