Scheduled signals are limit orders that wait for price to reach a specific entry point (priceOpen) before activating. Unlike immediate signals that open at current market price, scheduled signals remain in a "scheduled" state until market conditions satisfy the entry criteria or the signal is cancelled due to timeout or adverse price movement.
For information about the complete signal lifecycle including other states, see Signal States. For signal generation and validation, see Signal Generation and Validation. For persistence of scheduled signals, see Signal Persistence.
Scheduled signals enable strategies to implement limit order behavior, where positions open only when price reaches a favorable entry point. This contrasts with market orders that execute immediately at current price.
Key characteristics:
priceOpen is specified in ISignalDto, but position opens only when price reaches priceOpenpriceOpen before timeout (CC_SCHEDULE_AWAIT_MINUTES, default 120 minutes)priceStopLoss is hit before priceOpen is reachedscheduledAt (creation time) and pendingAt (activation time) track full lifecycleA signal becomes scheduled when getSignal() returns an ISignalDto with priceOpen specified. The system determines scheduling based on price conditions at creation time.
// Immediate signal (opens at current price)
return {
position: "long",
// priceOpen omitted - opens immediately
priceTakeProfit: 43000,
priceStopLoss: 41000,
minuteEstimatedTime: 60
};
// Scheduled signal (waits for priceOpen)
return {
position: "long",
priceOpen: 42000, // Waits for price to reach 42000
priceTakeProfit: 43000,
priceStopLoss: 41000,
minuteEstimatedTime: 60
};
When priceOpen is provided, the system checks if it should activate immediately or wait:
| Position | Condition for Immediate Activation | Behavior |
|---|---|---|
long |
currentPrice <= priceOpen |
Price already low enough, activate immediately |
short |
currentPrice >= priceOpen |
Price already high enough, activate immediately |
If immediate activation conditions are met, the signal transitions directly to "opened" state, skipping the "scheduled" phase.
Key state transitions:
priceOpen and risk checks passCC_SCHEDULE_AWAIT_MINUTESpriceStopLoss hit before priceOpen reachedpriceOpen but risk validation rejects activationScheduled signals activate when market price reaches the specified priceOpen. The activation logic differs by position type:
Critical ordering: StopLoss check has priority over activation check. This prevents opening positions that would immediately lose money.
// LONG position checks (from CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN)
if (scheduled.position === "long") {
// 1. Check StopLoss FIRST (cancellation takes priority)
if (currentPrice <= scheduled.priceStopLoss) {
shouldCancel = true;
}
// 2. Check activation only if StopLoss NOT hit
else if (currentPrice <= scheduled.priceOpen) {
shouldActivate = true;
}
}
// SHORT position checks
if (scheduled.position === "short") {
// 1. Check StopLoss FIRST
if (currentPrice >= scheduled.priceStopLoss) {
shouldCancel = true;
}
// 2. Check activation only if StopLoss NOT hit
else if (currentPrice >= scheduled.priceOpen) {
shouldActivate = true;
}
}
When activation occurs, pendingAt is updated to reflect the actual activation time:
| Mode | Activation Timestamp | Rationale |
|---|---|---|
| Backtest | candle.timestamp + 60000 |
Next candle after activation candle (precise timing) |
| Live | Date.now() |
Current time when activation detected (approximate timing) |
Scheduled signals can be cancelled without opening a position in two scenarios: timeout or pre-activation StopLoss hit.
Scheduled signals have a maximum waiting period defined by CC_SCHEDULE_AWAIT_MINUTES (default: 120 minutes). If priceOpen is not reached within this period, the signal is cancelled.
// Timeout calculation (from CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN)
const currentTime = execution.context.when.getTime();
const signalTime = scheduled.scheduledAt;
const maxTimeToWait = CC_SCHEDULE_AWAIT_MINUTES * 60 * 1000;
const elapsedTime = currentTime - signalTime;
if (elapsedTime >= maxTimeToWait) {
// Cancel signal, emit IStrategyTickResultCancelled
await setScheduledSignal(null);
}
Timeout characteristics:
setConfig()elapsedTime >= maxTimeToWait (inclusive)If price moves against the position and hits priceStopLoss before reaching priceOpen, the scheduled signal is cancelled. This prevents opening positions that are already in a losing state.
Pre-activation cancellation is critical because it prevents the following scenario:
Scheduled signals maintain two distinct timestamps that track the full lifecycle from creation to activation:
| Timestamp | Set At | Purpose | Persists After Activation |
|---|---|---|---|
scheduledAt |
Signal creation in GET_SIGNAL_FN |
Records when strategy first generated the signal | ✅ Yes (unchanged) |
pendingAt |
Initial: equals scheduledAtFinal: activation time |
Temporary placeholder until activation, then updated to actual activation time | ✅ Yes (updated) |
The dual-timestamp system enables accurate duration tracking:
// Timeout calculation: uses scheduledAt
const waitingTime = currentTime - scheduledAt;
if (waitingTime >= CC_SCHEDULE_AWAIT_MINUTES * 60000) {
// Cancel after 120 minutes of waiting
}
// Signal lifetime calculation: uses pendingAt
const activeTime = closeTime - pendingAt;
if (activeTime >= minuteEstimatedTime * 60000) {
// Close by time_expired
}
Without separate timestamps:
minuteEstimatedTime would include waiting period, causing premature time_expired closuresWith separate timestamps:
scheduledAt tracks total signal lifetime (creation → closure)pendingAt tracks active position lifetime (activation → closure)minuteEstimatedTime only counts against pendingAt, not scheduledAt// From ACTIVATE_SCHEDULED_SIGNAL_FN
const activationTime = activationTimestamp;
// Create activated signal with updated pendingAt
const activatedSignal: ISignalRow = {
...scheduled,
pendingAt: activationTime, // UPDATED from scheduledAt
_isScheduled: false,
};
await setPendingSignal(activatedSignal);
Scheduled signals are persisted to disk using PersistScheduleAdapter, enabling crash recovery for live trading. This is separate from PersistSignalAdapter (for active positions).
Scheduled signals are stored separately from active signals:
./dump/data/
├── schedule/
│ ├── my-strategy/
│ │ ├── BTCUSDT.json # Scheduled signal for BTCUSDT
│ │ └── ETHUSDT.json # Scheduled signal for ETHUSDT
│ └── another-strategy/
│ └── BTCUSDT.json
└── signal/
├── my-strategy/
│ └── BTCUSDT.json # Active position (after activation)
└── another-strategy/
└── BTCUSDT.json
priceOpen=42000./dump/data/schedule/my-strategy/BTCUSDT.jsonLive.background() called againWAIT_FOR_INIT_FN loads from PersistScheduleAdapterpriceOpen=42000PersistSignalAdapter| Operation | Function | File | Description |
|---|---|---|---|
| Write scheduled | setScheduledSignal(signal) |
src/client/ClientStrategy.ts:863-881 | Atomically write scheduled signal to disk |
| Read scheduled | readScheduleData() |
src/classes/Persist.ts | Load scheduled signal on restart |
| Delete scheduled | setScheduledSignal(null) |
src/client/ClientStrategy.ts:863-881 | Remove scheduled signal after activation/cancellation |
| Restore on init | WAIT_FOR_INIT_FN |
src/client/ClientStrategy.ts:525-551 | Load scheduled signal from disk on waitForInit() |
Scheduled signals undergo additional validation beyond immediate signals because they must remain valid during the waiting period.
For LONG positions:
// priceOpen must be BETWEEN SL and TP
if (signal.priceOpen <= signal.priceStopLoss) {
throw new Error(
`Long scheduled: priceOpen (${signal.priceOpen}) <= priceStopLoss (${signal.priceStopLoss}). ` +
`Signal would be immediately cancelled on activation.`
);
}
if (signal.priceOpen >= signal.priceTakeProfit) {
throw new Error(
`Long scheduled: priceOpen (${signal.priceOpen}) >= priceTakeProfit (${signal.priceTakeProfit}). ` +
`Signal would close immediately on activation. This is logically impossible for LONG position.`
);
}
For SHORT positions:
// priceOpen must be BETWEEN TP and SL
if (signal.priceOpen >= signal.priceStopLoss) {
throw new Error(
`Short scheduled: priceOpen (${signal.priceOpen}) >= priceStopLoss (${signal.priceStopLoss}). ` +
`Signal would be immediately cancelled on activation.`
);
}
if (signal.priceOpen <= signal.priceTakeProfit) {
throw new Error(
`Short scheduled: priceOpen (${signal.priceOpen}) <= priceTakeProfit (${signal.priceTakeProfit}). ` +
`Signal would close immediately on activation. This is logically impossible for SHORT position.`
);
}
| Validation | Error Message | Prevention |
|---|---|---|
priceOpen not finite |
priceOpen must be a finite number, got NaN |
Prevents math errors during price checks |
priceOpen <= 0 |
priceOpen must be positive, got -100 |
Prevents invalid negative prices |
LONG: priceOpen <= priceStopLoss |
Signal would be immediately cancelled on activation |
Prevents instant cancellation after activation |
LONG: priceOpen >= priceTakeProfit |
Signal would close immediately on activation |
Prevents impossible LONG logic |
SHORT: priceOpen >= priceStopLoss |
Signal would be immediately cancelled on activation |
Prevents instant cancellation after activation |
SHORT: priceOpen <= priceTakeProfit |
Signal would close immediately on activation |
Prevents impossible SHORT logic |
Scenario: Scheduled signal activates AND reaches TP/SL within the same 1-minute candle.
Candle: open=43000, high=43000, low=40500, close=42500
LONG: priceOpen=41000, priceTakeProfit=42000, priceStopLoss=39000
Sequence:
1. Low=40500 touches priceOpen=41000 → Signal ACTIVATES
2. Close=42500 above priceTakeProfit=42000 → Signal CLOSES by TP
3. Both occur within same candle timestamp
Behavior:
IStrategyTickResultOpened (activation)IStrategyTickResultClosed with closeReason="take_profit" (immediate closure)scheduledAt != pendingAt (different timestamps maintained)Scenario: Extreme volatility causes candle to cross both TP and SL after activation.
LONG: priceOpen=42000, priceTakeProfit=43000, priceStopLoss=41000
Volatile candle: open=42000, high=43500, low=40500, close=42000
Price path (inferred):
1. Opens at 42000 (position active)
2. Rises to 43500 (above TP=43000)
3. Falls to 40500 (below SL=41000)
Behavior:
take_profitScenario: Strategy generates multiple scheduled signals before the first activates.
Time T0: Signal1 scheduled (priceOpen=41000)
Time T1: Signal2 scheduled (priceOpen=40500)
Time T2: Signal3 scheduled (priceOpen=40000)
Behavior:
PersistScheduleAdapter stores single signal per {strategyName, symbol} keyCritical protection: Prevents leveraging portfolio beyond risk limits.
Scenario: LONG limit order where priceOpen is reached before StopLoss.
LONG: priceOpen=41000, priceStopLoss=40000
Price drops from 43000:
- At 41000: priceOpen reached → Signal ACTIVATES
- At 40000: priceStopLoss reached → Signal CLOSES by SL
Pre-activation cancellation is IMPOSSIBLE because priceOpen comes first.
Behavior:
stop_loss immediately after activationCritical distinction:
Scenario: Scheduled signal reaches exactly CC_SCHEDULE_AWAIT_MINUTES (120 minutes).
scheduledAt: 2024-01-01 00:00:00
currentTime: 2024-01-01 02:00:00
elapsedTime: 120 minutes
Condition: elapsedTime >= maxTimeToWait
Behavior:
>= means timeout occurs at exactly 120 minutesIStrategyTickResultCancelled emitted| Type | File | Purpose |
|---|---|---|
IScheduledSignalRow |
src/interfaces/Strategy.interface.ts:64-73 | Scheduled signal row (extends ISignalRow with required priceOpen) |
IStrategyTickResultScheduled |
types.d.ts:788-807 | Tick result when signal is in scheduled state |
IStrategyTickResultCancelled |
types.d.ts:867-895 | Tick result when scheduled signal is cancelled |
| Function | File | Purpose |
|---|---|---|
GET_SIGNAL_FN |
src/client/ClientStrategy.ts:332-476 | Determines if signal should be immediate or scheduled based on priceOpen |
CHECK_SCHEDULED_SIGNAL_TIMEOUT_FN |
src/client/ClientStrategy.ts:554-608 | Checks if scheduled signal has exceeded timeout limit |
CHECK_SCHEDULED_SIGNAL_PRICE_ACTIVATION_FN |
src/client/ClientStrategy.ts:610-644 | Determines if scheduled signal should activate or cancel based on price |
CANCEL_SCHEDULED_SIGNAL_BY_STOPLOSS_FN |
src/client/ClientStrategy.ts:646-679 | Cancels scheduled signal when StopLoss hit before activation |
ACTIVATE_SCHEDULED_SIGNAL_FN |
src/client/ClientStrategy.ts:681-774 | Activates scheduled signal when priceOpen is reached |
setScheduledSignal |
src/client/ClientStrategy.ts:863-881 | Persists scheduled signal to disk via PersistScheduleAdapter |
WAIT_FOR_INIT_FN |
src/client/ClientStrategy.ts:525-551 | Restores scheduled signal from disk on crash recovery |
| Class | File | Purpose |
|---|---|---|
ClientStrategy |
src/client/ClientStrategy.ts | Implements signal lifecycle including scheduled signal management |
PersistScheduleAdapter |
src/classes/Persist.ts | Persists scheduled signals to ./dump/data/schedule/ for crash recovery |
| Parameter | Default | File | Purpose |
|---|---|---|---|
CC_SCHEDULE_AWAIT_MINUTES |
120 |
src/config/params.ts | Maximum waiting time for scheduled signal before timeout cancellation |
CC_MIN_TAKEPROFIT_DISTANCE_PERCENT |
Configurable | src/config/params.ts | Minimum distance between priceOpen and priceTakeProfit |
CC_MIN_STOPLOSS_DISTANCE_PERCENT |
Configurable | src/config/params.ts | Minimum distance between priceOpen and priceStopLoss |