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.

Risk management in Backtest Kit operates at the portfolio level — validation runs across all active strategies and symbols before any signal is accepted into the lifecycle. Rather than embedding risk rules inside individual strategies (where they can be forgotten, duplicated, or inconsistently applied), the framework provides a dedicated risk schema layer that intercepts every pending signal before position entry. Rejection is logged automatically, strategies continue without crashing, and rejection statistics are available for analysis.

Define a Risk Profile

A risk profile is a named collection of validation functions registered with addRiskSchema. Each validation receives the pending signal alongside current market state and throws an error to reject the signal.
import { addRiskSchema } from 'backtest-kit';

addRiskSchema({
  riskName: 'conservative',
  validations: [
    // Enforce minimum 2:1 reward-to-risk ratio
    ({ pendingSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = pendingSignal;

      const reward = position === 'long'
        ? priceTakeProfit - priceOpen
        : priceOpen - priceTakeProfit;

      const risk = position === 'long'
        ? priceOpen - priceStopLoss
        : priceStopLoss - priceOpen;

      if (reward / risk < 2) {
        throw new Error('Risk/reward ratio must be at least 2:1');
      }
    },

    // Reject signals where TP is less than 1% away
    ({ pendingSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, position } = pendingSignal;

      const tpDistance = position === 'long'
        ? ((priceTakeProfit - priceOpen) / priceOpen) * 100
        : ((priceOpen - priceTakeProfit) / priceOpen) * 100;

      if (tpDistance < 1) {
        throw new Error(`TP too close: ${tpDistance.toFixed(2)}%`);
      }
    },
  ],
});
Validations run in order. If any function throws, the signal is rejected — subsequent validations in the list are not evaluated. The rejection is recorded with the error message for later analysis.

Attach a Risk Profile to a Strategy

The riskName field on addStrategySchema connects the strategy to a registered risk profile. Every signal produced by this strategy must pass all validations in the named profile before being accepted.
import { addStrategySchema } from 'backtest-kit';

addStrategySchema({
  strategyName: 'my-strategy',
  interval: '15m',
  riskName: 'conservative',   // ← links to the risk profile above
  getSignal: async (symbol) => {
    // ...signal generation logic...
  },
});
A single risk profile can be shared across multiple strategies, making it straightforward to enforce consistent portfolio-level rules across an entire multi-symbol, multi-strategy deployment.

What Each Validation Receives

Each validation function is called with a context object containing everything needed to evaluate the pending signal against portfolio state:
FieldTypeDescription
pendingSignalISignalRowThe proposed signal: entry price, TP, SL, direction, estimated time
currentPricenumberVWAP at the time of validation (last 5 one-minute candles)
symbolstringThe trading pair the signal was generated for
backtestbooleanWhether validation is running inside a backtest
priceOpen in pendingSignal may be undefined if the strategy did not specify a limit entry, in which case currentPrice is the correct fallback (as shown in the examples above).

Common Validation Patterns

Minimum R/R Ratio

Reject any signal where the potential reward does not justify the risk taken. A 2:1 minimum is a common baseline for trend-following strategies.

Maximum Position Count

Cap the number of simultaneously open positions to limit portfolio drawdown during adverse market conditions.

Time-of-Day Restrictions

Avoid entering trades during low-liquidity hours, news events, or exchange maintenance windows by checking the when timestamp from the execution context.

Volatility Filters

Gate entries on ATR or candle-range metrics to avoid entering during choppy, low-momentum conditions where TP and SL levels are likely to be hit randomly.
addRiskSchema({
  riskName: 'production',
  validations: [
    // Max 3 open positions across all symbols
    async ({ symbol }) => {
      const openCount = await getActivePositionCount();
      if (openCount >= 3) {
        throw new Error(`Portfolio full: ${openCount} open positions`);
      }
    },

    // No new entries between 23:00–01:00 UTC (low liquidity)
    ({ pendingSignal }) => {
      const hour = new Date(pendingSignal.timestamp).getUTCHours();
      if (hour === 23 || hour === 0) {
        throw new Error('No entries during low-liquidity window');
      }
    },

    // Minimum 2:1 R/R
    ({ pendingSignal, currentPrice }) => {
      const { priceOpen = currentPrice, priceTakeProfit, priceStopLoss, position } = pendingSignal;
      const reward = position === 'long' ? priceTakeProfit - priceOpen : priceOpen - priceTakeProfit;
      const risk   = position === 'long' ? priceOpen - priceStopLoss : priceStopLoss - priceOpen;
      if (reward / risk < 2) throw new Error('R/R below minimum threshold');
    },
  ],
});

Risk Rejection Handling

When a validation throws, the signal is rejected and the strategy continues running normally. No exception propagates to user code. The rejection is:
  1. Logged with the validation error message and the signal parameters
  2. Counted in the per-strategy rejection statistics
  3. Emitted to any listenRisk subscribers for real-time monitoring or alerting
The strategy will attempt to generate a new signal on the next tick according to its configured interval. Rejection does not introduce any additional cooldown beyond the normal interval throttle.

Monitoring Rejections with listenRisk

Subscribe to the listenRisk event emitter to receive a callback every time a signal is rejected by risk validation. This is useful for building dashboards, sending alerts, or logging rejection reasons to an external system.
import { listenRisk } from 'backtest-kit';

listenRisk(({ symbol, strategyName, rejectionNote, currentSignal }) => {
  console.warn(`[RISK REJECTED] ${strategyName}/${symbol}: ${rejectionNote}`);
  // Send to Telegram, Slack, monitoring dashboard, etc.
});
The rejectionNote field contains a human-readable explanation of why the trade was rejected. The currentSignal field carries the full pending signal that was blocked, making it straightforward to log signal parameters alongside the rejection reason.
Risk validation uses atomic check-and-reserve semantics in parallel multi-symbol execution. When multiple symbols are backtested concurrently and a validation checks a shared resource like total open position count, the framework serializes the check-and-commit operation. This prevents race conditions where two parallel backtests each read count = 2, both pass a max 3 check, and both open a position — leaving the portfolio with 4 open trades instead of the allowed 3.

Build docs developers (and LLMs) love