Validation Services provide schema validation and runtime checks for all component types in the framework. These services ensure that components are correctly configured at registration time and that runtime data (signals, prices, risk parameters) meets safety constraints before execution. Validation Services act as gatekeepers between the public API and the execution engine, preventing invalid configurations from causing runtime errors or financial losses.
For information about how validated schemas are stored and retrieved, see Schema Services. For details on how validation results flow into execution, see Global Services.
The framework provides six validation service classes, one for each component type. All validation services follow a common pattern: they validate schemas at registration time, memoize validation results, and provide a list() method for retrieving registered schemas.
| Service Class | Component Type | Primary Responsibilities | DI Symbol |
|---|---|---|---|
StrategyValidationService |
Strategy | Signal generation validation, interval checks, risk/sizing references | TYPES.strategyValidationService |
ExchangeValidationService |
Exchange | Market data interface validation, formatting function checks | TYPES.exchangeValidationService |
FrameValidationService |
Frame | Timeframe configuration validation, date range checks | TYPES.frameValidationService |
RiskValidationService |
Risk | Position limit validation, custom validation function checks | TYPES.riskValidationService |
SizingValidationService |
Sizing | Position sizing method validation, parameter range checks | TYPES.sizingValidationService |
WalkerValidationService |
Walker | Strategy comparison validation, metric selection checks | TYPES.walkerValidationService |
All validation services implement a consistent interface pattern:
interface IValidationService<TSchema> {
// Store and validate schema
addComponent(name: string, schema: TSchema): void;
// Retrieve all registered schemas
list(): Promise<TSchema[]>;
// Internal validation logic (memoized)
_validateSchema(schema: TSchema): ValidationResult;
}
The addComponent method (named addStrategy, addExchange, etc.) is called by the corresponding add* functions in the public API. Validation results are memoized to avoid repeated validation of the same schema.
Schema validation occurs when a component is registered via an add* function. The validation flow follows this sequence:
Each validation service performs component-specific checks:
strategyName is non-empty stringinterval matches FrameInterval enum valuesgetSignal is an async functionriskName reference exists (if specified)sizingName reference exists (if specified)callbacks structure is valid (if specified)exchangeName is non-empty stringgetCandles is an async function returning candle arrayformatPrice is an async functionformatQuantity is an async functioncallbacks structure is valid (if specified)frameName is non-empty stringinterval matches FrameInterval enum valuesstartDate is valid Date objectendDate is valid Date objectstartDate < endDatecallbacks structure is valid (if specified)riskName is non-empty stringmaxConcurrentPositions is positive integer (if specified)validations array contains valid functions (if specified)callbacks structure is valid (if specified)sizingName is non-empty stringmethod is one of: "fixed-percentage", "kelly-criterion", "atr-based"riskPercentage, kellyMultiplier)callbacks structure is valid (if specified)walkerName is non-empty stringexchangeName reference existsframeName reference existsstrategies is non-empty array of strategy namesmetric is valid metric name (if specified)callbacks structure is valid (if specified)Validation services use memoization to cache validation results. Once a schema is validated, subsequent calls with the same schema name return the cached result without re-running validation logic. This optimization is critical for performance during execution when validation services may be called frequently.
Signal validation occurs at runtime when a strategy generates a signal via getSignal(). The validation function VALIDATE_SIGNAL_FN performs comprehensive checks to ensure the signal is financially sound and meets safety constraints.
The framework validates all price fields to prevent impossible or dangerous trades:
| Check | Condition | Purpose |
|---|---|---|
| Positive Prices | priceOpen > 0, priceTakeProfit > 0, priceStopLoss > 0 |
Prevent negative or zero prices |
| Finite Numbers | isFinite(priceOpen), isFinite(priceTakeProfit), isFinite(priceStopLoss) |
Prevent NaN or Infinity values |
| Non-NaN | !isNaN(priceOpen), !isNaN(priceTakeProfit), !isNaN(priceStopLoss) |
Prevent calculation explosions |
Example: Negative Price Rejection
// This signal will be rejected at validation
{
position: "long",
priceOpen: -42000, // Invalid: negative price
priceTakeProfit: 43000,
priceStopLoss: 41000,
minuteEstimatedTime: 60
}
The framework enforces position-specific logic for Take Profit and Stop Loss prices:
LONG Position Logic:
priceTakeProfit > priceOpenpriceStopLoss < priceOpenSHORT Position Logic:
priceTakeProfit < priceOpenpriceStopLoss > priceOpenExample: Invalid LONG Signal
// This signal will be rejected at validation
{
position: "long",
priceOpen: 41000,
priceTakeProfit: 40000, // Invalid: TP below priceOpen for LONG
priceStopLoss: 39000,
minuteEstimatedTime: 60
}
Example: Invalid SHORT Signal
// This signal will be rejected at validation
{
position: "short",
priceOpen: 43000,
priceTakeProfit: 44000, // Invalid: TP above priceOpen for SHORT
priceStopLoss: 45000,
minuteEstimatedTime: 60
}
The framework validates minimum and maximum distances for Take Profit and Stop Loss to ensure trades are profitable after fees and risk is bounded:
| Parameter | Default Value | Purpose |
|---|---|---|
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT |
0.1% | Ensure TP distance covers trading fees (2×0.1%) plus minimum profit |
CC_MAX_STOPLOSS_DISTANCE_PERCENT |
20% | Prevent catastrophic losses (one signal cannot lose >20% of position) |
Example: Micro-Profit Rejection
// This signal will be rejected at validation
{
position: "long",
priceOpen: 42000,
priceTakeProfit: 42010, // Only 0.024% profit - fees will eat profit
priceStopLoss: 41000,
minuteEstimatedTime: 60
}
// Net PNL after fees: 0.024% - 0.2% = -0.176% (loss!)
Example: Extreme Stop Loss Rejection
// This signal will be rejected at validation
{
position: "long",
priceOpen: 42000,
priceTakeProfit: 43000,
priceStopLoss: 20000, // -52% loss - catastrophic risk!
minuteEstimatedTime: 60
}
The framework validates signal lifetime to prevent "eternal signals" that block risk limits indefinitely:
| Parameter | Default Value | Purpose |
|---|---|---|
CC_MAX_SIGNAL_LIFETIME_MINUTES |
1440 (1 day) | Prevent signals from blocking positions for weeks/months |
Example: Excessive Lifetime Rejection
// This signal will be rejected at validation
{
position: "long",
priceOpen: 42000,
priceTakeProfit: 43000,
priceStopLoss: 41000,
minuteEstimatedTime: 50000 // >34 days - strategy deadlock risk!
}
Validation behavior is controlled by global configuration parameters that can be modified at runtime using setConfig():
| Parameter | Type | Default | Description |
|---|---|---|---|
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT |
number |
0.1 | Minimum TP distance from priceOpen (percentage). Must be greater than trading fees to ensure profitable trades. |
CC_MAX_STOPLOSS_DISTANCE_PERCENT |
number |
20 | Maximum SL distance from priceOpen (percentage). Prevents catastrophic losses from extreme StopLoss values. |
CC_MAX_SIGNAL_LIFETIME_MINUTES |
number |
1440 | Maximum signal lifetime in minutes. Prevents eternal signals that block risk limits for weeks/months. |
CC_SCHEDULE_AWAIT_MINUTES |
number |
120 | Time to wait for scheduled signal to activate (in minutes). Signals that don't activate are cancelled. |
CC_AVG_PRICE_CANDLES_COUNT |
number |
5 | Number of candles to use for average price calculation (VWAP). Used in real-time monitoring. |
Example: Customizing Validation Parameters
import { setConfig } from "backtest-kit";
setConfig({
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT: 0.3, // Require 0.3% minimum TP
CC_MAX_STOPLOSS_DISTANCE_PERCENT: 10, // Limit SL to 10% max
CC_MAX_SIGNAL_LIFETIME_MINUTES: 720, // Limit signals to 12 hours
});
All add* functions follow the same pattern:
addComponent methodregister method// Pattern from src/function/add.ts:50-62
export function addStrategy(strategySchema: IStrategySchema) {
backtest.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
strategySchema,
});
backtest.strategyValidationService.addStrategy(
strategySchema.strategyName,
strategySchema
);
backtest.strategySchemaService.register(
strategySchema.strategyName,
strategySchema
);
}
This two-phase approach ensures that invalid schemas are rejected before being stored, preventing runtime errors later in the execution pipeline.
Validation services memoize validation results to avoid redundant checks. Once a schema is validated, subsequent references to the same component name use the cached validation result. This is critical for performance during execution when components may be referenced hundreds or thousands of times.
All validation services provide a list() method that returns an array of all registered schemas. This is used for debugging, documentation generation, and building dynamic UIs:
// Pattern from src/function/list.ts:41-44
export async function listExchanges(): Promise<IExchangeSchema[]> {
backtest.loggerService.log(LIST_EXCHANGES_METHOD_NAME);
return await backtest.exchangeValidationService.list();
}
Some validation services check that referenced components exist. For example, WalkerValidationService validates that all strategy names in the strategies array exist, and that the exchangeName and frameName references are valid.
This cross-component validation ensures that the execution engine never attempts to use non-existent components, preventing runtime errors.
When validation fails, the validation service throws an error immediately. This error propagates back to the caller (typically the add* function), which then propagates to the user. Validation errors are synchronous and deterministic - they occur at registration time, not at execution time.
Example Error Scenarios:
riskName not registered)maxConcurrentPositions)It's important to distinguish between two types of validation failures:
| Type | Timing | Behavior | Example |
|---|---|---|---|
| Schema Validation Error | Registration time | Throws exception, prevents component registration | Missing strategyName field |
| Signal Rejection | Runtime (during execution) | Signal marked as idle, no exception thrown |
TP/SL distances too close |
Signal rejections are part of normal execution flow - the framework validates every signal generated by getSignal() and silently rejects invalid ones. Schema validation errors are exceptional conditions that indicate programmer error.
After a schema passes validation:
*SchemaService (see Schema Services)*ConnectionService retrieves it (see Connection Services)*ConnectionService creates a memoized Client* instanceClient* instance uses the validated schema for all operationsThis ensures that only validated schemas reach the execution engine.
All validation services are registered in the DI container as singletons:
// From src/lib/core/provide.ts:102-109
{
provide(TYPES.exchangeValidationService, () => new ExchangeValidationService());
provide(TYPES.strategyValidationService, () => new StrategyValidationService());
provide(TYPES.frameValidationService, () => new FrameValidationService());
provide(TYPES.walkerValidationService, () => new WalkerValidationService());
provide(TYPES.sizingValidationService, () => new SizingValidationService());
provide(TYPES.riskValidationService, () => new RiskValidationService());
}
The singleton pattern ensures that validation results and registered schemas are shared across the entire application lifecycle.