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:
Trading pair symbol, e.g. "BTCUSDT".
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.
Name of the strategy requesting to open a position.
Exchange name used for this strategy.
Name of the risk profile being applied.
Frame name for backtest context; empty string in live mode.
Current VWAP price at the moment risk is checked.
Current timestamp in milliseconds.
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.