Component registration is the mechanism by which users configure backtest-kit's core building blocks before executing backtests, live trading, or strategy optimization. This page explains the declarative registration pattern using add* functions and how registered schemas are stored in schema services for later retrieval.
For detailed API documentation of each registration function and its parameters, see Component Registration Functions. For schema interface definitions, see Component Schemas. For the internal implementation of schema services, see Schema Services.
The registration system follows a consistent three-step pattern:
Diagram: Component Registration Flow
Each add* function performs validation before storage, ensuring schemas are valid at configuration time rather than runtime. Schema services use the ToolRegistry pattern to store configurations in memory with type-safe retrieval.
backtest-kit provides seven component types, each with a dedicated registration function:
| Component | Function | Purpose | Schema Type |
|---|---|---|---|
| Strategy | addStrategy |
Signal generation logic | IStrategySchema |
| Exchange | addExchange |
Market data source | IExchangeSchema |
| Frame | addFrame |
Backtest timeframe | IFrameSchema |
| Risk | addRisk |
Portfolio risk rules | IRiskSchema |
| Sizing | addSizing |
Position sizing method | ISizingSchema |
| Walker | addWalker |
Strategy comparison | IWalkerSchema |
| Optimizer | addOptimizer |
LLM strategy generation | IOptimizerSchema |
All registration functions share a consistent naming pattern: add<ComponentType>, where the component type matches the schema name.
Each registration function follows an identical implementation pattern:
Diagram: Registration Function Execution Flow
All registration functions inject three dependencies via the DI container:
ToolRegistryexport function addStrategy(strategySchema: IStrategySchema) {
backtest.loggerService.info(ADD_STRATEGY_METHOD_NAME, {
strategySchema,
});
backtest.strategyValidationService.addStrategy(
strategySchema.strategyName,
strategySchema
);
backtest.strategySchemaService.register(
strategySchema.strategyName,
strategySchema
);
}
Execution sequence:
loggerService.info() logs: "add.addStrategy" with full schema objectstrategyValidationService.addStrategy() validates:
strategyName is non-empty stringinterval is valid value ("1m" | "3m" | "5m" | "15m" | "30m" | "1h")getSignal is an async functionriskName or riskList is provided (if risk management enabled)strategySchemaService.register() stores schema in ToolRegistry keyed by strategyNameIf validation fails, an error is thrown and the schema is not registered.
Schema services use ToolRegistry from di-kit to store component configurations:
Diagram: ToolRegistry Usage in Schema Services
Each schema service extends a base pattern:
Storage: In-memory Map<string, TSchema> via ToolRegistry
Key: Component name (e.g., strategyName, exchangeName)
Value: Full schema object (e.g., IStrategySchema, IExchangeSchema)
Retrieval: Type-safe get(name) method
Validation: has(name) to check existence before retrieval
package.json:75-78 (di-kit dependency)
src/lib/core/types.ts:20-28 (Schema service symbols)
All schema services are registered as singletons in the DI container:
{
provide(TYPES.exchangeSchemaService, () => new ExchangeSchemaService());
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
provide(TYPES.frameSchemaService, () => new FrameSchemaService());
provide(TYPES.walkerSchemaService, () => new WalkerSchemaService());
provide(TYPES.sizingSchemaService, () => new SizingSchemaService());
provide(TYPES.riskSchemaService, () => new RiskSchemaService());
provide(TYPES.optimizerSchemaService, () => new OptimizerSchemaService());
}
Singleton behavior: Each schema service is instantiated once and shared across the entire application. This ensures that all components (validation services, connection services, command services) access the same registry.
A typical backtest setup registers multiple components with dependencies between them:
Diagram: Component Registration Dependencies
Registration order matters when components reference each other by name. For example:
riskName is specified)strategies array)Note: Validation services check for missing dependencies during registration. If a strategy references a non-existent riskName, addStrategy() throws an error.
// 1. Register exchange (no dependencies)
addExchange({
exchangeName: "test_exchange",
getCandles: async (symbol, interval, since, limit) => {
const exchange = new ccxt.binance();
const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
timestamp, open, high, low, close, volume
}));
},
formatPrice: async (symbol, price) => price.toFixed(2),
formatQuantity: async (symbol, quantity) => quantity.toFixed(8),
});
// 2. Register risk (no dependencies)
addRisk({
riskName: "demo_risk",
validations: [
{
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%",
},
],
});
// 3. Register frame (no dependencies)
addFrame({
frameName: "test_frame",
interval: "1m",
startDate: new Date("2025-12-01T00:00:00.000Z"),
endDate: new Date("2025-12-01T23:59:59.000Z"),
});
// 4. Register strategy (references risk)
addStrategy({
strategyName: "test_strategy",
interval: "5m",
riskName: "demo_risk", // References registered risk
getSignal: async (symbol) => {
// Strategy logic
},
});
// 5. Execute backtest (references all components)
Backtest.background("BTCUSDT", {
strategyName: "test_strategy",
exchangeName: "test_exchange",
frameName: "test_frame",
});
Diagram: Validation Service Processing
Validation layers:
Type Validation: Ensures required fields exist and have correct types
strategyName must be a non-empty stringgetSignal must be an async functionBusiness Logic Validation: Enforces domain-specific rules
interval must be one of: "1m", "3m", "5m", "15m", "30m", "1h"startDate must be before endDate in framesReference Validation: Checks that referenced components exist
riskName: "demo_risk", the risk must be registeredexchangeName: "binance", the exchange must be registeredMemoization: Validation results are cached by component name
memoize() from functools-kitexport function addStrategy(strategySchema: IStrategySchema) {
// Validation call before registration
backtest.strategyValidationService.addStrategy(
strategySchema.strategyName,
strategySchema
);
// Only reaches here if validation passes
backtest.strategySchemaService.register(
strategySchema.strategyName,
strategySchema
);
}
What StrategyValidationService.addStrategy() checks:
strategyName is a string (non-empty)interval is one of the valid intervalsgetSignal is a functionriskName is provided, check riskSchemaService.has(riskName) returns trueriskList is provided, check all risk names exist in riskSchemaServiceIf validation fails: Error is thrown immediately, preventing registration. The schema is not stored in the schema service.
Registered schemas are retrieved by connection services, which create client instances on demand:
Diagram: Schema Retrieval During Execution
Retrieval pattern:
Backtest.run(symbol, { strategyName, exchangeName, frameName })ClientStrategy) with schemasymbol:strategyName:mode key)getSignal() function)Memoization key structure:
${symbol}:${strategyName}:${backtest ? 'backtest' : 'live'}${exchangeName}:${backtest ? 'backtest' : 'live'}${riskName}${sizingName}${frameName}This ensures proper isolation:
Strategies are isolated by symbol and execution mode
Exchanges have separate instances for backtest vs. live (temporal isolation)
Risk/Sizing/Frame use simpler keys (no mode dependency)
When Backtest.run("BTCUSDT", { strategyName: "test_strategy", ... }) executes:
BacktestCommandService calls StrategyConnectionService.getClient("BTCUSDT", "test_strategy", true)"BTCUSDT:test_strategy:backtest"StrategySchemaService.get("test_strategy") → returns IStrategySchemanew ClientStrategy(schema, symbol, backtest=true)ClientStrategy instanceclientStrategy.backtest(timeframes) to execute strategyUnderstanding the separation between registration phase and execution phase is critical:
| Phase | When | Purpose | Services Involved |
|---|---|---|---|
| Registration | Before execution | Declare components | add* functions, Validation Services, Schema Services |
| Execution | During run()/background() |
Use components | Command Services, Connection Services, Client Classes |
Registration is declarative: Users specify what to use, not how or when to use it.
Execution is imperative: Framework orchestrates component usage based on execution mode (backtest/live/walker).
Diagram: Registration and Execution Sequence
Key benefits of separation:
Component registration in backtest-kit follows a consistent, type-safe pattern:
add* function with schema objectToolRegistryThis architecture provides:
For detailed parameter documentation, see Component Registration Functions. For schema interfaces, see Component Schemas. For connection service implementation, see Connection Services.