Skip to main content

Documentation Index

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

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

Backtest Kit’s risk management system is a pre-signal gate: every time getSignal returns a non-null value, the engine runs it through your registered validation functions before opening any position. Validations receive the pending signal, the current VWAP price, and the full list of currently active positions across all strategies. Throwing an error inside a validation function rejects the signal; returning without error (or returning null/void) allows it through.

How Risk Validation Works

Register a risk profile with addRiskSchema, then reference it by riskName in your strategy schema. The engine instantiates a ClientRisk instance per (riskName, exchangeName, frameName) combination. Multiple strategies sharing the same riskName share the same ClientRisk instance, enabling cross-strategy position counting.
import { addRiskSchema, addStrategySchema } from 'backtest-kit';

addRiskSchema({
  riskName: 'demo',
  validations: [/* ... */],
});

addStrategySchema({
  strategyName: 'my-strategy',
  riskName: 'demo',       // attach this risk profile
  getSignal: async () => { /* ... */ },
  // ...
});
The validations array accepts:
  • Functions matching IRiskValidationFn directly
  • Objects matching IRiskValidation with a validate function and optional note string for documentation

IRiskCheckArgs Fields

Every validation function receives an IRiskValidationPayload which extends IRiskCheckArgs:
symbol
string
required
Trading pair symbol, e.g. "BTCUSDT".
currentSignal
IRiskSignalRow
required
The signal being validated, with all prices resolved. priceOpen is always present (defaults to currentPrice for market entries). Extends IPublicSignalRow so all signal fields — including originalPriceStopLoss, originalPriceTakeProfit, and partialExecuted — are available.
strategyName
string
required
Name of the strategy requesting to open a position.
exchangeName
string
required
Exchange name used for this strategy.
riskName
string
required
Name of the risk profile being applied.
frameName
string
required
Frame name for backtest context; empty string in live mode.
currentPrice
number
required
Current VWAP price at the moment risk is checked.
timestamp
number
required
Current timestamp in milliseconds.
activePositionCount
number
required
Number of currently active positions across all strategies sharing this risk profile.
activePositions
IRiskActivePosition[]
required
Full list of active positions for cross-strategy analysis.

IRiskActivePosition

Each entry in activePositions describes an open trade held by any strategy using this risk profile:
interface IRiskActivePosition {
  strategyName: StrategyName;
  exchangeName: ExchangeName;
  frameName: FrameName;
  symbol: string;
  position: "long" | "short";
  priceOpen: number;
  priceStopLoss: number;
  priceTakeProfit: number;
  minuteEstimatedTime: number;
  openTimestamp: number;
}

Common Validation Patterns

Minimum TP Distance (1%)

Ensure the take-profit target is far enough from entry to cover fees and still make a profit:
({ currentSignal, currentPrice }) => {
  const { priceOpen = currentPrice, priceTakeProfit, position } = currentSignal;
  const tpDistance = position === 'long'
    ? ((priceTakeProfit - priceOpen) / priceOpen) * 100
    : ((priceOpen - priceTakeProfit) / priceOpen) * 100;
  if (tpDistance < 1) {
    throw new Error(`TP too close: ${tpDistance.toFixed(2)}%`);
  }
},

Minimum R/R Ratio (2:1)

Reject signals where the potential loss exceeds half the potential gain:
({ currentSignal, currentPrice }) => {
  const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = currentSignal;
  const reward = position === 'long'
    ? priceTakeProfit - priceOpen
    : priceOpen - priceTakeProfit;
  const risk = position === 'long'
    ? priceOpen - priceStopLoss
    : priceStopLoss - priceOpen;
  if (reward / risk < 2) {
    throw new Error(`Poor R/R ratio: ${(reward / risk).toFixed(2)}`);
  }
},

Maximum Open Positions

Limit the portfolio to 3 open positions at any time across all strategies sharing this risk profile:
({ activePositionCount }) => {
  if (activePositionCount >= 3) {
    throw new Error(`Max 3 positions reached (currently ${activePositionCount})`);
  }
},

Time-Window Restriction

Only trade during UTC business hours (08:00–20:00):
({ timestamp }) => {
  const hour = new Date(timestamp).getUTCHours();
  if (hour < 8 || hour >= 20) {
    throw new Error(`Outside trading window: ${hour}:00 UTC`);
  }
},

Cross-Strategy Symbol Deduplication

Prevent opening a second position on the same symbol if one is already open:
({ symbol, activePositions }) => {
  const alreadyOpen = activePositions.some((p) => p.symbol === symbol);
  if (alreadyOpen) {
    throw new Error(`Already have an open position on ${symbol}`);
  }
},

Full Example

import { addRiskSchema } from 'backtest-kit';

addRiskSchema({
  riskName: 'production',
  validations: [
    // 1. Minimum TP distance
    ({ currentSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, position } = currentSignal;
      const tp = position === 'long'
        ? ((priceTakeProfit - priceOpen) / priceOpen) * 100
        : ((priceOpen - priceTakeProfit) / priceOpen) * 100;
      if (tp < 1) throw new Error(`TP too close: ${tp.toFixed(2)}%`);
    },
    // 2. Minimum R/R ratio
    ({ currentSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = currentSignal;
      const reward = position === 'long' ? priceTakeProfit - priceOpen : priceOpen - priceTakeProfit;
      const risk   = position === 'long' ? priceOpen - priceStopLoss : priceStopLoss - priceOpen;
      if (reward / risk < 2) throw new Error('Poor R/R ratio');
    },
    // 3. Max concurrent positions
    ({ activePositionCount }) => {
      if (activePositionCount >= 5) throw new Error('Max 5 positions reached');
    },
    // 4. Trading hours only
    ({ timestamp }) => {
      const hour = new Date(timestamp).getUTCHours();
      if (hour < 8 || hour >= 22) throw new Error(`Outside trading hours: ${hour}h UTC`);
    },
  ],
  callbacks: {
    onRejected: (symbol, params) => {
      console.warn(`[RISK REJECTED] ${symbol}${params.strategyName}`);
    },
  },
});

listenRisk: Monitoring Rejections

Subscribe to the global risk rejection emitter to log, alert, or record rejected signals. This emitter fires only when a signal is rejected — allowed signals do not emit here.
import { listenRisk } from 'backtest-kit';

listenRisk((event) => {
  console.warn(`[RISK] ${event.symbol} rejected`);
  console.warn(`  Strategy:   ${event.strategyName}`);
  console.warn(`  Reason:     ${event.rejectionNote}`);
  console.warn(`  Active pos: ${event.activePositionCount}`);
  console.warn(`  Price:      ${event.currentPrice}`);
  console.warn(`  Timestamp:  ${new Date(event.timestamp).toISOString()}`);
});

// One-shot listener with filter
listenRiskOnce(
  (event) => event.symbol === 'BTCUSDT' && event.activePositionCount >= 3,
  (event) => console.log('BTC blocked by position limit')
);
Validations throw errors — the error message becomes event.rejectionNote in the risk event. Write descriptive messages like "TP too close: 0.45%" or "Max 3 positions reached" to make monitoring dashboards more useful.

Attaching Multiple Risk Profiles

A strategy can reference multiple risk profiles simultaneously using riskList. All profiles must pass before the signal is accepted:
addStrategySchema({
  strategyName: 'multi-risk-strategy',
  riskList: ['portfolio-limits', 'time-window', 'rr-check'],
  getSignal: async () => { /* ... */ },
  // ...
});
Each profile in riskList is evaluated independently. The first failure stops evaluation and rejects the signal.

Build docs developers (and LLMs) love