Architecture

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.

Mermaid Diagram

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.

Mermaid Diagram

Key Global Services:

  • StrategyGlobalService - Strategy execution with validation
  • ExchangeGlobalService - Exchange data access with context injection
  • LiveGlobalService - Live trading orchestration
  • BacktestGlobalService - Backtest orchestration
  • WalkerGlobalService - Multi-strategy comparison
  • RiskGlobalService - Risk management coordination

Logic 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:

Mermaid Diagram

Public vs Private Split:

  • Public Services (BacktestLogicPublicService, LiveLogicPublicService) - Wrap generators with MethodContextService.runAsyncIterator() to propagate strategyName, exchangeName, frameName
  • Private Services (BacktestLogicPrivateService, LiveLogicPrivateService) - Implement async generator logic with ExecutionContextService.runInContext() calls

Connection Services provide memoized client instance management. They resolve schema configurations, inject dependencies, and return cached client instances to avoid repeated instantiation.

Mermaid Diagram

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.

Mermaid Diagram

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:

Mermaid Diagram

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;
}

Mermaid Diagram

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:

  • No parameter drilling through 6+ layers
  • Type-safe context access via DI
  • Automatic propagation through async boundaries
  • Isolated execution contexts prevent cross-contamination

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:

Mermaid Diagram

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?

  • Prevents race conditions in async event handlers
  • Guarantees FIFO ordering even with slow handlers
  • Avoids event handler interleaving

Mermaid Diagram

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