Every signal in Backtest Kit moves through a strict, deterministic state machine. Rather than relying on runtime null checks or optional fields, the framework encodes each lifecycle phase as a TypeScript discriminated union — invalid states are structurally unrepresentable at compile time, so an entire class of state corruption bugs simply cannot exist.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-docs/llms.txt
Use this file to discover all available pages before exploring further.
State Diagram
closed or cancelled, it is terminal.
States Explained
idle
No active signal exists for this symbol. The strategy is running
getSignal on each tick, monitoring market conditions and waiting for a setup to form.scheduled
A signal has been created and validated. The framework is waiting for price to reach the specified entry level before establishing a position. The signal holds an expiry timeout governed by
CC_SCHEDULE_AWAIT_MINUTES.opened
The entry price has been reached and the position has been established. The signal transitions to
opened for exactly one tick before becoming active. Strategy callbacks like onOpen fire at this point.active
The position is live and being monitored on every tick. The framework evaluates VWAP against the take-profit and stop-loss levels. Trailing stops are updated here as price moves favorably.
closed
The position has been exited. The
closeReason field identifies how it ended: take_profit, stop_loss, manual, or time_expired. All PnL calculations are finalized and the closed signal is emitted to reporting.cancelled
The signal was rejected before it could be executed — either by risk validation, a conflicting signal, user intervention, or a timeout. No position was ever opened.
TypeScript Discriminated Union
The type safety is enforced through a discriminated union on theaction field. Each result variant carries only the fields that are meaningful in that state — there are no optional properties anywhere in the type hierarchy.
IStrategyTickResultClosed includes closeReason, pnl, and closeTimestamp — fields that make no sense on IStrategyTickResultIdle and therefore don’t exist on it. The TypeScript compiler enforces this: you cannot access result.closeReason without first narrowing to the closed variant.
Narrowing in practice looks like this:
Accessing closed-position data like
pnl or closeReason on an active result is a compile-time error, not a runtime one. The TypeScript structural type system makes it impossible to express — the property simply doesn’t exist on the type.Signal Validation
Before a signal is accepted into the lifecycle at all, it passes throughVALIDATE_SIGNAL_FN. This runs synchronously and throws with a descriptive error if any constraint is violated. Invalid signals are caught by a trycatch wrapper and silently rejected — the strategy continues running without crashing.
Validation enforces the following invariants:
| Check | Rule |
|---|---|
| Positive prices | priceOpen, priceTakeProfit, and priceStopLoss must all be greater than zero |
| Long TP direction | priceTakeProfit > priceOpen |
| Long SL direction | priceStopLoss < priceOpen |
| Short TP direction | priceTakeProfit < priceOpen |
| Short SL direction | priceStopLoss > priceOpen |
| Time parameters | minuteEstimatedTime and timestamp must be positive |
| Interval throttle | getSignal cannot be called more frequently than the strategy’s configured interval |
ClientStrategy level by comparing the current tick timestamp against _lastSignalTimestamp. If less than intervalMs has elapsed, getSignal is skipped entirely and null is returned.
addRiskSchema run after structural validation passes. Risk validations can additionally check reward/risk ratio, portfolio exposure, time-of-day windows, and any other portfolio-level constraint.
Signal Cancellation and Timeout
A signal in thescheduled state can be cancelled before it ever becomes opened. The cancellation causes are:
- Timeout — the
CC_SCHEDULE_AWAIT_MINUTESwindow expires before price reaches the entry level. The default timeout is 120 minutes. - Risk rejection — a risk validation function throws during the pending-signal evaluation cycle.
- Conflicting signal — a new
getSignalcall returns a different signal ID while another is still scheduled. The previous signal is cancelled and the new one takes its place. - User intervention — the live bot is stopped and restarted without the signal being recoverable from persisted state.