Context propagation in backtest-kit enables implicit passing of runtime parameters through the service call stack without explicit function arguments. The system uses two context types: MethodContext for schema routing (which strategy/exchange/frame to use) and ExecutionContext for runtime state (backtest mode, current date). This eliminates manual parameter threading across dozens of function calls while maintaining type safety.
For dependency injection mechanics, see Dependency Injection System. For how services use context to route to specific implementations, see Connection Services.
The framework maintains two distinct context scopes that serve different purposes in the execution pipeline:
| Context Type | Scope | Purpose | Key Properties |
|---|---|---|---|
| MethodContext | Per-operation | Routes to correct schema instances | strategyName, exchangeName, frameName |
| ExecutionContext | Per-tick | Provides runtime execution state | date, backtestMode, symbol |
MethodContextService uses the di-scoped library to create ambient context that flows through async operations without explicit parameters. The service contains an IMethodContext object with three schema names that determine which registered implementations to use.
Diagram: MethodContextService propagates schema names through async generator execution
The IMethodContext interface defines the schema routing parameters:
interface IMethodContext {
exchangeName: ExchangeName; // Which exchange schema to use
strategyName: StrategyName; // Which strategy schema to use
frameName: FrameName; // Which frame schema to use (empty for live)
}
Each property corresponds to a schema registered via addStrategy(), addExchange(), or addFrame(). Connection services retrieve this context to determine which cached client instance to return.
The following sequence shows how context flows from public API to connection services:
Diagram: Context propagation through service layers using di-scoped ambient context
ExecutionContextService provides execution-specific state such as the current timestamp (for backtest simulation or live trading) and the execution mode (backtest vs. live). Unlike MethodContext, which remains constant for an entire run, ExecutionContext can change per tick.
| Property | Type | Purpose | Set By |
|---|---|---|---|
date |
Date |
Current execution timestamp | BacktestLogicPrivateService or LiveLogicPrivateService |
backtestMode |
boolean |
Whether running in backtest mode | Logic services |
symbol |
string |
Current trading pair | Logic services |
The private logic services set execution context before each tick operation:
Backtest Mode:
// BacktestLogicPrivateService iterates through historical timeframes
const when = timeframes[i]; // Historical timestamp
const result = await this.strategyGlobalService.tick(symbol, when, true);
Live Mode:
// LiveLogicPrivateService uses real-time dates
const when = new Date(); // Current timestamp
const result = await this.strategyGlobalService.tick(symbol, when, false);
Global services inject this context into client instances, allowing functions like getCandles() to fetch data for the correct timestamp without receiving explicit date parameters.
JavaScript async generators introduce complexity for context propagation because each yield pauses execution and resumes later. Standard dependency injection would lose context across these boundaries.
The MethodContextService.runAsyncIterator() method wraps async generators to maintain context across yields:
Diagram: runAsyncIterator maintains context across async generator yields
Both public logic services follow the same pattern:
public run = (symbol: string, context: { strategyName, exchangeName, frameName }) => {
return MethodContextService.runAsyncIterator(
this.privateLogicService.run(symbol), // Async generator
{
exchangeName: context.exchangeName,
strategyName: context.strategyName,
frameName: context.frameName,
}
);
};
The runAsyncIterator method:
Connection services are the primary consumers of MethodContext. They use the context to determine which cached client instance to return:
Diagram: Connection services consume MethodContext to route to correct client instances
A key architectural principle: client classes never access context services. This keeps business logic pure and testable:
| Layer | Context Access | Reason |
|---|---|---|
| Public Logic Services | Sets context via runAsyncIterator() |
Provides user-facing API with explicit parameters |
| Private Logic Services | No context access | Pure orchestration logic |
| Global Services | Injects ExecutionContext |
Bridges context to clients |
| Connection Services | Reads MethodContext |
Routes to correct schema |
| Client Classes | No context access | Pure business logic, fully testable |
This separation enables unit testing clients without mocking the DI system.
The framework uses the di-scoped library for ambient context management. The scoped() function creates context-aware service classes:
export const MethodContextService = scoped(
class {
constructor(readonly context: IMethodContext) {}
}
);
Key features:
MethodContextService.run(), MethodContextService.runAsyncIterator()MethodContextService.get() returns current instanceTMethodContextService type helper for injectionDiagram: Context lifecycle from creation through generator completion
The DI system registers context services as singletons but their scoped instances vary per operation:
| Service | Registration Type | Instance Scope | Purpose |
|---|---|---|---|
ExecutionContextService |
Singleton | Global | Stores current execution state |
MethodContextService |
Scoped constructor | Per-operation | Provides schema routing |
The complete flow showing both context types in action:
Diagram: Interaction between MethodContext and ExecutionContext during execution
MethodContextService.get() returns typed context with IDE autocompletedi-scoped library for scoped contextFor information about: