This document explains the interval throttling mechanism that prevents signal spam by enforcing minimum time gaps between getSignal calls. The throttling system uses INTERVAL_MINUTES mapping and _lastSignalTimestamp tracking to ensure strategies respect their configured signal generation intervals.
For information about the broader live trading execution loop, see Live Execution Flow. For details on signal generation and validation, see Signal Generation and Validation.
Interval throttling is a critical mechanism that prevents strategies from generating signals too frequently. Each strategy declares a SignalInterval (e.g., "1m", "5m", "1h") which determines the minimum time between consecutive getSignal function calls. The framework enforces this interval by tracking the timestamp of the last signal generation attempt and rejecting premature calls.
Key Benefits:
getSignal functionsThe SignalInterval type defines the allowed throttling intervals:
type SignalInterval = "1m" | "3m" | "5m" | "15m" | "30m" | "1h"
This interval is specified in the strategy schema during registration:
| Field | Type | Required | Description |
|---|---|---|---|
interval |
SignalInterval |
Yes | Minimum time between getSignal calls |
strategyName |
string |
Yes | Unique strategy identifier |
getSignal |
function |
Yes | Signal generation function (throttled) |
Example Strategy Registration:
addStrategy({
strategyName: "my-strategy",
interval: "5m", // Throttle to maximum one signal per 5 minutes
getSignal: async (symbol) => {
// This function will only be called every 5 minutes
return { /* signal data */ };
}
});
The framework converts SignalInterval strings to millisecond durations via the INTERVAL_MINUTES constant:
Mapping Definition:
The mapping is defined at src/client/ClientStrategy.ts:31-38:
const INTERVAL_MINUTES: Record<SignalInterval, number> = {
"1m": 1,
"3m": 3,
"5m": 5,
"15m": 15,
"30m": 30,
"1h": 60,
};
Usage in Throttling Check:
const intervalMinutes = INTERVAL_MINUTES[self.params.interval];
const intervalMs = intervalMinutes * 60 * 1000;
This conversion happens during every throttling check within GET_SIGNAL_FN.
Each ClientStrategy instance maintains a private field _lastSignalTimestamp that records when getSignal was last called:
Field Declaration:
The field is declared in ClientStrategy class (not shown in provided excerpts, but referenced at src/client/ClientStrategy.ts:201-207):
private _lastSignalTimestamp: number | null = null;
Update Logic:
self._lastSignalTimestamp = currentTime;
This assignment occurs immediately before calling the user's getSignal function, ensuring that the timestamp reflects the most recent signal generation attempt.
The throttling check occurs in GET_SIGNAL_FN before calling the user-defined getSignal function:
Implementation at src/client/ClientStrategy.ts:194-208:
const currentTime = self.params.execution.context.when.getTime();
{
const intervalMinutes = INTERVAL_MINUTES[self.params.interval];
const intervalMs = intervalMinutes * 60 * 1000;
// Check that enough time has passed since last getSignal
if (
self._lastSignalTimestamp !== null &&
currentTime - self._lastSignalTimestamp < intervalMs
) {
return null;
}
self._lastSignalTimestamp = currentTime;
}
Key Observations:
_lastSignalTimestamp is null, the check is skipped (no delay on strategy startup)null without calling getSignalgetSignal to prevent race conditionsexecution.context.when which is either Date.now() (live) or historical timestamp (backtest)The throttling check is the first gate in the signal generation pipeline:
Position in Call Stack:
ClientStrategy.tick(symbol) or ClientStrategy.backtest(candles)GET_SIGNAL_FN checks _lastSignalTimestamp immediatelygetSignal → ValidationIStrategyTickResult (idle if throttled)In live trading, throttling operates on real-time clock progression. The live execution loop repeatedly calls tick() with Date.now() as the context timestamp. Throttling prevents excessive getSignal calls during this infinite loop:
while (true) {
// ExecutionContext.when = Date.now()
const result = await this.strategyConnectionService.tick();
// Throttling happens inside tick() → GET_SIGNAL_FN
await sleep(TICK_TTL);
}
In backtesting, throttling operates on historical timestamp progression. The backtest loop iterates through discrete timestamps from Frame.getTimeframe(). Throttling ensures that even if the frame interval is "1m" (one tick per minute), a strategy with interval "5m" only evaluates getSignal every 5 minutes:
| Frame Timestamp | Throttle Check | Result |
|---|---|---|
| 2024-01-01 00:00 | First call (null) | Allow → getSignal() |
| 2024-01-01 00:01 | 1 min < 5 min | Block → return idle |
| 2024-01-01 00:02 | 2 min < 5 min | Block → return idle |
| 2024-01-01 00:03 | 3 min < 5 min | Block → return idle |
| 2024-01-01 00:04 | 4 min < 5 min | Block → return idle |
| 2024-01-01 00:05 | 5 min >= 5 min | Allow → getSignal() |
| 2024-01-01 00:06 | 1 min < 5 min | Block → return idle |
The throttling mechanism has two bypass conditions where getSignal is not called, regardless of interval:
if (self._isStopped) {
return null;
}
When ClientStrategy.stop() is called (e.g., during graceful shutdown), the _isStopped flag prevents all future getSignal calls. This check occurs before the throttling check at src/client/ClientStrategy.ts:191-193.
When a scheduled signal is waiting for activation (self._scheduledSignal !== null), the strategy does not generate new signals. This prevents multiple concurrent signals from the same strategy.
Check Location:
The scheduled signal check happens in ClientStrategy.tick() before calling GET_SIGNAL_FN. See Signal Lifecycle Overview for details on scheduled signal behavior.
Throttling and risk management serve different purposes:
| Aspect | Throttling | Risk Management |
|---|---|---|
| Purpose | Rate limit signal generation | Portfolio position limits |
| Scope | Per-strategy, per-symbol | Cross-strategy, portfolio-level |
| Timing | Before getSignal call |
After signal generation |
| Configuration | interval in strategy schema |
riskName and validations |
| Bypass | None (always enforced) | Can be disabled (no riskName) |
| Failure Mode | Silent (return null) | Logged rejection |
Example Scenario:
addStrategy({
strategyName: "scalper",
interval: "1m", // Throttling: max 1 signal/minute
riskName: "aggressive", // Risk: check position limits
getSignal: async (symbol) => { /* ... */ }
});
addRisk({
riskName: "aggressive",
validations: [
(payload) => {
// Risk: max 5 concurrent positions across all strategies
if (payload.activePositionCount >= 5) {
throw new Error("Portfolio limit reached");
}
}
]
});
Execution Flow:
getSignal not called| Entity | Type | Location | Role |
|---|---|---|---|
SignalInterval |
Type | src/interfaces/Strategy.interface.ts:11-17 | Defines allowed throttling intervals |
INTERVAL_MINUTES |
Constant | src/client/ClientStrategy.ts:31-38 | Maps interval strings to minute durations |
_lastSignalTimestamp |
Field | ClientStrategy class |
Tracks last signal generation time |
GET_SIGNAL_FN |
Function | src/client/ClientStrategy.ts:187-283 | Implements throttling logic |
IStrategySchema.interval |
Property | src/interfaces/Strategy.interface.ts:126 | Strategy's configured throttle interval |
ExecutionContextService.context.when |
Property | src/lib/services/context/ExecutionContextService.ts | Current timestamp (live or backtest) |