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.

Event listeners are the primary way to react to strategy events and control position management in Backtest Kit. They decouple your observation logic from your strategy logic: a strategy emits events as it runs, and any number of listeners can react to those events independently. All listener callbacks are wrapped with queued from functools-kit, ensuring sequential async execution — your callback for event N completes before event N+1 is delivered, regardless of how long it takes. Each listen* function returns an unsubscribe function. Call it to stop receiving events.

Signal Lifecycle Listeners

These listeners fire for every signal tick result — every state transition in the idle → opened → active → closed lifecycle.

listenSignal(callback)

Subscribes to all signal events from both backtest and live runs.
import { listenSignal } from 'backtest-kit';

const unsubscribe = listenSignal(async (event) => {
  console.log(event.action, event.symbol, event.strategyName);
});

// Later: unsubscribe();
callback
(event: IStrategyTickResult) => Promise<void>
required
Called for every tick result across all symbols and strategies, in both backtest and live mode.

listenSignalBacktest(callback)

Subscribes only to events emitted during backtest runs. Events from Live.background() are not delivered.
import { listenSignalBacktest } from 'backtest-kit';

listenSignalBacktest(async (event) => {
  if (event.action === 'closed') {
    console.log('Backtest trade closed:', event.pnl, event.closeReason);
  }
});

listenSignalLive(callback)

Subscribes only to events emitted during live trading runs. Events from Backtest.background() are not delivered.
import { listenSignalLive } from 'backtest-kit';

listenSignalLive(async (event) => {
  if (event.action === 'opened') {
    // Send Telegram alert, update dashboard, etc.
  }
});

listenSignalOnce(filterFn, callback)

Fires exactly once when the first event matching filterFn is received. Unsubscribes automatically after the first match.
import { listenSignalOnce } from 'backtest-kit';

listenSignalOnce(
  (event) => event.action === 'closed' && event.symbol === 'BTCUSDT',
  async (event) => {
    console.log('First BTCUSDT close:', event.pnl);
  }
);
filterFn
(event: IStrategyTickResult) => boolean
required
Predicate function. The callback only fires when this returns true.
callback
(event: IStrategyTickResult) => Promise<void>
required
Executed exactly once on the first matching event.

listenSignalBacktestOnce(filterFn, callback) / listenSignalLiveOnce(filterFn, callback)

Same as listenSignalOnce but scoped to backtest-only or live-only events respectively.

Position Ping Listeners

Ping listeners fire on a per-minute heartbeat while a signal is in a specific state. They are the primary hook for DCA logic, partial profit-taking, trailing stop management, and breakeven adjustments.

listenActivePing(callback)

Fires on every tick while a position is active (open, monitoring TP/SL). Use this for ongoing position management logic.
import {
  listenActivePing,
  commitPartialProfit,
  commitBreakeven,
  getPositionPnlPercent,
} from 'backtest-kit';

listenActivePing(async ({ symbol, currentPrice, data, timestamp }) => {
  const pnl = await getPositionPnlPercent(symbol);
  if (pnl === null) return;

  if (pnl > 5) {
    await commitPartialProfit(symbol, 50);  // take 50% profit at +5%
  }

  if (pnl > 1.5) {
    await commitBreakeven(symbol);  // move SL to entry once safely in profit
  }
});
callback
(event: ActivePingContract) => Promise<void>
required
Receives an ActivePingContract with the following fields:
  • symbol — trading pair
  • currentPrice — VWAP at the time of the ping
  • data — the full active signal row (ISignalRow)
  • strategyName, exchangeName, frameName
  • backtesttrue during backtesting
  • timestamp — ping timestamp in milliseconds

listenIdlePing(callback)

Fires on every tick when no position is active for the given symbol/strategy combination. Use for pre-signal analysis, warm-up logic, or running idle checks.
import { listenIdlePing } from 'backtest-kit';

listenIdlePing(async ({ symbol, currentPrice, timestamp }) => {
  // Check conditions when no trade is open
  console.log(`${symbol} idle at ${currentPrice}`);
});
callback
(event: IdlePingContract) => Promise<void>
required
Receives an IdlePingContract with symbol, currentPrice, strategyName, exchangeName, frameName, backtest, and timestamp.

listenSchedulePing(callback)

Fires on every tick while a signal is in the scheduled state — waiting for the price to reach priceOpen before the position opens. Use for monitoring limit-entry orders or cancelling stale scheduled signals.
import { listenSchedulePing, getPositionWaitingMinutes, commitCancelScheduled } from 'backtest-kit';

listenSchedulePing(async ({ symbol, currentPrice, data }) => {
  const waitMinutes = await getPositionWaitingMinutes(symbol);
  if (waitMinutes !== null && waitMinutes > 240) {
    // Cancel if scheduled signal has been waiting more than 4 hours
    await commitCancelScheduled(symbol, { note: 'Timed out waiting for entry' });
  }
});
callback
(event: SchedulePingContract) => Promise<void>
required
Receives a SchedulePingContract with symbol, currentPrice, data (the scheduled signal), strategyName, exchangeName, frameName, backtest, and timestamp.

Once Variants for Ping Listeners

All three ping listeners have Once variants that fire exactly once when the filter condition is met:
import { listenActivePingOnce, listenIdlePingOnce, listenSchedulePingOnce } from 'backtest-kit';

listenActivePingOnce(
  ({ symbol }) => symbol === 'BTCUSDT',
  async ({ symbol, currentPrice }) => {
    console.log('First BTCUSDT active ping at', currentPrice);
  }
);

System Event Listeners

listenError(callback)

Fires when a non-fatal error occurs during background execution (e.g. a failed getCandles call, a rejected signal due to validation, or a failed commit retry). The strategy continues running after these errors.
import { listenError } from 'backtest-kit';

listenError((error) => {
  console.error('Strategy error:', error.message);
  // Send to Sentry, DataDog, etc.
});
callback
(error: Error) => void
required
Receives the Error object. Not async — errors in this callback are not caught.

listenRisk(callback)

Fires when a signal is rejected by a risk validation. Useful for monitoring your risk rules in production, alerting when trades are being blocked, or auditing the rejection reasons.
import { listenRisk } from 'backtest-kit';

listenRisk(async (event) => {
  console.log(
    `Signal rejected for ${event.symbol} by "${event.strategyName}": ${event.rejectionNote}`
  );
  console.log(`Active positions at time of rejection: ${event.activePositionCount}`);
});
callback
(event: RiskContract) => Promise<void>
required
Receives a RiskContract with symbol, currentSignal, strategyName, exchangeName, frameName, currentPrice, activePositionCount, rejectionNote, rejectionId, timestamp, and backtest.

listenDone(callback) / listenDoneBacktest(callback) / listenDoneLive(callback)

Fires when a background run completes. For Backtest.background(), this fires after the frame window is exhausted. For Live.background(), this fires after the stop function is called and any open position has closed gracefully.
import { listenDoneBacktest, Backtest } from 'backtest-kit';

listenDoneBacktest(async (event) => {
  console.log(`Backtest done: ${event.symbol} / ${event.strategyName}`);
  await Backtest.dump(event.symbol, event.strategyName);
});
callback
(event: DoneContract) => Promise<void>
required
Receives a DoneContract with symbol, strategyName, exchangeName, frameName, and backtest.

listenDoneOnce(callback) / listenDoneBacktestOnce(filterFn, callback) / listenDoneLiveOnce(filterFn, callback)

Fires exactly once. The BacktestOnce and LiveOnce variants accept a filter predicate.
import { listenDoneBacktestOnce, Backtest } from 'backtest-kit';

listenDoneBacktestOnce(
  (event) => event.strategyName === 'my-strategy',
  async (event) => {
    const stats = await Backtest.getData(event.strategyName);
    console.log('Final Sharpe:', stats.sharpeRatio);
  }
);

listenProgress(callback) / listenBacktestProgress(callback)

Fires periodically during a backtest with a progress percentage. Useful for progress bars in CLIs or UI dashboards.
import { listenBacktestProgress } from 'backtest-kit';

listenBacktestProgress((event) => {
  process.stdout.write(`\rBacktest progress: ${event.progress.toFixed(1)}%`);
});
callback
(event: ProgressBacktestContract) => void
required
Receives a ProgressBacktestContract with symbol, strategyName, exchangeName, frameName, totalFrames, processedFrames, and progress (0–100).

Usage Patterns

DCA ladder with partial exits

import {
  listenActivePing,
  commitAverageBuy,
  commitPartialProfit,
  commitBreakeven,
  getPositionPnlPercent,
  getPositionInvestedCount,
  getPositionEntryOverlap,
} from 'backtest-kit';

listenActivePing(async ({ symbol, currentPrice }) => {
  const pnl = await getPositionPnlPercent(symbol);
  const count = await getPositionInvestedCount(symbol);
  if (pnl === null || count === null) return;

  // DCA: add entry if down and no overlap within 3%
  if (pnl < -2 && count < 4) {
    const overlap = await getPositionEntryOverlap(symbol, currentPrice, {
      upperPercent: 3,
      lowerPercent: 3,
    });
    if (!overlap) await commitAverageBuy(symbol);
  }

  // Partial exits on the way up
  if (pnl > 5)  await commitPartialProfit(symbol, 30);  // take 30% at +5%
  if (pnl > 10) await commitPartialProfit(symbol, 50);  // take 50% of remaining at +10%
  if (pnl > 1.5) await commitBreakeven(symbol);
});

Auto-report on backtest completion

import { listenDoneBacktest, Backtest } from 'backtest-kit';

listenDoneBacktest(async (event) => {
  await Backtest.dump(event.symbol, event.strategyName);
  const stats = await Backtest.getData(event.strategyName);
  console.log(`Win rate: ${(stats.winRate * 100).toFixed(1)}%`);
  console.log(`Sharpe: ${stats.sharpeRatio?.toFixed(2)}`);
  console.log(`Total PnL: ${stats.totalPnl?.toFixed(2)}%`);
});

Build docs developers (and LLMs) love