This document provides a comprehensive overview of backtest-kit's layered architecture, including the service layer organization, dependency injection system, context propagation patterns, and event-driven infrastructure. For details on individual component types (strategies, exchanges, frames), see Component Types. For execution flow specifics, see Backtesting, Live Trading, and Walker Mode.
The framework follows clean architecture principles with six distinct layers, separated by dependency injection boundaries. Each layer has a specific responsibility, with dependencies flowing inward toward business logic.
The public API layer consists of exported functions and classes that users interact with directly. These provide a clean, stable interface while delegating implementation to lower layers.
| Function Category | Exports | Purpose |
|---|---|---|
| Registration | addStrategy, addExchange, addFrame, addRisk, addSizing, addWalker |
Register component schemas |
| Configuration | setLogger, setConfig |
Configure global settings |
| Introspection | listStrategies, listExchanges, etc. |
Query registered components |
| Execution | Backtest, Live, Walker, Schedule, Performance |
Run backtests and live trading |
| Event Listeners | listenSignal, listenError, listenDone, etc. |
Subscribe to system events |
| Utilities | getCandles, getAveragePrice, formatPrice, formatQuantity |
Exchange helpers |
Global Services act as context-aware entry points that wrap lower layers with MethodContextService and ExecutionContextService scope management. They coordinate validation and delegate to Logic Services.
Key Global Services:
StrategyGlobalService - Strategy execution with validationExchangeGlobalService - Exchange data access with context injectionLiveGlobalService - Live trading orchestrationBacktestGlobalService - Backtest orchestrationWalkerGlobalService - Multi-strategy comparisonRiskGlobalService - Risk management coordinationLogic Services implement core orchestration logic using async generators for backtest/live execution. They are split into Public (context management) and Private (core logic) services to separate concerns.
Logic Service Pattern:
Public vs Private Split:
BacktestLogicPublicService, LiveLogicPublicService) - Wrap generators with MethodContextService.runAsyncIterator() to propagate strategyName, exchangeName, frameNameBacktestLogicPrivateService, LiveLogicPrivateService) - Implement async generator logic with ExecutionContextService.runInContext() callsConnection Services provide memoized client instance management. They resolve schema configurations, inject dependencies, and return cached client instances to avoid repeated instantiation.
Key Connection Services:
| Service | Creates | Memoization Key |
|---|---|---|
StrategyConnectionService |
ClientStrategy |
strategyName |
ExchangeConnectionService |
ClientExchange |
exchangeName |
FrameConnectionService |
ClientFrame |
frameName |
RiskConnectionService |
ClientRisk |
riskName |
SizingConnectionService |
ClientSizing |
sizingName |
Memoization Pattern: Connection Services use functools-kit's memoize to cache instances: this.getClient = memoize((name) => new ClientStrategy(params))
Schema Services use the ToolRegistry pattern to store and retrieve component configurations. Validation Services perform runtime checks using memoization to cache validation results.
Schema Service Pattern:
// StrategySchemaService
private readonly _registry = new ToolRegistry<StrategyName, IStrategySchema>();
register(name: StrategyName, schema: IStrategySchema): void {
this._registry.add(name, schema);
}
get(name: StrategyName): IStrategySchema {
return this._registry.get(name);
}
Validation Service Pattern:
// StrategyValidationService
private readonly _validate = singleshot(async (name: StrategyName) => {
if (!this.schemaService.has(name)) {
throw new Error(`Strategy ${name} not registered`);
}
// Additional validation logic
});
async validate(name: StrategyName): Promise<void> {
await this._validate(name);
}
Client Classes contain pure business logic without dependency injection. They receive dependencies through constructor parameters and implement prototype methods for memory efficiency.
Key Client Classes:
| Client | Purpose | Key Methods |
|---|---|---|
ClientStrategy |
Signal lifecycle management | tick(), backtest(), stop() |
ClientExchange |
Market data & VWAP calculation | getCandles(), getAveragePrice() |
ClientFrame |
Timeframe generation | getTimeframe() |
ClientRisk |
Portfolio risk tracking | checkSignal(), addSignal(), removeSignal() |
ClientSizing |
Position size calculation | calculate() |
Memory Efficiency Pattern: All methods are defined on the prototype, not as arrow functions:
class ClientStrategy {
// Prototype method (shared across instances)
async tick(symbol: string): Promise<IStrategyTickResult> {
// Implementation
}
}
The framework uses a custom DI container with Symbol-based tokens for type-safe service resolution.
All service dependencies use unique Symbol tokens to avoid naming collisions and enable type-safe resolution:
// src/lib/core/types.ts
const TYPES = {
loggerService: Symbol('loggerService'),
strategyGlobalService: Symbol('strategyGlobalService'),
exchangeConnectionService: Symbol('exchangeConnectionService'),
// ... 30+ more tokens
};
Services are bound to tokens using factory functions in provide.ts:
// src/lib/core/provide.ts
provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
provide(TYPES.loggerService, () => new LoggerService());
Services resolve dependencies by injecting tokens:
// src/lib/index.ts
const backtest = {
loggerService: inject<LoggerService>(TYPES.loggerService),
strategyGlobalService: inject<StrategyGlobalService>(TYPES.strategyGlobalService),
exchangeConnectionService: inject<ExchangeConnectionService>(TYPES.exchangeConnectionService),
// ... all services
};
The following diagram maps the actual dependency relationships between services:
Context propagation uses di-scoped to implicitly pass execution parameters through nested async operations without explicit parameter drilling.
Two context types flow through the system:
IMethodContext - Component selection:
interface IMethodContext {
exchangeName: ExchangeName;
strategyName: StrategyName;
frameName: FrameName;
}
IExecutionContext - Runtime parameters:
interface IExecutionContext {
symbol: string;
when: Date;
backtest: boolean;
}
MethodContextService wraps async generators to propagate strategyName, exchangeName, frameName:
// BacktestLogicPublicService
async *run(symbol: string, context: IBacktestContext) {
yield* MethodContextService.runAsyncIterator(
this.logicPrivateService.run(symbol),
{
strategyName: context.strategyName,
exchangeName: context.exchangeName,
frameName: context.frameName
}
);
}
ExecutionContextService wraps individual operations with runtime parameters:
// BacktestLogicPrivateService
for (const when of timeframe) {
const result = await ExecutionContextService.runInContext(
async () => await this.strategyGlobalService.tick(symbol),
{ symbol, when, backtest: true }
);
yield result;
}
Services access context via dependency injection:
class ExchangeConnectionService {
private readonly methodContextService: TMethodContextService;
async getClient(symbol: string): Promise<ClientExchange> {
// Implicitly access context
const exchangeName = this.methodContextService.context.exchangeName;
return this.getCachedClient(exchangeName);
}
}
Benefits:
The framework uses functools-kit's Subject for pub-sub event handling with queued processing to ensure sequential execution.
All events are emitted through global Subject instances:
| Emitter | Type | Usage |
|---|---|---|
signalEmitter |
IStrategyTickResult |
All tick results (idle/opened/active/closed) |
signalBacktestEmitter |
IStrategyTickResult |
Backtest signals only |
signalLiveEmitter |
IStrategyTickResult |
Live signals only |
errorEmitter |
Error |
Background execution errors |
doneEmitter |
DoneContract |
Execution completion events |
progressEmitter |
ProgressContract |
Walker/backtest progress |
performanceEmitter |
PerformanceContract |
Timing metrics |
walkerEmitter |
IWalkerStrategyResult |
Walker strategy results |
validationEmitter |
Error |
Risk validation errors |
All event listeners use functools-kit's queued() wrapper to ensure sequential processing:
export const listenSignal = (fn: (data: IStrategyTickResult) => void | Promise<void>) => {
return signalEmitter.subscribe(queued(fn));
};
Why Queued Processing?
Events are emitted from Logic Services after yielding results:
// BacktestLogicPrivateService
async *run(symbol: string) {
for (const when of timeframe) {
const result = await this.strategyGlobalService.tick(symbol);
// Emit to event system
signalEmitter.next(result);
signalBacktestEmitter.next(result);
yield result;
}
// Emit completion
doneEmitter.next({
backtest: true,
symbol,
strategyName: context.strategyName,
exchangeName: context.exchangeName
});
}
The following table maps service categories to their file locations:
| Category | Pattern | Location | Count |
|---|---|---|---|
| Base Services | LoggerService |
src/lib/services/base/ |
1 |
| Context Services | ExecutionContextService, MethodContextService |
src/lib/services/context/ |
2 |
| Connection Services | *ConnectionService |
src/lib/services/connection/ |
5 |
| Schema Services | *SchemaService |
src/lib/services/schema/ |
6 |
| Validation Services | *ValidationService |
src/lib/services/validation/ |
6 |
| Global Services | *GlobalService |
src/lib/services/global/ |
8 |
| Logic Services | *LogicPublicService, *LogicPrivateService |
src/lib/services/logic/ |
6 |
| Markdown Services | *MarkdownService |
src/lib/services/markdown/ |
6 |
Total Services: 40+ injectable services