Event System

The Event System provides observability and monitoring capabilities for backtest-kit through a publish-subscribe architecture. It enables external code to react to strategy execution events, errors, and progress updates without coupling to internal execution logic.

This document covers event emitters, listener functions, queued processing, and event flow patterns. For signal lifecycle states, see Signal Lifecycle. For execution mode details, see Execution Modes. For callback configuration, see Strategy Schemas.


The event system uses Subject instances from functools-kit as event emitters. All listener functions wrap callbacks with the queued utility to ensure sequential processing even when callbacks are asynchronous.

Core Pattern:

Event SourceSubject.next() → Queued ListenerUser Callback

Event Emitter Diagram

Mermaid Diagram


Signal events represent strategy execution state changes. Three emitters handle different execution contexts:

Emitter Purpose Event Type
signalEmitter All signal events (live + backtest) IStrategyTickResult
signalBacktestEmitter Backtest-only signals IStrategyTickResult
signalLiveEmitter Live-only signals IStrategyTickResult

The IStrategyTickResult discriminated union includes:

  • IStrategyTickResultIdle - No active position
  • IStrategyTickResultScheduled - Delayed entry signal created
  • IStrategyTickResultOpened - New position opened
  • IStrategyTickResultActive - Position being monitored
  • IStrategyTickResultClosed - Position closed with PnL
  • IStrategyTickResultCancelled - Scheduled signal cancelled

Completion events signal when background execution finishes:

Emitter Purpose Event Type Trigger
doneBacktestSubject Backtest completion DoneContract Backtest.background() ends
doneLiveSubject Live trading completion DoneContract Live.background() ends
doneWalkerSubject Walker completion DoneContract Walker.background() ends

Progress events track execution advancement:

Emitter Purpose Event Type Frequency
progressEmitter Backtest progress ProgressContract Per timeframe batch
walkerEmitter Walker strategy progress WalkerContract Per strategy completion
walkerCompleteSubject Walker final results IWalkerResults Once at end
Emitter Purpose Event Type Trigger
errorEmitter Background execution errors Error Caught exceptions in .background()
validationSubject Risk validation failures Error Risk validation function throws
Emitter Purpose Event Type Usage
performanceEmitter Timing metrics PerformanceContract Profiling and bottleneck detection

All listener functions follow a consistent pattern:

function listen*(fn: (event: EventType) => void): () => void

Returns an unsubscribe function to stop listening.

Function Emitter Queued Description
listenSignal() signalEmitter Yes All signal events
listenSignalBacktest() signalBacktestEmitter Yes Backtest signals only
listenSignalLive() signalLiveEmitter Yes Live signals only
listenError() errorEmitter Yes Background errors
listenDoneBacktest() doneBacktestSubject Yes Backtest completion
listenDoneLive() doneLiveSubject Yes Live completion
listenDoneWalker() doneWalkerSubject Yes Walker completion
listenProgress() progressEmitter Yes Backtest progress
listenPerformance() performanceEmitter Yes Performance metrics
listenWalker() walkerEmitter Yes Walker progress
listenWalkerComplete() walkerCompleteSubject Yes Walker final results
listenValidation() validationSubject Yes Risk validation errors

Once listeners use .filter().once() pattern for one-time execution:

function listen*Once(
filterFn: (event: EventType) => boolean,
fn: (event: EventType) => void
): () => void
Function Emitter Pattern
listenSignalOnce() signalEmitter Filter + auto-unsubscribe
listenSignalBacktestOnce() signalBacktestEmitter Filter + auto-unsubscribe
listenSignalLiveOnce() signalLiveEmitter Filter + auto-unsubscribe
listenDoneBacktestOnce() doneBacktestSubject Filter + auto-unsubscribe
listenDoneLiveOnce() doneLiveSubject Filter + auto-unsubscribe
listenDoneWalkerOnce() doneWalkerSubject Filter + auto-unsubscribe
listenWalkerOnce() walkerEmitter Filter + auto-unsubscribe

All continuous listeners wrap callbacks with queued() from functools-kit to ensure sequential execution:

export function listenSignal(fn: (event: IStrategyTickResult) => void) {
backtest.loggerService.log(LISTEN_SIGNAL_METHOD_NAME);
return signalEmitter.subscribe(queued(async (event) => fn(event)));
}

Guarantees:

  1. Events processed in order received
  2. Next event waits for previous callback to complete
  3. No concurrent callback execution
  4. Prevents race conditions in async handlers

Queued Processing Flow

Mermaid Diagram


Backtest Signal Event Flow

Mermaid Diagram

Key Emission Points:

  1. Signal Events: Emitted by BacktestLogicPrivateService.run() for each tick result
  2. Progress Events: Emitted periodically during timeframe iteration
  3. Completion Events: Emitted when Backtest.background() finishes

Live Signal Event Flow

Mermaid Diagram

Key Emission Points:

  1. Signal Events: Emitted by LiveLogicPrivateService.run() for each tick result
  2. Persistence: State saved before events emitted
  3. Completion Events: Emitted when Live.background() stops and last position closes

Walker Progress Event Flow

Mermaid Diagram

Key Emission Points:

  1. Progress Events: Emitted after each strategy backtest completes
  2. Completion Events: Emitted when all strategies tested
  3. Final Results: Emitted with best strategy selection

Event payloads are defined as TypeScript interfaces:

interface DoneContract {
exchangeName: string;
strategyName: string;
backtest: boolean;
symbol: string;
}

Emitted by completion subjects (doneBacktestSubject, doneLiveSubject, doneWalkerSubject).

interface ProgressContract {
strategyName: string;
exchangeName: string;
frameName: string;
symbol: string;
processedFrames: number;
totalFrames: number;
progress: number; // 0-1 float
}

Emitted by progressEmitter during backtest execution.

interface PerformanceContract {
metricType: PerformanceMetricType;
duration: number; // milliseconds
strategyName?: string;
exchangeName?: string;
symbol?: string;
}

Emitted by performanceEmitter for profiling.

interface WalkerContract {
walkerName: string;
strategyName: string;
exchangeName: string;
frameName: string;
symbol: string;
strategiesTested: number;
totalStrategies: number;
metricValue: number;
bestStrategy: string;
bestMetric: number;
}

Emitted by walkerEmitter after each strategy completes.


Events are emitted by logic services and client classes:

BacktestLogicPrivateService:

// Emit signal events
await signalEmitter.next(result);
await signalBacktestEmitter.next(result);

// Emit progress
await progressEmitter.next({...});

Backtest.background():

// Emit completion
await doneBacktestSubject.next({
exchangeName: context.exchangeName,
strategyName: context.strategyName,
backtest: true,
symbol,
});

Basic Listener:

import { listenSignalBacktest } from "backtest-kit";

const unsubscribe = listenSignalBacktest((event) => {
if (event.action === "closed") {
console.log(`PnL: ${event.pnl.pnlPercentage}%`);
}
});

// Later: stop listening
unsubscribe();

Once Listener:

import { listenDoneBacktestOnce } from "backtest-kit";

listenDoneBacktestOnce(
(event) => event.symbol === "BTCUSDT",
(event) => {
console.log("BTCUSDT backtest completed");
}
);

Error Handling:

import { listenError } from "backtest-kit";

listenError((error) => {
console.error("Background error:", error.message);
// Send to monitoring service
});

Markdown services automatically subscribe to event emitters to accumulate data for report generation:

BacktestMarkdownService:

  • Subscribes to signalBacktestEmitter
  • Accumulates closed signals
  • Generates statistics and markdown reports

LiveMarkdownService:

  • Subscribes to signalLiveEmitter
  • Maintains bounded queue (MAX_EVENTS = 25)
  • Prevents memory leaks in long-running processes

WalkerMarkdownService:

  • Subscribes to walkerEmitter and walkerCompleteSubject
  • Tracks strategy comparison results

Event Type Emitter Listener Contract Queued Once Variant
All signals signalEmitter listenSignal() IStrategyTickResult Yes Yes
Backtest signals signalBacktestEmitter listenSignalBacktest() IStrategyTickResult Yes Yes
Live signals signalLiveEmitter listenSignalLive() IStrategyTickResult Yes Yes
Backtest done doneBacktestSubject listenDoneBacktest() DoneContract Yes Yes
Live done doneLiveSubject listenDoneLive() DoneContract Yes Yes
Walker done doneWalkerSubject listenDoneWalker() DoneContract Yes Yes
Progress progressEmitter listenProgress() ProgressContract Yes No
Performance performanceEmitter listenPerformance() PerformanceContract Yes No
Walker progress walkerEmitter listenWalker() WalkerContract Yes Yes
Walker complete walkerCompleteSubject listenWalkerComplete() IWalkerResults Yes No
Errors errorEmitter listenError() Error Yes No
Validation validationSubject listenValidation() Error Yes No