An agent-native Node.js trading engine: research, backtest, and trade live through one signal contract.
A Node.js toolkit for testing, validating, and operating trading strategies, built so humans and AI agents work from the same primitives.
One signal() contract runs across research and execution:
- run candle or tick backtests
- model slippage, commissions, borrow, carry, and funding
- validate parameters with walk-forward tests and research statistics
- combine multiple systems into a shared-capital portfolio
- move the same strategy into paper or live execution, single or multi-symbol
- export reports, metrics, and trade ledgers
Agent-native. The tradelab-mcp server exposes 25 tools over stdio, so an AI agent can run the whole loop itself: pull data, run and score backtests, track hypotheses across runs with built-in overfitting guards, then open a paper or live session and place risk-sized bracket orders behind a kill-switch. Agents get the same depth a quant does, not a thin read-only wrapper. See docs/mcp.md.
npm install tradelabRequires Node.js 18 or newer.
import { backtest, getHistoricalCandles, ema, exportBacktestArtifacts } from "tradelab";
const candles = await getHistoricalCandles({
source: "yahoo",
symbol: "SPY",
interval: "1d",
period: "2y",
cache: true,
});
const result = backtest({
candles,
symbol: "SPY",
interval: "1d",
equity: 10_000,
riskPct: 1,
warmupBars: 50,
costs: {
slippageBps: 1,
commissionBps: 0.5,
},
signal({ candles: history, bar }) {
const closes = history.map((c) => c.close);
const fast = ema(closes, 10);
const slow = ema(closes, 30);
const i = closes.length - 1;
if (fast[i - 1] <= slow[i - 1] && fast[i] > slow[i]) {
return { side: "long", stop: bar.close * 0.97, rr: 2 };
}
return null;
},
});
console.log(result.metrics);
exportBacktestArtifacts({ result, outDir: "./output" });Start with result.metrics for the summary and result.positions for completed trades. Use trades when you need every realized leg, including partial exits.
| Goal | API or command |
|---|---|
| Backtest one strategy | backtest({ candles, signal }) |
| Backtest an async strategy | backtestAsync({ candles, signal }) |
| Replay tick or quote data | backtestTicks({ ticks, signal }) |
| Run several systems together | backtestPortfolio({ systems }) |
| Test parameter stability | walkForwardOptimize(options) |
| Run a parallel parameter sweep | optimize({ signalModulePath, parameterSets }) |
| Use indicators | import { rsi, macd, vwap } from "tradelab/ta" |
| Check overfitting risk | research.monteCarlo, research.deflatedSharpe |
| Run in paper or live mode | LiveEngine, LiveOrchestrator, tradelab paper |
| Trade multiple symbols in one session | SessionManager.create({ symbols: ["BTC","ETH"] }) with per-symbol pushBar and placeOrder |
| Watch a live run locally | createDashboardServer({ source }) with equity curve, KPI strip, controls |
| Get notified on fills or risk halts | attachNotifier(session, { onEvent, webhookUrl }) from tradelab/live |
| Let MCP clients run research tools | tradelab-mcp with run_backtest, walk_forward, analyze_robustness, optimize_strategy, compare_strategies, candle_stats |
| Let MCP agents trade (paper/live) | tradelab-mcp with create_session, feed_price, place_order, bracket orders, halt_all kill-switch (see docs/mcp.md) |
| Track strategy research across runs | tradelab-mcp with research_open, research_log, research_recall, research_close (see docs/mcp.md) |
| Summarize metrics in plain English | summarize(metrics) returns one plain-English paragraph |
| Run a built-in preset from the CLI | tradelab run ema-cross --source yahoo --symbol SPY --period 1y |
| Export reports and machine data | exportBacktestArtifacts, exportMetricsJSON |
Your strategy is a function. Return null to do nothing, or return a trade signal.
function signal({ candles, index, bar, equity, openPosition, pendingOrder }) {
if (openPosition || index < 50) return null;
return {
side: "long",
entry: bar.close, // optional; defaults to current close
stop: bar.close - 2,
rr: 2, // take profit at 2R
};
}Common signal fields:
| Field | Meaning |
|---|---|
side |
long, short, buy, or sell |
entry |
Entry price. Defaults to the current close |
stop |
Required stop level for sizing and risk |
takeProfit |
Explicit target price |
rr |
Builds target from risk when takeProfit is absent |
qty or size |
Fixed size override |
riskPct or riskFraction |
Per-trade risk override |
Use getHistoricalCandles() for Yahoo Finance, CSV files, and cached datasets.
const yahoo = await getHistoricalCandles({
source: "yahoo",
symbol: "QQQ",
interval: "1d",
period: "1y",
cache: true,
});
const csv = await getHistoricalCandles({
source: "csv",
csvPath: "./data/btc.csv",
});Candles are normalized to:
{
(time, open, high, low, close, volume);
}Cost assumptions belong in the run, not in post-processing.
const result = backtest({
candles,
signal,
costs: {
slippageBps: 2,
spreadBps: 1,
commissionBps: 1,
minCommission: 1,
carry: {
longAnnualBps: 500,
shortAnnualBps: 800,
},
funding: {
rateBps: 10,
intervalMs: 8 * 60 * 60 * 1000,
anchorMs: 0,
},
},
});exit.financing is included on closed trades when carry or funding applies. It is already deducted from exit.pnl and aggregate metrics.
Use a normal backtest to build the strategy. Use validation tools before trusting it.
import { walkForwardOptimize, grid } from "tradelab";
const wf = walkForwardOptimize({
candles,
trainBars: 180,
testBars: 60,
mode: "anchored",
scoreBy: "profitFactor",
parameterSets: grid({
fast: [8, 10, 12],
slow: [21, 30, 50],
rr: [1.5, 2, 3],
}),
signalFactory(params) {
return createEmaSignal(params);
},
});
console.log(wf.metrics);
console.log(wf.bestParamsSummary);For larger sweeps, use optimize() with a strategy module:
const out = await optimize({
candles,
interval: "1d",
signalModulePath: new URL("./strategy.js", import.meta.url).pathname,
parameterSets: grid({ fast: [8, 10], slow: [30, 50] }),
scoreBy: "sharpeAnnualized",
});backtestPortfolio() runs multiple systems against shared capital. Capital is locked only when an order fills, so later systems size against what is still available.
const portfolio = backtestPortfolio({
equity: 100_000,
interval: "1d",
maxDailyLossPct: 3,
systems: [
{ symbol: "SPY", candles: spy, signal: spySignal, weight: 2 },
{ symbol: "QQQ", candles: qqq, signal: qqqSignal, weight: 1 },
],
});Portfolio equity points include lockedCapital and availableCapital.
The live package uses the same signal shape as backtests.
import { LiveEngine, PaperEngine, JsonFileStorage } from "tradelab/live";
const engine = new LiveEngine({
id: "aapl-1m",
symbol: "AAPL",
interval: "1m",
mode: "polling",
broker: new PaperEngine({ equity: 25_000 }),
storage: new JsonFileStorage({ baseDir: "./output/live-state" }),
signal,
});
await engine.start();Run the same flow from the terminal:
tradelab paper --symbol AAPL --interval 1m --mode polling --once true
tradelab live --config ./live-portfolio.json --paperAdd --dashboard --dashboardPort 4317 to open a local Server-Sent Events dashboard.
tradelab-mcp exposes 25 tools over stdio to any MCP-capable agent (Claude Desktop, Cursor, and similar). They cover the full loop an agent needs to work a strategy end to end: research it, validate it, track the search, then trade it. See docs/mcp.md for the full tool reference and agent trading guide.
Research tools: list_strategies, fetch_candles, run_backtest, walk_forward, analyze_robustness, optimize_strategy, compare_strategies, candle_stats
Research loop tools: research_open, research_log, research_recall, research_close for persistent file-backed hypothesis tracking. run_backtest auto-logs when researchId is passed.
Agent trading tools (paper by default; live gated): create_session, list_sessions, session_status, feed_price, place_order, close_position, flatten, cancel_order, account, positions, recent_events, attach_strategy, halt_all
create_session accepts a symbols array for multi-symbol portfolio sessions. Pass symbol to feed_price and place_order to direct bars and orders to a specific instrument.
Paper trading needs no credentials. Live trading requires TRADELAB_ALLOW_LIVE=true and confirmLive: true plus a credentialed broker. halt_all is an emergency kill-switch that flattens all positions and stops every session.
Use it from any MCP client that can launch a stdio server:
{
"mcpServers": {
"tradelab": {
"command": "npx",
"args": ["-y", "tradelab", "tradelab-mcp"]
}
}
}tradelab backtest --source yahoo --symbol SPY --interval 1d --period 1y
tradelab portfolio --csvPaths ./spy.csv,./qqq.csv --symbols SPY,QQQ
tradelab walk-forward --source yahoo --symbol QQQ --interval 1d --period 2y
tradelab run ema-cross --source yahoo --symbol SPY --period 1y
tradelab status --dir ./output/live-statetradelab run <preset> runs a named built-in strategy on Yahoo or CSV data and prints a plain-English summary. Pass --params '{"fast":5,"slow":20}' to override defaults.
- Docs home
- Backtesting
- Data, reporting, and CLI
- Live trading
- MCP server
- Research tools
- Strategy examples
- API reference
import { backtest, getHistoricalCandles } from "tradelab";
import { rsi, macd, vwap } from "tradelab/ta";
import { LiveEngine, PaperEngine, TradingSession, SessionManager } from "tradelab/live";CommonJS is supported for the main, data, live, and TA entry points:
const { backtest } = require("tradelab");MIT