This page introduces the fundamental architectural patterns and execution models of backtest-kit. It covers the three execution modes (Backtest, Live, Walker), the signal lifecycle state machine, and the component-based registration system. For detailed API documentation, see Public API Reference. For implementation details of specific components, see Component Types.
backtest-kit provides three execution modes that share the same strategy and exchange components but operate under different timing and data models:
| Mode | Time Progression | Data Source | Use Case | Completion |
|---|---|---|---|---|
| Backtest | Historical timestamps from IFrameSchema |
Frame.getTimeframe() array |
Strategy validation on past data | Finite (when timeframe exhausted) |
| Live | Real-time via Date.now() |
Live exchange API calls | Production trading | Infinite (until stopped) |
| Walker | Orchestrates multiple Backtests | Shared IFrameSchema across strategies |
A/B testing and optimization | Finite (after all strategies tested) |
The execution modes are accessed through singleton utility classes:
Backtest.run(symbol, { strategyName, exchangeName, frameName }) - src/classes/Backtest.ts:38-66Live.run(symbol, { strategyName, exchangeName }) - src/classes/Live.ts:55-82Walker.run(symbol, { walkerName }) - src/classes/Walker.ts:39-87Each mode implements a similar async generator pattern but with different orchestration logic. Backtest and Live both call strategy.tick() or strategy.backtest(), while Walker iterates through multiple strategies and compares their results by a configurable metric (WalkerMetric).
Signals progress through a state machine implemented as a discriminated union of result types. The action field serves as the discriminator for type-safe state handling.
The framework uses TypeScript discriminated unions for type-safe signal state handling:
type IStrategyTickResult =
| IStrategyTickResultIdle
| IStrategyTickResultScheduled
| IStrategyTickResultOpened
| IStrategyTickResultActive
| IStrategyTickResultClosed
| IStrategyTickResultCancelled;
Each state type has a unique action discriminator:
priceOpen, waiting for price to reach entry point - src/interfaces/Strategy.interface.ts:181-194Signals track two critical timestamps for accurate duration calculation:
priceOpen (updated on scheduled signal activation) - src/interfaces/Strategy.interface.ts:56-56The minuteEstimatedTime countdown uses pendingAt, not scheduledAt, ensuring scheduled signals don't count waiting time toward expiration - src/client/ClientStrategy.ts:681-683.
backtest-kit uses a registration-based architecture where components are defined as schemas and instantiated on-demand via dependency injection.
The framework provides six registrable component types, each with a dedicated schema interface:
| Component | Schema Interface | Purpose | Registration Function |
|---|---|---|---|
| Strategy | IStrategySchema |
Signal generation logic via getSignal() |
addStrategy() |
| Exchange | IExchangeSchema |
Market data via getCandles(), price formatting |
addExchange() |
| Frame | IFrameSchema |
Backtest timeframe generation via getTimeframe() |
addFrame() |
| Risk | IRiskSchema |
Portfolio-level position limits and validations | addRisk() |
| Sizing | ISizingSchema |
Position size calculation (fixed, Kelly, ATR) | addSizing() |
| Walker | IWalkerSchema |
Multi-strategy comparison configuration | addWalker() |
Each schema is stored in a corresponding *SchemaService using the ToolRegistry pattern - src/lib/services/schema/StrategySchemaService.ts, src/lib/services/schema/ExchangeSchemaService.ts, etc.
The framework uses memoized Connection Services to lazily instantiate Client classes:
This two-tier architecture (Schema Services + Connection Services) enables:
*ValidationService.validate() - src/lib/services/validation/StrategyValidationService.tswaitForInit() before first operation - src/client/ClientStrategy.ts:298-330The framework uses di-scoped to propagate execution context without explicit parameter passing. Two context types flow through the system:
ExecutionContext (IExecutionContext):
symbol: Trading pair (e.g., "BTCUSDT")when: Current timestamp (historical for backtest, Date.now() for live)backtest: Boolean flag indicating execution modeMethodContext (IMethodContext):
strategyName: Which strategy schema to useexchangeName: Which exchange schema to useframeName: Which frame schema to use (empty string for live mode)This pattern enables clean strategy code without framework boilerplate:
// Strategy author writes:
const candles = await getCandles(symbol, interval, limit);
// Framework automatically injects:
// - executionContext.when (which timestamp to query)
// - methodContext.exchangeName (which exchange to use)
// - executionContext.backtest (historical vs real-time data)
For detailed context propagation mechanics, see Context Propagation.