This document describes the six-layer service architecture of backtest-kit and the responsibilities of each layer. The framework follows a clean architecture pattern with strict separation of concerns, dependency injection, and unidirectional data flow from public API down to infrastructure.
For information about how dependency injection works across these layers, see Dependency Injection System. For details on how context flows through the layers, see Context Propagation.
The backtest-kit framework organizes services into six distinct layers, each with specific responsibilities. Services are bound via dependency injection using Symbol-based tokens defined in src/lib/core/types.ts:1-81.
Layer Organization Diagram
The Public API layer provides the user-facing functions for component registration and execution. These functions have no business logic and immediately delegate to Global Services.
Component registration functions are exposed in src/function/add.ts:1-342:
| Function | Purpose | Delegates To |
|---|---|---|
addStrategy() |
Register strategy schema | StrategyValidationService.addStrategy()StrategySchemaService.register() |
addExchange() |
Register exchange schema | ExchangeValidationService.addExchange()ExchangeSchemaService.register() |
addFrame() |
Register timeframe schema | FrameValidationService.addFrame()FrameSchemaService.register() |
addRisk() |
Register risk profile | RiskValidationService.addRisk()RiskSchemaService.register() |
addSizing() |
Register sizing method | SizingValidationService.addSizing()SizingSchemaService.register() |
addWalker() |
Register strategy comparison | WalkerValidationService.addWalker()WalkerSchemaService.register() |
Component listing functions are exposed in src/function/list.ts:1-218:
| Function | Purpose | Delegates To |
|---|---|---|
listStrategies() |
Get all strategy schemas | StrategyValidationService.list() |
listExchanges() |
Get all exchange schemas | ExchangeValidationService.list() |
listFrames() |
Get all frame schemas | FrameValidationService.list() |
listRisks() |
Get all risk schemas | RiskValidationService.list() |
listSizings() |
Get all sizing schemas | SizingValidationService.list() |
listWalkers() |
Get all walker schemas | WalkerValidationService.list() |
Execution entry points provide static methods for running backtests and live trading:
Backtest.run() / Backtest.background() - Historical simulationLive.run() / Live.background() - Real-time tradingWalker.run() / Walker.background() - Multi-strategy comparisonThese delegate to corresponding Global Services which wrap Logic Services with validation.
Logic Services orchestrate the core execution flow. They are split into Public and Private services to separate context management from core logic.
Public logic services wrap execution in context and provide the public interface:
Logic Service Responsibilities
Each public service:
MethodContextService.runAsyncIterator()Example from BacktestLogicPublicService:
The public service receives context and wraps the private service:
run(symbol, { strategyName, exchangeName, frameName }) {
return methodContextService.runAsyncIterator(
() => backtestLogicPrivateService.run(symbol),
{ strategyName, exchangeName, frameName }
)
}
Private logic services contain the core execution orchestration:
| Service | Responsibility | Key Methods |
|---|---|---|
BacktestLogicPrivateService |
Iterate timeframes, call strategy.tick(), yield signals | run(), emits to signalBacktestEmitter |
LiveLogicPrivateService |
Infinite loop, Date.now() progression, persistence | run(), emits to signalLiveEmitter |
WalkerLogicPrivateService |
Run multiple strategies, compare metrics | run(), emits to walkerEmitter |
These services have dependencies on:
StrategyGlobalService - Strategy operationsExchangeGlobalService - Market data (backtest only)FrameGlobalService - Timeframe generation (backtest only)Connection Services manage memoized client instances. Each service creates and caches client instances by component name, ensuring only one instance exists per registered component.
Connection services are defined in src/lib/core/types.ts:10-16:
const connectionServices = {
exchangeConnectionService: Symbol('exchangeConnectionService'),
strategyConnectionService: Symbol('strategyConnectionService'),
frameConnectionService: Symbol('frameConnectionService'),
sizingConnectionService: Symbol('sizingConnectionService'),
riskConnectionService: Symbol('riskConnectionService'),
}
Connection Service Architecture
Each Connection Service:
Connection services inject their dependencies into client constructors:
StrategyConnectionService injects RiskConnectionService and ExchangeConnectionServiceExchangeConnectionService injects ExecutionContextServiceRiskConnectionService injects RiskSchemaServiceFrameConnectionService injects FrameSchemaServiceSizingConnectionService injects SizingSchemaServiceClient Classes contain pure business logic without dependency injection dependencies. They implement the core algorithms and state management.
Client Class Hierarchy
| Client Class | Primary Responsibility | Key State |
|---|---|---|
ClientStrategy |
Signal lifecycle management, tick/backtest execution | _pendingSignal, _lastSignalTimestamp |
ClientExchange |
Market data fetching, VWAP calculation, formatting | Stateless (delegates to schema functions) |
ClientRisk |
Position tracking, custom validation execution | _activePositions Map |
ClientFrame |
Timeframe generation for backtesting | Stateless |
ClientSizing |
Position size calculation | Stateless |
Client classes have NO direct dependency injection. Instead:
Example: ClientStrategy receives schema functions and other clients as constructor parameters, not DI symbols.
Schema and Validation Services manage component registration and runtime validation. They use a parallel service structure with distinct responsibilities.
Schema Services store component configurations using the ToolRegistry pattern:
Schema Service Pattern
Each Schema Service provides:
| Method | Purpose |
|---|---|
register(name, schema) |
Store component configuration |
get(name) |
Retrieve configuration by name |
validateShallow(schema) |
Check schema structure and types |
override(name, partial) |
Update existing schema |
Schema services are defined in src/lib/core/types.ts:18-25:
const schemaServices = {
exchangeSchemaService: Symbol('exchangeSchemaService'),
strategySchemaService: Symbol('strategySchemaService'),
frameSchemaService: Symbol('frameSchemaService'),
walkerSchemaService: Symbol('walkerSchemaService'),
sizingSchemaService: Symbol('sizingSchemaService'),
riskSchemaService: Symbol('riskSchemaService'),
}
Validation Services perform runtime existence checks with memoization:
Validation Service Pattern
Each Validation Service provides:
| Method | Purpose |
|---|---|
addStrategy(name, schema) |
Register component for validation |
validate(name) |
Check existence and dependencies |
list() |
Return all registered schemas |
Validation services are defined in src/lib/core/types.ts:59-66:
const validationServices = {
exchangeValidationService: Symbol('exchangeValidationService'),
strategyValidationService: Symbol('strategyValidationService'),
frameValidationService: Symbol('frameValidationService'),
walkerValidationService: Symbol('walkerValidationService'),
sizingValidationService: Symbol('sizingValidationService'),
riskValidationService: Symbol('riskValidationService'),
}
Example Validation Flow:
When BacktestLogicPublicService starts execution:
StrategyValidationService.validate(strategyName)ExchangeValidationService.validate(exchangeName)FrameValidationService.validate(frameName)riskName, validates via RiskValidationService.validate(riskName)Markdown Services subscribe to event emitters and accumulate data for report generation. Each execution mode has a dedicated markdown service.
Markdown services are defined in src/lib/core/types.ts:50-57:
const markdownServices = {
backtestMarkdownService: Symbol('backtestMarkdownService'),
liveMarkdownService: Symbol('liveMarkdownService'),
scheduleMarkdownService: Symbol('scheduleMarkdownService'),
performanceMarkdownService: Symbol('performanceMarkdownService'),
walkerMarkdownService: Symbol('walkerMarkdownService'),
heatMarkdownService: Symbol('heatMarkdownService'),
}
Markdown Service Data Flow
Each Markdown Service:
LiveMarkdownService max 250 events)getData()getReport()dump()Storage is memoized per component:
BacktestMarkdownService - One storage per (strategyName, exchangeName, frameName)LiveMarkdownService - One storage per (strategyName, exchangeName)WalkerMarkdownService - One storage per walkerNameThree services span all layers and provide infrastructure concerns.
The LoggerService provides logging infrastructure with automatic context injection:
Injected into nearly every service via src/lib/core/types.ts:1-3:
const baseServices = {
loggerService: Symbol('loggerService'),
}
The ExecutionContextService manages execution-time parameters using di-scoped:
| Property | Purpose |
|---|---|
symbol |
Trading pair (e.g., "BTCUSDT") |
when |
Current timestamp (Date for live, historical for backtest) |
backtest |
Boolean flag indicating execution mode |
Used by client classes to access current execution context without explicit parameter passing.
The MethodContextService manages component selection using di-scoped:
| Property | Purpose |
|---|---|
strategyName |
Active strategy identifier |
exchangeName |
Active exchange identifier |
frameName |
Active frame identifier (backtest only) |
Used by Connection Services to resolve which component instance to return.
Component Registration Pattern
Backtest Execution Pattern
All data flows unidirectionally down the layers:
Layer 1 (Public API)
↓ Delegates
Layer 2 (Logic Services)
↓ Requests Instances
Layer 3 (Connection Services)
↓ Creates/Returns
Layer 4 (Client Classes)
↓ Uses
Layer 5 (Schemas)
Events flow horizontally to Layer 6 (Markdown Services) for reporting.
All services are bound in src/lib/core/provide.ts:1-111 using the provide() function from the DI container. Services are organized in blocks matching their layer:
// Base Services (Cross-cutting)
provide(TYPES.loggerService, () => new LoggerService());
// Context Services (Cross-cutting)
provide(TYPES.executionContextService, () => new ExecutionContextService());
provide(TYPES.methodContextService, () => new MethodContextService());
// Connection Services (Layer 3)
provide(TYPES.strategyConnectionService, () => new StrategyConnectionService());
provide(TYPES.exchangeConnectionService, () => new ExchangeConnectionService());
// Schema Services (Layer 5)
provide(TYPES.strategySchemaService, () => new StrategySchemaService());
// Global Services (Layer 2 wrappers)
provide(TYPES.strategyGlobalService, () => new StrategyGlobalService());
// Logic Services (Layer 2)
provide(TYPES.backtestLogicPrivateService, () => new BacktestLogicPrivateService());
provide(TYPES.backtestLogicPublicService, () => new BacktestLogicPublicService());
// Markdown Services (Layer 6)
provide(TYPES.backtestMarkdownService, () => new BacktestMarkdownService());
// Validation Services (Layer 5)
provide(TYPES.strategyValidationService, () => new StrategyValidationService());
All services are then exported as a single object in src/lib/index.ts:152-162:
export const backtest = {
...baseServices,
...contextServices,
...connectionServices,
...schemaServices,
...globalServices,
...logicPrivateServices,
...logicPublicServices,
...markdownServices,
...validationServices,
};