This page describes the three execution modes available in backtest-kit: Backtest, Live, and Walker. Each mode orchestrates strategy execution differently to serve distinct use cases. For details on how to register strategies and exchanges, see Component Registration. For information about signal state transitions within these modes, see Signal Lifecycle Overview.
The framework provides three execution classes:
Backtest - Historical simulation with deterministic resultsLive - Real-time trading with crash recoveryWalker - Multi-strategy comparison for optimizationAll three modes share the same strategy code but execute it in different contexts with different time progression models.
| Feature | Backtest | Live | Walker |
|---|---|---|---|
| Execution Pattern | Finite iteration over timeframe | Infinite loop | Sequential backtest runs |
| Time Progression | Historical timestamps from Frame.getTimeframe() |
Real-time Date.now() |
Historical per strategy |
| Context Flag | backtest=true |
backtest=false |
backtest=true |
| Yielded Results | Closed signals only | Opened, closed, cancelled | Progress updates |
| Entry Point | Backtest.run() / Backtest.background() |
Live.run() / Live.background() |
Walker.run() / Walker.background() |
| Orchestration Service | BacktestLogicPrivateService |
LiveLogicPrivateService |
WalkerLogicPrivateService |
| Persistence | None (in-memory only) | Atomic file writes via PersistSignalAdapter |
None (per backtest run) |
| Completion | When timeframe exhausted | Never (infinite) | When all strategies tested |
| Fast-Forward | Yes via strategy.backtest() |
No | Yes (per strategy) |
| Primary Use Case | Strategy validation | Production trading | Strategy optimization |
Backtest Execution Flow Diagram - Shows how BacktestLogicPrivateService iterates through historical timeframes and fast-forwards through signal lifetimes using the backtest() method.
Timeframe Iteration: Backtest mode gets a discrete array of timestamps from Frame.getTimeframe() and iterates through them sequentially. Each timestamp represents when strategy.tick() should be called.
const timeframes = await this.frameGlobalService.getTimeframe(
symbol,
this.methodContextService.context.frameName
);
Fast-Forward Optimization: When a signal opens, backtest mode fetches the next N candles (where N = minuteEstimatedTime) and passes them to strategy.backtest(). This method scans through candles to detect take profit or stop loss hits without calling tick() for every timestamp.
const candles = await this.exchangeGlobalService.getNextCandles(
symbol,
"1m",
signal.minuteEstimatedTime,
when,
true
);
const backtestResult = await this.strategyGlobalService.backtest(
symbol,
candles,
when,
true
);
Timeframe Skipping: After a signal closes, backtest mode skips all intermediate timeframes until closeTimestamp, avoiding redundant processing:
while (
i < timeframes.length &&
timeframes[i].getTime() < backtestResult.closeTimestamp
) {
i++;
}
Scheduled Signal Handling: For scheduled signals (limit orders), backtest mode requests additional candles to monitor activation: CC_SCHEDULE_AWAIT_MINUTES + minuteEstimatedTime + 1. The backtest() method first checks if priceOpen is reached within the timeout window, then monitors TP/SL if activated.
Backtest mode uses async generators to stream results, avoiding array accumulation:
public async *run(symbol: string) {
// Yields results one at a time
while (i < timeframes.length) {
// ... process signal
yield backtestResult;
}
}
This allows processing years of data without exhausting memory. Consumers can break early for conditional termination:
for await (const result of Backtest.run("BTCUSDT", context)) {
if (result.pnl.pnlPercentage < -10) break; // Early exit
}
Live Execution Flow Diagram - Shows how LiveLogicPrivateService runs an infinite loop with real-time clock progression and crash-safe persistence.
Infinite Loop Pattern: Live mode never completes. It runs while (true) with sleep intervals between ticks:
const TICK_TTL = 1 * 60 * 1_000 + 1; // 60 seconds + 1ms
public async *run(symbol: string) {
while (true) {
const when = new Date(); // Real-time progression
const result = await this.strategyGlobalService.tick(symbol, when, false);
// Yield opened/closed/cancelled, skip idle/active
if (result.action === "opened" || result.action === "closed" || result.action === "cancelled") {
yield result;
}
await sleep(TICK_TTL);
}
}
Crash Recovery: On startup, ClientStrategy.waitForInit() reads persisted signal state from disk. If a pending signal exists, it's restored and continues monitoring:
async waitForInit(initial: boolean): Promise<void> {
if (await this.persistSignalAdapter.hasValue(this.entityId)) {
this._signal = await this.persistSignalAdapter.readValue(this.entityId);
// Signal restored, continue from last state
}
}
Atomic Persistence: Every state transition writes to disk atomically via PersistSignalAdapter.writeValue(). This ensures no signal is duplicated or lost on crash. The persistence layer uses temporary files with atomic rename for crash-safety.
Real-Time Monitoring: Unlike backtest mode which uses historical candles for fast-forward, live mode calls getAveragePrice() on every tick to check if take profit or stop loss is hit. This uses VWAP from the last 5 1-minute candles.
For scheduled signals in live mode, minuteEstimatedTime is counted from pendingAt (activation time), not scheduledAt (creation time). This ensures the signal runs for the full duration after activation:
// Scheduled signal lifecycle
scheduledAt: 1704067200000 // Signal created
pendingAt: 1704070800000 // Activated 1 hour later
closeAt: 1704157200000 // Closes minuteEstimatedTime after pendingAt
This was a critical bug fix to prevent premature closure causing financial losses on fees. The test suite verifies this behavior in test/e2e/timing.test.mjs:34-153.
Walker Execution Flow Diagram - Shows how WalkerLogicPrivateService orchestrates multiple backtests sequentially and selects the best strategy by comparing metrics.
Sequential Backtest Execution: Walker mode runs Backtest.run() for each strategy in the list, consuming all results before moving to the next:
for (const strategyName of walkerSchema.strategies) {
// Run full backtest for this strategy
for await (const _ of Backtest.run(symbol, {
strategyName,
exchangeName: walkerSchema.exchangeName,
frameName: walkerSchema.frameName
})) {
// Consume all results
}
// Get statistics after completion
const stats = await backtestMarkdownService.getData(strategyName);
// Compare metrics...
}
Metric-Based Comparison: The walker schema specifies which metric to use for comparison (default: sharpeRatio). Available metrics include:
sharpeRatio - Risk-adjusted return (avgPnl / stdDev)annualizedSharpeRatio - Sharpe × √365winRate - Percentage of winning tradesavgPnl - Average PNL per tradetotalPnl - Cumulative PNLcertaintyRatio - avgWin / |avgLoss|Progress Events: Walker emits progress events after each strategy completes, enabling real-time progress tracking:
walkerEmitter.next({
walkerName,
symbol,
strategyName: currentStrategy,
strategiesTested: i + 1,
totalStrategies,
progress: (i + 1) / totalStrategies,
bestStrategy: currentBest,
bestMetric: currentBestValue
});
State Isolation: Each backtest run in walker mode starts with cleared state for markdown services, strategy, and risk profiles. This ensures strategies don't interfere with each other.
Walker returns comparison data with all tested strategies sorted by metric:
interface IWalkerResults {
walkerName: string;
symbol: string;
metric: string;
bestStrategy: string;
bestMetric: number;
strategies: Array<{
strategyName: string;
stats: BacktestStatistics;
metric: number;
}>;
}
The markdown report includes a comparison table showing all metrics side-by-side for strategy selection.
All three execution modes use the same context propagation architecture but with different parameters:
Context Propagation Across Modes - Shows how both MethodContext and ExecutionContext wrap execution in all three modes.
MethodContext - Identifies which components to use:
strategyName - Which strategy to executeexchangeName - Which exchange to use for dataframeName - Which timeframe to use (backtest only)ExecutionContext - Provides runtime parameters:
symbol - Trading pair being processedwhen - Current timestamp (historical or real-time)backtest - Boolean flag for mode identificationThe key difference between modes is the value of when and backtest:
| Mode | when | backtest |
|---|---|---|
| Backtest | timeframes[i] from Frame |
true |
| Live | new Date() |
false |
| Walker | timeframes[i] per strategy |
true |
Each mode emits events to different subjects for filtered consumption:
Event Emission Architecture - Shows how signals route through global and mode-specific emitters for filtered consumption.
Users can subscribe to all events or mode-specific events:
// All modes
listenSignal((event) => {
console.log(event.action, event.strategyName);
});
// Backtest only
listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log("Backtest PNL:", event.pnl.pnlPercentage);
}
});
// Live only
listenSignalLive((event) => {
if (event.action === "opened") {
console.log("Live signal opened:", event.signal.id);
}
});
All listeners use queued processing to ensure sequential execution even with async callbacks, preventing race conditions.
| Use Case | Recommended Mode | Rationale |
|---|---|---|
| Strategy development | Backtest | Fast iteration with deterministic results |
| Parameter optimization | Walker | Automated comparison of multiple configurations |
| Strategy validation | Backtest | Verify logic against historical data |
| Paper trading | Live | Test real-time execution without risk |
| Production trading | Live | Execute actual trades with crash recovery |
| Performance comparison | Walker | Identify best strategy from candidates |
| Backtesting multiple symbols | Backtest (sequential runs) | Run same strategy across different symbols |
| Multi-timeframe analysis | Backtest (multiple Frame schemas) | Compare performance across timeframes |
Common patterns combine multiple modes:
Development → Validation → Optimization
Backtest.run() for initial validationWalker.run() to optimize parametersLive.run()Paper Trading → Production
Live.run() with test credentialsSchedule.getData() for cancellation rateMulti-Symbol Scanning
Backtest.run() for each symbolHeat.getData() to compare portfolio-wide performanceAll modes emit performance events for profiling:
performanceEmitter.next({
timestamp: Date.now(),
previousTimestamp: lastEventTime,
metricType: "backtest_signal" | "backtest_timeframe" | "backtest_total" | "live_tick",
duration: performance.now() - startTime,
strategyName,
exchangeName,
symbol,
backtest: boolean
});
Available metric types:
backtest_signal - Time to process one signal (tick + backtest)backtest_timeframe - Time to process one timeframe iterationbacktest_total - Total backtest durationlive_tick - Time to process one live tickThese metrics enable bottleneck detection and optimization of strategy logic or data fetching.