Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-docs/llms.txt

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

The broker adapter is Backtest Kit’s integration point between the framework’s internal position state machine and a real exchange. Every trade mutation — opening a position, closing at TP/SL, adjusting a trailing stop, adding a DCA entry — fires a corresponding hook on the adapter before the framework mutates its internal state. If the adapter throws (exchange rejection, network timeout, rate limit), the mutation is skipped and the internal state is left exactly as it was. The framework retries automatically on the next tick. This transactional guarantee means a failing exchange can never desync the engine’s ledger from what actually happened on the exchange.

The IBroker Interface

The broker adapter implements eight methods corresponding to the eight position events the framework can emit. All methods are optional (Partial<IBroker>) — unimplemented hooks are no-ops.
MethodFired when
onSignalOpenCommitA new position is being activated (entry order)
onSignalCloseCommitPosition is being closed (SL/TP hit or manual close)
onPartialProfitCommitA partial profit close is being executed
onPartialLossCommitA partial loss close is being executed
onTrailingStopCommitThe stop-loss price is being adjusted upward (trailing)
onTrailingTakeCommitThe take-profit price is being adjusted
onBreakevenCommitThe stop-loss is being moved to the entry price
onAverageBuyCommitA DCA entry is being added to the position
An optional waitForInit() method can be used to warm up exchange connections before the first tick fires.

Minimal Broker Adapter

The minimal adapter handles the two most common operations — opening and closing positions:
import {
  Broker,
  IBroker,
  BrokerSignalOpenPayload,
  BrokerSignalClosePayload,
} from 'backtest-kit';

class MyBroker implements Partial<IBroker> {

  async waitForInit(): Promise<void> {
    // Pre-warm exchange connection, load markets, etc.
    // Called once on startup before any hook fires.
  }

  async onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void> {
    const { symbol, cost, priceOpen, priceTakeProfit, priceStopLoss, position } = payload;
    // Place entry order on exchange.
    // Throw to prevent the open from being recorded internally.
  }

  async onSignalCloseCommit(payload: BrokerSignalClosePayload): Promise<void> {
    const { symbol, currentPrice } = payload;
    // Place close order on exchange.
    // Throw to prevent the close from being recorded internally.
  }
}

Broker.useBrokerAdapter(MyBroker);
Broker.enable();
Pass the class constructor to useBrokerAdapter, not an instance. The framework instantiates it internally and manages the lifecycle.

Full Broker Hook Reference

async onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void> {
  const {
    symbol,            // trading pair, e.g. 'BTCUSDT'
    cost,              // position size in quote currency (e.g. $100 USDT)
    priceOpen,         // target entry price (VWAP-aligned)
    priceTakeProfit,   // TP level
    priceStopLoss,     // SL level
    position,          // 'long' | 'short'
  } = payload;
  // Place limit or market entry order.
  // Place TP and SL orders after fill.
  // If entry times out, throw — position state is not mutated.
}
async onSignalCloseCommit(payload: BrokerSignalClosePayload): Promise<void> {
  const {
    symbol,
    position,          // 'long' | 'short'
    currentPrice,      // target close price
    priceTakeProfit,   // current TP (for restore if close times out)
    priceStopLoss,     // current SL (for restore if close times out)
  } = payload;
  // Cancel existing SL/TP orders, then place close order.
  // If position was already closed by exchange-side SL/TP, throw gracefully.
}
async onPartialProfitCommit(payload: BrokerPartialProfitPayload): Promise<void> {
  const {
    symbol,
    percentToClose,    // percentage of current position to close (0–100)
    currentPrice,      // target close price for the partial
    position,
    priceTakeProfit,
    priceStopLoss,
  } = payload;
  // Cancel existing SL/TP, sell `percentToClose`% of position.
  // Restore SL/TP on the remaining qty after fill.
}
async onPartialLossCommit(payload: BrokerPartialLossPayload): Promise<void> {
  const {
    symbol,
    percentToClose,
    currentPrice,
    position,
    priceTakeProfit,
    priceStopLoss,
  } = payload;
  // Same pattern as partial profit but triggered at a loss level.
}
async onTrailingStopCommit(payload: BrokerTrailingStopPayload): Promise<void> {
  const {
    symbol,
    newStopLossPrice,  // new SL price (higher than original for long)
    position,
  } = payload;
  // Cancel the existing SL order and place a new one at newStopLossPrice.
}
async onTrailingTakeCommit(payload: BrokerTrailingTakePayload): Promise<void> {
  const {
    symbol,
    newTakeProfitPrice,
    position,
  } = payload;
  // Cancel the existing TP order and place a new one at newTakeProfitPrice.
}
async onBreakevenCommit(payload: BrokerBreakevenPayload): Promise<void> {
  const {
    symbol,
    newStopLossPrice,  // equal to original entry price
    position,
  } = payload;
  // Cancel existing SL, place new SL at the breakeven (entry) price.
}
async onAverageBuyCommit(payload: BrokerAverageBuyPayload): Promise<void> {
  const {
    symbol,
    currentPrice,      // DCA entry price
    cost,              // additional cost in quote currency
    position,
    priceTakeProfit,   // updated TP for the new combined position
    priceStopLoss,     // updated SL for the new combined position
  } = payload;
  // Cancel existing SL/TP, place DCA entry, restore SL/TP on total position.
}

Registering via Module Hook

The cleanest way to register a broker adapter with @backtest-kit/cli is through a mode-specific module file. The CLI loads ./modules/<mode>.module.ts as a side-effect import before the run starts. Create modules/live.module.ts:
import ccxt from 'ccxt';
import { singleshot } from 'functools-kit';
import {
  Broker,
  IBroker,
  BrokerSignalOpenPayload,
  BrokerSignalClosePayload,
} from 'backtest-kit';

const getExchange = singleshot(async () => {
  const exchange = new ccxt.binance({
    apiKey:  process.env.BINANCE_API_KEY,
    secret:  process.env.BINANCE_API_SECRET,
    options: { defaultType: 'future', adjustForTimeDifference: true },
    enableRateLimit: true,
  });
  await exchange.loadMarkets();
  return exchange;
});

Broker.useBrokerAdapter(
  class implements Partial<IBroker> {
    async waitForInit() {
      await getExchange();
    }

    async onSignalOpenCommit(payload: BrokerSignalOpenPayload) {
      const { symbol, cost, priceOpen, priceTakeProfit, priceStopLoss, position } = payload;
      const exchange = await getExchange();
      // ... place entry + TP + SL orders
    }

    async onSignalCloseCommit(payload: BrokerSignalClosePayload) {
      const { symbol, currentPrice } = payload;
      const exchange = await getExchange();
      // ... cancel open orders, close position
    }
  }
);

Broker.enable();
The CLI maps mode flags to module files:
CLI flagModule loaded
--backtestmodules/backtest.module.ts
--papermodules/paper.module.ts
--livemodules/live.module.ts
A missing module file is a soft warning — the run continues without a broker adapter (no real orders placed).

Rollback on Exchange Failure

The transactional guarantee is what makes the broker adapter production-safe:
1

Adapter hook fires before state mutation

The framework calls e.g. onSignalOpenCommit with the full payload.
2

Exchange rejects the order (or network times out)

The adapter throws an Error. The exchange has not filled the order.
3

Framework catches the error and rolls back

The internal position state is left exactly as it was before the hook was called. No opened event is emitted. The signal remains in its previous state (e.g. idle).
4

Automatic retry on the next tick

On the next strategy interval, the framework evaluates the signal again. If getSignal returns the same entry parameters, onSignalOpenCommit fires again. A transient exchange error heals itself without any manual intervention.
You can deliberately test this rollback path by registering a broker that always throws:
import { Broker, IBroker, BrokerSignalOpenPayload } from 'backtest-kit';

Broker.useBrokerAdapter(
  class implements Partial<IBroker> {
    async onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void> {
      console.log('SIGNAL_OPEN', payload);
      throw new Error('Exchange not available — rollback test');
    }
  }
);

Broker.enable();
The UI will surface the thrown message back to the operator. The engine’s position state will remain unchanged, confirming the transactional guarantee.

Backtest Mode Behaviour

The broker adapter is automatically bypassed in backtest mode. All eight hooks are no-ops during historical replay — no exchange API calls are made, no orders are placed, and no real capital is at risk. Registering a broker adapter in a backtest module file has no effect. This is intentional: the framework isolates exchange code from historical simulations to prevent accidental order placement during development.

Build docs developers (and LLMs) love