ClientRisk is the core implementation of the IRisk interface responsible for signal validation and position tracking within the backtest-kit framework. This class executes user-defined validation rules, enforces risk management policies, and maintains active position counts across trading strategies.
This document covers the internal implementation of ClientRisk and how it integrates with the signal lifecycle. For information about:
addRisk(), see Risk SchemasClientRisk implements the IRisk interface, which defines three core methods for risk management:
| Method | Purpose | When Called |
|---|---|---|
checkSignal |
Validates pending signal against risk rules | Before opening position (immediate or scheduled) |
addSignal |
Tracks new active position | After signal validation passes |
removeSignal |
Removes position from tracking | When signal closes (TP/SL/timeout) |
ClientRisk wraps user-defined risk schemas registered via addRisk(). Each schema contains an array of validation rules that are executed sequentially:
The checkSignal method is the primary entry point for risk validation. It receives a validation payload and executes all registered validation rules:
interface ICheckSignalPayload {
pendingSignal: ISignalDto; // Signal to validate
symbol: string; // Trading pair (e.g., "BTCUSDT")
strategyName: StrategyName; // Strategy identifier
exchangeName: ExchangeName; // Exchange identifier
currentPrice: number; // Current VWAP price
timestamp: number; // Unix timestamp (ms)
}
Risk schemas define validation functions that throw errors on failure. Common validation patterns include:
Ensures take profit and stop loss are sufficiently distant from entry price to avoid micro-profits and instant stop-outs:
// Example: Minimum TP distance validation
{
validate: ({ 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 distance ${tpDistance.toFixed(2)}% < 1%`);
}
},
note: "TP distance must be at least 1%"
}
Ensures favorable risk-reward ratio:
// Example: Minimum 1:2 RR ratio validation
{
validate: ({ 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;
const rrRatio = reward / risk;
if (rrRatio < 2) {
throw new Error(`RR ratio ${rrRatio.toFixed(2)} < 2:1`);
}
},
note: "Risk-Reward ratio must be at least 1:2"
}
ClientRisk maintains active position counts per symbol-strategy pair to enforce concurrent position limits:
| Method | Signature | Purpose |
|---|---|---|
addSignal |
(symbol: string, ctx: {strategyName, riskName}) => Promise<void> |
Increments position counter for symbol-strategy pair |
removeSignal |
(symbol: string, ctx: {strategyName, riskName}) => Promise<void> |
Decrements position counter when signal closes |
ClientRisk implements a fail-fast validation pattern where the first validation failure immediately stops execution:
This pattern ensures:
note field in validation ruleonReject for custom handlingWhen a strategy has no riskName configured, ClientRisk is replaced with a no-op implementation that always passes validation:
const NOOP_RISK: IRisk = {
checkSignal: () => Promise.resolve(true),
addSignal: () => Promise.resolve(),
removeSignal: () => Promise.resolve(),
}
This allows strategies to operate without risk management while maintaining the same interface contract.
ClientRisk instances are managed by RiskConnectionService, which provides routing and memoization:
The connection service ensures:
riskName stringWhen validation fails, ClientRisk emits rejection events through the riskSubject event emitter:
interface IRiskEvent {
symbol: string;
strategyName: StrategyName;
exchangeName: ExchangeName;
pendingSignal: ISignalDto;
currentPrice: number;
timestamp: number;
rejectionReason: string; // Error message from validation
validationNote: string; // "note" field from validation rule
}
ClientRisk serves as the gatekeeper for signal execution, enforcing user-defined risk policies before positions are opened. Key characteristics:
| Aspect | Implementation |
|---|---|
| Interface | Implements IRisk with three methods |
| Validation Model | Sequential execution with fail-fast pattern |
| Position Tracking | Maintains active position counts per symbol-strategy |
| Integration | Called by ClientStrategy during signal lifecycle |
| Event System | Emits rejection events through riskSubject |
| Flexibility | Supports custom validation logic via schemas |
| Fallback | NOOP implementation when no risk profile assigned |
The separation of ClientRisk from dependency injection allows risk logic to be pure, testable, and reusable across different execution contexts (backtest, live, walker).