Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/theonetrade/ai-trading-mcp/llms.txt

Use this file to discover all available pages before exploring further.

The backtest-kit runtime is assembled from three schema registrations: one for the strategy callbacks, one for the exchange adapter, and one for the MCP feed renderer. Each registration is a single function call. The default strategy in this project uses exactly this pattern and keeps the signal-generation side completely empty — giving you a clean slate to build on.

The Deliberately Empty Default Strategy

content/manual.strategy/manual.strategy.ts registers no entry logic whatsoever. The three callbacks exist only to log events to stdout. Entries happen only when the agent calls open_position; the engine owns every level, limit, and validation.
import { addStrategySchema } from "backtest-kit";

addStrategySchema({
  strategyName: "main_strategy",
  callbacks: {
    onOpen(
      symbol,
      { priceOpen, priceStopLoss, priceTakeProfit, position },
      currentPrice,
    ) {
      console.log("Position opened", {
        symbol,
        position,
        priceOpen,
        priceTakeProfit,
        priceStopLoss,
        currentPrice,
      });
    },
    onClose(
      symbol,
      { priceOpen, priceStopLoss, priceTakeProfit, position },
      currentPrice,
    ) {
      console.log("Position closed", {
        symbol,
        position,
        priceOpen,
        priceTakeProfit,
        priceStopLoss,
        currentPrice,
      });
    },
    onActivePing(
      symbol,
      { priceOpen, priceStopLoss, priceTakeProfit, position },
      currentPrice,
    ) {
      console.log("Position active", {
        symbol,
        position,
        priceOpen,
        priceTakeProfit,
        priceStopLoss,
        currentPrice,
      });
    },
  },
});
The callbacks are for logging and monitoring — they are not the place to generate entry signals. To open a position programmatically from within a strategy, call the engine’s signal API directly. For agent-driven trading the callbacks remain empty and the agent calls open_position through the MCP tools.

addStrategySchema API

addStrategySchema registers the strategy’s name and lifecycle hooks. The engine calls the appropriate callback each time a position transitions state for any symbol in the whitelist.
addStrategySchema({
  strategyName: string,
  callbacks: {
    onOpen(symbol, signal, currentPrice): void,
    onClose(symbol, signal, currentPrice): void,
    onActivePing(symbol, signal, currentPrice): void,
  },
});
Each callback receives the same three arguments:
Called once when a position is confirmed open.
ParameterTypeDescription
symbolstringThe trading pair, e.g. "BTCUSDT"
signal.priceOpennumberActual entry price recorded by the engine
signal.priceStopLossnumberEngine-computed stop-loss level
signal.priceTakeProfitnumberEngine-computed take-profit level
signal.position"long" | "short"Direction of the trade
currentPricenumberLast candle close at the time of the callback
Called once when a position is confirmed closed (take-profit hit, stop-loss hit, or manual close via close_position).
ParameterTypeDescription
symbolstringThe trading pair
signal.priceOpennumberOriginal entry price
signal.priceStopLossnumberStop-loss level at close time
signal.priceTakeProfitnumberTake-profit level at close time
signal.position"long" | "short"Direction of the trade
currentPricenumberPrice at which the close was confirmed
Called on every engine tick while a position is open. Use this for trailing-stop logic, monitoring, or external notifications — not for opening new positions.
ParameterTypeDescription
symbolstringThe trading pair
signal.priceOpennumberOriginal entry price
signal.priceStopLossnumberCurrent stop-loss level
signal.priceTakeProfitnumberCurrent take-profit level
signal.position"long" | "short"Direction of the trade
currentPricenumberCurrent candle close

addExchangeSchema API

addExchangeSchema registers the market-data and order-execution adapter for a named exchange. The engine calls the data functions on every tick; the commit hooks are called only when a real order needs to be placed.
addExchangeSchema({
  exchangeName: string,
  getCandles(symbol, interval, since, limit): Promise<Candle[]>,
  getOrderBook(symbol, depth): Promise<OrderBook>,
  formatPrice(symbol, price): Promise<number>,
  formatQuantity(symbol, quantity): Promise<number>,
  // live mode only:
  onOrderOpenCommit(payload: BrokerOrderOpenPayload): Promise<void>,
  onOrderCloseCommit(payload: BrokerOrderClosePayload): Promise<void>,
});

Paper mode

In paper mode the engine simulates fills internally — onOrderOpenCommit and onOrderCloseCommit are not needed. The paper module (modules/paper.module.ts and content/manual.strategy/modules/paper.module.ts) registers only the data side:
import { addExchangeSchema, roundTicks } from "backtest-kit";
import { singleshot } from "functools-kit";
import ccxt from "ccxt";

const getExchange = singleshot(async () => {
  const exchange = new ccxt.binance({
    options: {
      defaultType: "spot",
      adjustForTimeDifference: true,
      recvWindow: 60000,
    },
    enableRateLimit: true,
  });
  await exchange.loadMarkets();
  return exchange;
});

addExchangeSchema({
  exchangeName: "ccxt-exchange",
  getCandles: async (symbol, interval, since, limit) => {
    const exchange = await getExchange();
    const candles = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
    return candles.map(([timestamp, open, high, low, close, volume]) => ({
      timestamp, open, high, low, close, volume,
    }));
  },
  getOrderBook: async (symbol, depth, _from, _to, backtest) => {
    if (backtest) {
      throw new Error(
        "Order book fetching is not supported in backtest mode for the default exchange schema. Please implement it according to your needs.",
      );
    }
    const exchange = await getExchange();
    const bookData = await exchange.fetchOrderBook(symbol, depth);
    return {
      symbol,
      asks: bookData.asks.map(([price, quantity]) => ({ price: String(price), quantity: String(quantity) })),
      bids: bookData.bids.map(([price, quantity]) => ({ price: String(price), quantity: String(quantity) })),
    };
  },
  formatPrice: async (symbol, price) => {
    const exchange = await getExchange();
    const market = exchange.market(symbol);
    const tickSize = market.limits?.price?.min || market.precision?.price;
    if (tickSize !== undefined) return roundTicks(price, tickSize);
    return exchange.priceToPrecision(symbol, price);
  },
  formatQuantity: async (symbol, quantity) => {
    const exchange = await getExchange();
    const market = exchange.market(symbol);
    const stepSize = market.limits?.amount?.min || market.precision?.amount;
    if (stepSize !== undefined) return roundTicks(quantity, stepSize);
    return exchange.amountToPrecision(symbol, quantity);
  },
});

Live mode

In live mode, onOrderOpenCommit and onOrderCloseCommit implement the actual broker calls. The live module (modules/live.module.ts) adds a Broker.useBrokerAdapter class on top of the same data registration. See the Live Broker page for the full methodology.
import { Broker, OrderTransientError, OrderRejectedError } from "backtest-kit";
import type { IBroker, BrokerOrderOpenPayload, BrokerOrderClosePayload } from "backtest-kit";

Broker.useBrokerAdapter(
  class implements Partial<IBroker> {
    async waitForInit(): Promise<void> {
      await getSpotExchange();
    }

    async onOrderOpenCommit(payload: BrokerOrderOpenPayload): Promise<void> {
      if (payload.backtest) return;
      // ... commit_buy + commit_trade (OCO brackets)
    }

    async onOrderCloseCommit(payload: BrokerOrderClosePayload): Promise<void> {
      if (payload.backtest) return;
      // ... commit_cancel (cancel-sweep + verify + sell free balance)
    }
  },
);

Broker.enable();
Always gate every commit method with if (payload.backtest) return. The same broker adapter instance runs in both paper and live contexts; without the guard a backtest run would attempt real Binance calls.

addMCPSchema API

addMCPSchema registers the hook that composes what the model sees on every get_status call. In this project it is wired in packages/agent/src/config/setup.ts:
import { addMCPSchema } from "backtest-kit";
import ioc from "../lib";

addMCPSchema({
  mcpName: "manual_mcp",
  async getMessages(context, when, mcpName) {
    return await ioc.statusControllerService.getStatus(context, when, mcpName);
  },
});
getMessages returns an array of MCP content blocks — text and image blocks. StatusControllerService composes three sources:
  1. Default engine messages — portfolio snapshot, open positions, unrealized PnL.
  2. Command history — previous open_position / close_position calls and their outcomes.
  3. Telegram feed — the last 15 posts from the configured channel as text blocks, with chart screenshots as MCP image blocks.
The composition is wrapped in queued() so concurrent get_status calls line up rather than stampede the Telegram client.

Project Layout for Custom Strategies

1

Create a strategy folder under content/

content/
  your.strategy/
    your.strategy.ts    ← entry point
    modules/
      paper.module.ts   ← exchange adapter (paper)
      live.module.ts    ← exchange adapter (live)
    session.txt         ← GramJS session (gitignored)
    dump/               ← created at runtime
The strategy file’s folder becomes the process working directory at runtime. session.txt, dump/mcp/, dump/images/, and dump/report/ all resolve relative to it.
2

Register your schema in the strategy entry point

Import the exchange module, then call addStrategySchema. Both registrations happen at module load time before the engine starts.
import "./modules/paper.module.ts"; // or live.module.ts
import { addStrategySchema } from "backtest-kit";

addStrategySchema({
  strategyName: "your_strategy",
  callbacks: { onOpen, onClose, onActivePing },
});
3

Pass --entry to the CLI

npm start -- --paper --entry ./content/your.strategy/your.strategy.ts --ui
For live mode:
npm start -- --live --entry ./content/your.strategy/your.strategy.ts --ui

How the runtime resolves modules

config/loader.config.ts imports @pro/agent and @pro/main into the CLI runtime at startup:
import "@pro/agent";
import "@pro/main";
config/alias.config.ts maps the @pro/* aliases to the built package outputs:
export default {
  "@pro/agent": require("../packages/agent/build/index.cjs"),
  "@pro/main":  require("../packages/main/build/index.cjs"),
}
Run npm run build after any change to packages/agent or packages/main — the CLI loads the compiled .cjs files, not the TypeScript sources.
Keep custom strategies inside content/ subdirectories. The CLI resolves the working directory from the strategy file’s folder, and the dump viewer on :60050 expects dump/ to live there. Placing strategy files outside content/ does not break the engine but makes the audit trail harder to locate.

Build docs developers (and LLMs) love