Signal Lifecycle

This document provides a comprehensive guide to the signal lifecycle in backtest-kit. It covers signal states, generation, validation, state transitions, and persistence. The signal lifecycle is the core mechanism through which trading positions are created, monitored, and closed by the framework.

For information about risk management checks that occur during signal generation, see Risk Management. For details on execution modes (Backtest vs Live) that affect lifecycle behavior, see Execution Modes.


Signals in backtest-kit follow a discriminated union pattern with six possible states. Each state is represented by a specific TypeScript interface with an action discriminator field for type-safe handling.

Mermaid Diagram


The framework defines a hierarchy of signal types with increasing levels of completeness and metadata.

Type Description Key Fields Usage
ISignalDto User-returned signal from getSignal() position, priceTakeProfit, priceStopLoss, minuteEstimatedTime, optional priceOpen Returned by strategy's getSignal function
ISignalRow Validated signal with metadata Extends ISignalDto + id, priceOpen (required), scheduledAt, pendingAt, symbol, strategyName, exchangeName, _isScheduled Used throughout lifecycle
IScheduledSignalRow Scheduled signal variant Extends ISignalRow, enforces priceOpen presence Represents delayed entry signals

Mermaid Diagram


Signal generation occurs within ClientStrategy and involves throttling, risk checks, and validation. The GET_SIGNAL_FN wrapper coordinates this process.

Mermaid Diagram


The VALIDATE_SIGNAL_FN enforces critical safety checks to prevent invalid signals from entering the system. All validations throw descriptive errors if checks fail.

1. Finite Number Protection

// Protects against NaN/Infinity from calculation errors
if (!isFinite(signal.priceOpen)) { /* error */ }
if (!isFinite(signal.priceTakeProfit)) { /* error */ }
if (!isFinite(signal.priceStopLoss)) { /* error */ }

2. Price Positivity

// All prices must be positive
priceOpen > 0
priceTakeProfit > 0
priceStopLoss > 0

3. Position Logic (Long)

// Long position: buy low, sell high
priceTakeProfit > priceOpen > priceStopLoss

4. Position Logic (Short)

// Short position: sell high, buy low
priceStopLoss > priceOpen > priceTakeProfit

5. TakeProfit Distance

// Must cover trading fees (default 0.3% > 2×0.1% fees)
const tpDistancePercent = Math.abs((priceTakeProfit - priceOpen) / priceOpen) * 100;
tpDistancePercent >= CC_MIN_TAKEPROFIT_DISTANCE_PERCENT

6. StopLoss Distance

// Prevents catastrophic losses (default max 20%)
const slDistancePercent = Math.abs((priceStopLoss - priceOpen) / priceOpen) * 100;
slDistancePercent <= CC_MAX_STOPLOSS_DISTANCE_PERCENT

7. Signal Lifetime

// Prevents eternal signals blocking risk limits (default max 1440 minutes = 1 day)
minuteEstimatedTime <= CC_MAX_SIGNAL_LIFETIME_MINUTES

When no active signal exists, ClientStrategy.tick() attempts to generate a new signal. The flow differs based on whether priceOpen is specified.

Mermaid Diagram

Key Difference: Immediate signals undergo risk check and call risk.addSignal() immediately. Scheduled signals defer risk check until price activation.


Scheduled signals represent delayed entry positions that wait for price to reach priceOpen. They have special activation and cancellation logic.

Mermaid Diagram

The framework prioritizes StopLoss cancellation over activation to prevent opening positions that would immediately lose:

// CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN logic
if (scheduled.position === "long") {
// Check StopLoss FIRST (cancellation priority)
if (currentPrice <= scheduled.priceStopLoss) {
shouldCancel = true;
}
// Only activate if NOT cancelled
else if (currentPrice <= scheduled.priceOpen) {
shouldActivate = true;
}
}

Once a signal is opened (stored in _pendingSignal), it enters active monitoring. The framework checks for TP/SL conditions and time expiration on each tick.

Mermaid Diagram

Critical Detail: Time expiration uses pendingAt timestamp, not scheduledAt. For scheduled signals, this ensures minuteEstimatedTime counts from activation, not from creation.


Signals maintain two critical timestamps with distinct semantics:

Timestamp Meaning Set When Used For
scheduledAt Signal creation time Signal first generated by getSignal() Tracking signal age, scheduled timeout calculation
pendingAt Position active time Immediate: same as scheduledAt
Scheduled: updated on activation
minuteEstimatedTime duration calculation, TP/SL/time monitoring

Mermaid Diagram

Mermaid Diagram


In live trading mode, signals are persisted to disk after every state change to enable crash recovery. The PersistSignalAdapter provides atomic file operations.

Mermaid Diagram

// setPendingSignal implementation
async setPendingSignal(signal: ISignalRow | null) {
this._pendingSignal = signal;

// Persist only in live mode (not backtest)
if (!this.params.execution.context.backtest) {
await PersistSignalAdaper.writeSignalData(
this.params.strategyName,
this.params.execution.context.symbol,
signal
);
}
}

// waitForInit implementation
async waitForInit() {
if (this.params.execution.context.backtest) {
return; // No persistence in backtest
}

const pendingSignal = await PersistSignalAdaper.readSignalData(
this.params.strategyName,
this.params.execution.context.symbol
);

if (pendingSignal) {
this._pendingSignal = pendingSignal;

// Call onActive callback for restored signal
if (this.params.callbacks?.onActive) {
const currentPrice = await this.params.exchange.getAveragePrice(
this.params.execution.context.symbol
);
this.params.callbacks.onActive(
this.params.execution.context.symbol,
pendingSignal,
currentPrice,
false // backtest=false
);
}
}
}

Note: Scheduled signals (_scheduledSignal) are NOT persisted. Only active positions (_pendingSignal) survive crashes.


Profit and loss is calculated by toProfitLossDto which applies trading fees and slippage to both entry and exit prices.

Mermaid Diagram

// Original signal
priceOpen = 100
priceTakeProfit = 101

// TP hit, calculate PnL
priceClose = 101

// Apply fees/slippage to entry
entryPrice = 100 * (1 + 0.001) * (1 + 0.001) = 100.2001

// Apply fees/slippage to exit
exitPrice = 101 * (1 - 0.001) * (1 - 0.001) = 100.797999

// Calculate PnL
pnlPercentage = ((100.797999 - 100.2001) / 100.2001) * 100 = 0.597%
// Original signal
priceOpen = 100
priceTakeProfit = 99

// TP hit, calculate PnL
priceClose = 99

// Apply fees/slippage to entry (worse price for short = lower)
entryPrice = 100 * (1 - 0.001) * (1 - 0.001) = 99.7999

// Apply fees/slippage to exit (worse price for short = higher)
exitPrice = 99 * (1 + 0.001) * (1 + 0.001) = 99.198001

// Calculate PnL
pnlPercentage = ((99.7999 - 99.198001) / 99.7999) * 100 = 0.603%

Note: The CC_MIN_TAKEPROFIT_DISTANCE_PERCENT default of 0.3% accounts for the 0.2% total fees (entry + exit), ensuring profitable trades after costs.


The signal lifecycle behaves differently in backtest and live modes due to timing and data availability constraints.

Aspect Backtest Mode Live Mode
Time Source Historical candle timestamps Date.now()
Signal Generation Once per candle timestamp Throttled by real time + INTERVAL_MINUTES
TP/SL Detection Check candle.high and candle.low Check VWAP from getAveragePrice()
Fast-Forward strategy.backtest(candles) processes all at once strategy.tick() processes one tick at a time
Scheduled Activation Timestamp candle.timestamp + 60*1000 (next candle) Actual tick time when detected
Persistence None PersistSignalAdapter writes to disk
Crash Recovery N/A waitForInit() restores state
Callbacks backtest=true flag backtest=false flag

Mermaid Diagram

Key Optimization: The backtest method processes all candles in a single pass without yielding control, making it significantly faster than tick-by-tick iteration.


Every state transition emits events through Subject-based emitters, enabling observability and report generation.

Mermaid Diagram

Event Flow: Each state transition calls the specific lifecycle callback (e.g., onOpen), then always calls onTick with the full result. The result is then emitted to all registered listeners via the Subject pattern.


Function Location Purpose Returns
GET_SIGNAL_FN ClientStrategy.ts:187-283 Throttled signal generation with risk check ISignalRow | IScheduledSignalRow | null
VALIDATE_SIGNAL_FN ClientStrategy.ts:40-185 Validate prices, TP/SL logic, distances, lifetime void (throws on error)
CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN ClientStrategy.ts:332-386 Check if scheduled signal timed out IStrategyTickResultCancelled | null
CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN ClientStrategy.ts:388-422 Determine if scheduled signal should activate/cancel { shouldActivate, shouldCancel }
ACTIVATE_SCHEDULED_SIGNAL_FN ClientStrategy.ts:459-551 Convert scheduled to active signal (live) IStrategyTickResultOpened | null
ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN ClientStrategy.ts:897-973 Convert scheduled to active signal (backtest) boolean
OPEN_NEW_PENDING_SIGNAL_FN ClientStrategy.ts:623-673 Create immediate entry signal IStrategyTickResultOpened | null
OPEN_NEW_SCHEDULED_SIGNAL_FN ClientStrategy.ts:578-621 Create delayed entry signal IStrategyTickResultScheduled
CHECK_PENDING_SIGNAL_COMPLETION_FN ClientStrategy.ts:675-734 Check TP/SL/time conditions IStrategyTickResultClosed | null
CLOSE_PENDING_SIGNAL_FN ClientStrategy.ts:736-789 Close signal and calculate PnL (live) IStrategyTickResultClosed
CLOSE_PENDING_SIGNAL_IN_BACKTEST_FN ClientStrategy.ts:975-1006 Close signal and calculate PnL (backtest) IStrategyTickResultClosed
toProfitLossDto toProfitLossDto.ts:1-50 Calculate PnL with fees/slippage IStrategyPnL