Layer Responsibilities

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

Mermaid 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 simulation
  • Live.run() / Live.background() - Real-time trading
  • Walker.run() / Walker.background() - Multi-strategy comparison

These 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

Mermaid Diagram

Each public service:

  1. Accepts context parameters (strategyName, exchangeName, frameName)
  2. Wraps private service call with MethodContextService.runAsyncIterator()
  3. Returns async generator from private service

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 operations
  • ExchangeGlobalService - Market data (backtest only)
  • FrameGlobalService - Timeframe generation (backtest only)
  • Markdown services - Report generation

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

Mermaid Diagram

Each Connection Service:

  1. Retrieves schema from corresponding Schema Service
  2. Checks memoization cache by component name
  3. Creates client instance if not cached, injecting dependencies
  4. Returns memoized instance for subsequent calls

Connection services inject their dependencies into client constructors:

  • StrategyConnectionService injects RiskConnectionService and ExchangeConnectionService
  • ExchangeConnectionService injects ExecutionContextService
  • RiskConnectionService injects RiskSchemaService
  • FrameConnectionService injects FrameSchemaService
  • SizingConnectionService injects SizingSchemaService

Client Classes contain pure business logic without dependency injection dependencies. They implement the core algorithms and state management.

Client Class Hierarchy

Mermaid Diagram

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:

  1. Constructor parameters are simple values or functions from schemas
  2. Service dependencies are passed via Connection Service injection
  3. Prototype methods are used for memory efficiency (not arrow functions)

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

Mermaid Diagram

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

Mermaid Diagram

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:

  1. Calls StrategyValidationService.validate(strategyName)
  2. Calls ExchangeValidationService.validate(exchangeName)
  3. Calls FrameValidationService.validate(frameName)
  4. If strategy has 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

Mermaid Diagram

Each Markdown Service:

  1. Subscribes to emitter during initialization
  2. Accumulates events in memoized storage by strategy/walker name
  3. Limits event queue (e.g., LiveMarkdownService max 250 events)
  4. Calculates statistics on demand via getData()
  5. Formats markdown via getReport()
  6. Writes to disk via dump()

Storage is memoized per component:

  • BacktestMarkdownService - One storage per (strategyName, exchangeName, frameName)
  • LiveMarkdownService - One storage per (strategyName, exchangeName)
  • WalkerMarkdownService - One storage per walkerName

Three services span all layers and provide infrastructure concerns.

The LoggerService provides logging infrastructure with automatic context injection:

Mermaid Diagram

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

Mermaid Diagram

  1. Public API function receives schema
  2. Validation Service stores for runtime checks
  3. Schema Service validates structure and stores in registry

Backtest Execution Pattern

Mermaid Diagram

  1. Public service wraps with MethodContext
  2. Private service gets client from Connection Service
  3. Connection Service returns memoized instance
  4. Private service calls client methods
  5. Results yielded to user

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