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.

addRiskSchema registers a named risk profile that gates signal acceptance before any position is opened. When a strategy references a riskName, Backtest Kit runs all validation functions in that profile against the incoming signal. Any validation that throws—or returns an IRiskRejectionResult—rejects the signal and emits a riskSubject event. Returning void or null from a validation approves the signal. Multiple strategies can share the same risk profile, and a single strategy can reference multiple profiles via riskList. The ClientRisk instance tracking active positions is shared across all strategies that reference the same riskName, enabling cross-strategy portfolio-level limits.

Function Signature

addRiskSchema(riskSchema: IRiskSchema): void

Parameters

riskName
string (RiskName)
required
A unique string identifier for this risk profile. Referenced as riskName in addStrategySchema and as riskList entries. Duplicate names throw at registration.
validations
Array<IRiskValidation | IRiskValidationFn>
required
Array of validation rules. Each entry is either:
  • A plain function (payload: IRiskValidationPayload) => RiskRejection | Promise<RiskRejection>
  • An object { validate: IRiskValidationFn, note?: string } with an optional description.
Validations run in array order. The first rejection stops evaluation.
note
string
Optional developer note for documentation or introspection.
callbacks
Partial<IRiskCallbacks>
Optional lifecycle callbacks:
  • onRejected(symbol, params) — called when a signal is rejected.
  • onAllowed(symbol, params) — called when a signal passes all validations.

IRiskValidationPayload — Validation Context

Each validation function receives this payload:
symbol
string
Trading pair symbol (e.g., "BTCUSDT").
currentSignal
IRiskSignalRow
The signal being evaluated. Includes position, priceOpen, priceTakeProfit, priceStopLoss, minuteEstimatedTime, and cost. priceOpen is always present (Backtest Kit calculates the effective entry before running validations).
strategyName
string
The strategy requesting the position.
exchangeName
string
The exchange on which the signal will be executed.
riskName
string
The risk profile being evaluated.
frameName
string
The frame for backtest runs; empty string for live mode.
currentPrice
number
Current VWAP price at the moment of validation.
timestamp
number
Unix timestamp in milliseconds of the current tick.
activePositionCount
number
Total number of open positions tracked across all strategies sharing this risk profile. Use this to enforce portfolio-wide position limits.
activePositions
IRiskActivePosition[]
Full list of currently active positions with strategyName, exchangeName, symbol, position, priceOpen, priceStopLoss, priceTakeProfit, minuteEstimatedTime, and openTimestamp.

Example — TP Distance and R/R Ratio Validations

import { addRiskSchema } from 'backtest-kit';

addRiskSchema({
  riskName: 'conservative',

  validations: [
    // Validation 1: TP must be at least 1% from entry
    {
      validate: ({ 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 1%)`);
        }
      },
      note: 'Minimum 1% take-profit distance',
    },

    // Validation 2: Risk/Reward ratio must be at least 2:1
    {
      validate: ({ 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 (risk <= 0 || reward / risk < 2) {
          throw new Error(`Poor R/R ratio: ${(reward / risk).toFixed(2)} (minimum 2.0)`);
        }
      },
      note: 'Minimum 2:1 reward-to-risk ratio',
    },

    // Validation 3: No more than 3 open positions across the portfolio
    ({ activePositionCount }) => {
      if (activePositionCount >= 3) {
        throw new Error(`Portfolio limit reached: ${activePositionCount} active positions`);
      }
    },
  ],

  callbacks: {
    onRejected: (symbol, params) => {
      console.warn(`[RISK] Signal rejected for ${symbol} (${params.strategyName})`);
    },
    onAllowed: (symbol, params) => {
      console.debug(`[RISK] Signal approved for ${symbol} (${params.strategyName})`);
    },
  },
});
Then reference the profile in a strategy:
import { addStrategySchema } from 'backtest-kit';

addStrategySchema({
  strategyName: 'my-strategy',
  interval: '5m',
  riskName: 'conservative',   // all three validations run before any signal is accepted
  getSignal: async (symbol) => { /* ... */ },
});
Throwing an error inside a validation function is the recommended rejection pattern. The error message becomes the rejectionNote on the emitted RiskContract event and is accessible via listenRisk. You can also return an IRiskRejectionResult object { id: string | null, note: string } for structured rejection data.

Build docs developers (and LLMs) love