Signal Lifecycle Overview

This page describes the signal state machine and lifecycle management in the backtest-kit framework. A signal represents a single trading position from creation through closure, transitioning through well-defined states. This document covers signal generation, validation, state transitions, persistence, and closure conditions.

For information about how signals are executed in different modes (backtest vs live), see Execution Modes. For details on component registration and schema definitions, see Component Registration. For deep implementation details of the ClientStrategy class that manages signals, see ClientStrategy.


The signal lifecycle is modeled as a finite state machine with six distinct states. Signals transition between states based on market conditions, time constraints, and risk parameters.

Mermaid Diagram

State Discriminator Signal Type Description
idle action: "idle" null No active signal exists. Strategy can generate new signal on next getSignal() call.
scheduled action: "scheduled" IScheduledSignalRow Signal created with delayed entry. Waiting for market price to reach priceOpen.
opened action: "opened" ISignalRow Signal just created (immediate) or activated (from scheduled). Position tracking begins.
active action: "active" ISignalRow Signal being monitored for TP/SL/time expiration. Continues until close condition met.
closed action: "closed" ISignalRow Final state with PnL calculation. Includes closeReason and closeTimestamp.
cancelled action: "cancelled" IScheduledSignalRow Scheduled signal cancelled before activation. Timeout or StopLoss hit.

The framework uses a discriminated union pattern for type-safe signal handling. Three core interfaces represent signals at different lifecycle stages.

Mermaid Diagram

Each state transition yields a specific tick result type, enabling type-safe handling with TypeScript discriminators:

Result Type Discriminator Contains Use Case
IStrategyTickResultIdle action: "idle" signal: null No signal exists, strategy idle
IStrategyTickResultScheduled action: "scheduled" signal: IScheduledSignalRow Scheduled signal created
IStrategyTickResultOpened action: "opened" signal: ISignalRow Signal opened/activated
IStrategyTickResultActive action: "active" signal: ISignalRow Signal monitoring continues
IStrategyTickResultClosed action: "closed" signal: ISignalRow, pnl, closeReason Signal closed with result
IStrategyTickResultCancelled action: "cancelled" signal: IScheduledSignalRow Scheduled signal cancelled

Signal generation occurs via the user-defined getSignal() function, followed by framework-level validation and augmentation.

Mermaid Diagram

The VALIDATE_SIGNAL_FN enforces financial safety constraints to prevent invalid signals:

Validation Category Rule Configuration
Price Finiteness All prices must be finite numbers (no NaN/Infinity) Hard-coded
Price Positivity All prices must be > 0 Hard-coded
Long Position Logic priceTakeProfit > priceOpen > priceStopLoss Hard-coded
Short Position Logic priceStopLoss > priceOpen > priceTakeProfit Hard-coded
Minimum TP Distance TP must cover fees + minimum profit CC_MIN_TAKEPROFIT_DISTANCE_PERCENT (default: 0.3%)
Maximum SL Distance SL cannot exceed maximum loss threshold CC_MAX_STOPLOSS_DISTANCE_PERCENT (default: 20%)
Time Constraints minuteEstimatedTime > 0 Hard-coded
Maximum Lifetime Signal cannot block risk limits indefinitely CC_MAX_SIGNAL_LIFETIME_MINUTES (default: 1440 min = 1 day)

User-provided ISignalDto is augmented with framework metadata:

// User provides (from getSignal):
{
position: "long",
priceTakeProfit: 45000,
priceStopLoss: 43000,
minuteEstimatedTime: 60
}

// Framework augments to ISignalRow:
{
id: "uuid-v4-generated", // Auto-generated
priceOpen: 44000, // currentPrice or user-specified
exchangeName: "binance", // From method context
strategyName: "momentum-strategy", // From method context
scheduledAt: 1640000000000, // Current timestamp
pendingAt: 1640000000000, // Same as scheduledAt initially
symbol: "BTCUSDT", // From execution context
_isScheduled: false, // true if priceOpen specified
// ... original fields
}

Occurs when getSignal() returns a signal with priceOpen specified. Signal waits for market price to reach entry point.

Mermaid Diagram

Key Implementation: Scheduled signals do NOT perform risk check at creation time. Risk validation occurs during activation when position opens.

Occurs when getSignal() returns a signal without priceOpen. Position opens immediately at current VWAP.

Mermaid Diagram

Occurs when market price reaches priceOpen for a scheduled signal. Triggers risk check at activation time. The pendingAt timestamp is updated during activation. Time-based expiration calculates from pendingAt, not scheduledAt.

Occurs when scheduled signal times out or StopLoss is hit before activation.

Timeout Condition:

const elapsedTime = currentTime - scheduled.scheduledAt;
const maxTimeToWait = CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
if (elapsedTime >= maxTimeToWait) {
// Cancel signal
}

StopLoss Condition (Priority over activation):

  • Long: currentPrice <= priceStopLoss → cancel
  • Short: currentPrice >= priceStopLoss → cancel

Occurs when signal meets closure condition: TakeProfit hit, StopLoss hit, or time expired.

Mermaid Diagram

Critical: When TakeProfit or StopLoss triggers, the framework uses the exact TP/SL price for PnL calculation, not the current VWAP. This ensures deterministic results.


Signals track two distinct timestamps for lifecycle management:

Timestamp Set During Purpose Usage
scheduledAt Signal creation Records when signal was first generated Timeout calculation for scheduled signals
pendingAt Position activation Records when position opened at priceOpen Time expiration calculation for active signals

Immediate Signals (no priceOpen):

scheduledAt: currentTime,  // Set at creation
pendingAt: currentTime // Same as scheduledAt

Scheduled Signals (with priceOpen):

// At creation:
scheduledAt: currentTime, // Set at creation
pendingAt: currentTime // Temporarily equals scheduledAt

// At activation:
scheduledAt: unchanged, // Original creation time
pendingAt: activationTime // Updated to activation timestamp

Timeout Calculation:

  • Scheduled signal timeout: currentTime - scheduledAt >= CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000
  • Active signal expiration: currentTime - pendingAt >= minuteEstimatedTime * 60 * 1000

Strategies can register callbacks to observe signal state transitions. All callbacks are optional.

Mermaid Diagram

For each state transition, callbacks execute in this order:

  1. State-specific callback (e.g., onOpen, onClose)
  2. onTick callback with tick result

Example for signal opening:

// 1. State-specific callback
if (callbacks.onOpen) {
callbacks.onOpen(symbol, signal, currentPrice, backtest);
}

// 2. onTick callback
if (callbacks.onTick) {
const result: IStrategyTickResultOpened = { action: "opened", ... };
callbacks.onTick(symbol, result, backtest);
}

The ClientStrategy class maintains internal state for signal tracking:

Variable Type Purpose
_pendingSignal ISignalRow | null Currently active signal being monitored
_scheduledSignal IScheduledSignalRow | null Scheduled signal awaiting activation
_lastSignalTimestamp number | null Timestamp of last getSignal() call (for throttling)
_isStopped boolean Flag to prevent new signal generation

Only one signal can exist per symbol at a time. The framework enforces mutual exclusion:

  • _pendingSignal and _scheduledSignal are never both non-null
  • Scheduled signal transitions to pending signal on activation
  • New signals rejected if active signal exists (via risk check)

In live mode, signals persist to disk atomically for crash recovery:

// Persist after every state change
await PersistSignalAdapter.writeSignalData(
strategyName,
symbol,
pendingSignal
);

// Restore on initialization
const restored = await PersistSignalAdapter.readSignalData(
strategyName,
symbol
);

File Location: signal-{strategyName}-{symbol}.json

Atomic Write Pattern: Write to temp file, then rename for crash-safe persistence.


The signal lifecycle follows a deterministic state machine with six states: idle, scheduled, opened, active, closed, and cancelled. Signals are generated via user-defined getSignal() functions, validated by framework rules, and monitored through VWAP-based price checks. State transitions trigger lifecycle callbacks for observability. Timestamps (scheduledAt vs pendingAt) enable precise timeout and expiration calculations. In live mode, signals persist atomically to disk for crash recovery.

For implementation details of signal processing within specific execution modes, see Backtest Execution Flow and Live Execution Flow.