Called when a DCA (average-buy) entry is committed.
Called when a breakeven stop is committed (stop loss moved to entry price).
Called on every live tick while a pending signal (open position) is monitored,
BEFORE TP/SL/time evaluation (payload.type is always "active").
Query the exchange by payload.signalId. Return normally to keep monitoring.
THROW semantics (resolved into IBrokerOrderVerdict):
payload.attempt incremented, up to
CC_ORDER_CHECK_RETRY_ATTEMPTS CONSECUTIVE failures (a successful check resets the
streak to 0). A connectivity blip no longer closes a live position on the spot.
Exhaustion acts terminally (close "closed") and signals a fatal exit (exitEmitter).
With the config at 0 any failure is terminal immediately (legacy).Manual wiring — EXCEPTION-BASED VARIANT
This is the throw-driven alternative to the imperative commit-function wiring in
onSignalActivePing:
onSignalActivePing + src/function/strategy.ts): call
commitClosePending / commitCreateTakeProfit / commitCreateStopLoss to close with the
correct reason and handle TP vs SL vs no-counterparty separately.Pick ONE per condition — do not both throw here AND commitClosePending in the active-ping for
the same "order gone" event.
async onOrderActiveCheck(payload: BrokerOrderCheckPayload) {
let order: Order | null;
try {
order = await this.exchange.getOrderById(payload.signalId);
} catch (networkError) {
// transient — tolerated up to CC_ORDER_CHECK_RETRY_ATTEMPTS consecutive failures
throw OrderTransientError.fromError(networkError);
}
if (!order) {
throw new OrderDeletedError(`Order ${payload.signalId} not found`); // confirmed gone -> close "closed" at once
}
}
Called when a signal is being closed (take-profit, stop-loss, or manual close). Emitted via syncSubject BEFORE the framework mutates strategy state, so it is also the close gate.
MANUAL WIRING — EXCEPTION-BASED: place the real exit order here (tag/look up by payload.signalId)
and record final PnL. Return normally to let the close proceed. THROW semantics
(resolved into IBrokerOrderVerdict):
payload.attempt incremented, up to CC_ORDER_CLOSE_RETRY_ATTEMPTS
consecutive rejections. On exhaustion the engine FORCE-CLOSES its own state with the
ORIGINAL closeReason and signals a fatal exit (exitEmitter) — the real exchange
position must then be reconciled by the adapter/operator (the close lifecycle event
still reaches onSignalPendingClose). With the config at 0 the cap is disabled and
a rejected close retries forever (legacy).This differs from onSignalPendingClose, which is the informational lifecycle hook that fires
AFTER the close is committed (and cannot veto it).
Called when an order is being opened. Emitted via syncSubject BEFORE the framework mutates
strategy state, so it is also the open gate. Discriminated by payload.type:
MANUAL WIRING — EXCEPTION-BASED: place the real order here (tag the exchange order with
clientOrderId = payload.signalId so later onOrderActiveCheck / onOrderScheduleCheck /
onSignalActivePing can find it). Return normally to let the open proceed. THROW
semantics (resolved into IBrokerOrderVerdict):
payload.attempt incremented, up to
CC_ORDER_OPEN_RETRY_ATTEMPTS. The attempt is PRE-ARMED (persisted before this hook
runs), so even a crash mid-attempt resumes with attempt >= 1. Because the id is
stable, the adapter MUST reconcile at attempt > 0: query the prior order by
clientOrderId BEFORE re-sending and confirm the open if it filled. Do NOT rely on
catching a "duplicate" error on re-send — on Binance the duplicate-clientOrderId
guard only covers OPEN orders, an instantly-filled market order will not dup.
Exhaustion drops the signal and signals a fatal exit (exitEmitter).
With the config at 0 the retry slot is disabled: the next tick regenerates a FRESH id.This differs from onSignalPendingOpen, which is the informational lifecycle hook that fires
AFTER the open is committed (and cannot veto it).
Called on every live tick while a scheduled signal (resting entry order) is monitored,
BEFORE timeout/price-activation evaluation (payload.type is always "schedule").
Query the exchange by payload.signalId. Return normally to keep monitoring.
THROW semantics (resolved into IBrokerOrderVerdict):
commitActivateScheduled instead (a throw here is a terminal cancel, not an
activation).payload.attempt incremented, up to
CC_ORDER_CHECK_RETRY_ATTEMPTS CONSECUTIVE failures (a successful check resets the
streak). Exhaustion cancels the scheduled signal (reason "user") and signals a fatal
exit (exitEmitter). With the config at 0 any failure is terminal immediately (legacy).Manual wiring — EXCEPTION-BASED VARIANT: the throw-driven alternative to the imperative
commit-function wiring in onSignalSchedulePing (commitActivateScheduled /
commitCancelScheduled). Pick ONE per condition.
async onOrderScheduleCheck(payload: BrokerOrderCheckPayload) {
let order: Order | null;
try {
order = await this.exchange.getOrderById(payload.signalId);
} catch (networkError) {
// transient — tolerated up to CC_ORDER_CHECK_RETRY_ATTEMPTS consecutive failures
throw OrderTransientError.fromError(networkError);
}
if (!order) {
throw new OrderDeletedError(`Order ${payload.signalId} not found`); // confirmed gone -> cancel "user" at once
}
}
Called when a partial loss close is committed.
Called when a partial profit close is committed.
Called on every live tick while a pending (open) signal is monitored.
Purely informational mirror of the active-ping lifecycle — a throw here does NOT close the
position (unlike onOrderActiveCheck).
Manual wiring — EVENT-BASED (driving an open position from real exchange state)
Primary per-tick event-based hook for an open position (a throw does NOT close it — react to
the event and decide imperatively). This is where you reconcile the framework's VWAP view with
real fills: catch a SL that gapped through the level, or a TP that filled before VWAP
reached it. Poll your real order and translate its state into strategy state via the
commit-functions from src/function/strategy.ts (callable here because the ping is emitted inside
the strategy tick; effects are deferred to the next tick):
commitCreateTakeProfit(symbol, { id }) — real TP order filled (possibly before VWAP reached
the level) → force close, reason "take_profit".commitCreateStopLoss(symbol, { id }) — real SL order filled (e.g. price gapped through SL) →
force close, reason "stop_loss".commitClosePending(symbol, { id }) — no counterparty (no buyer/seller, liquidity gap) → close
now with reason "closed", instead of throwing.import { commitCreateTakeProfit, commitCreateStopLoss, commitClosePending } from "backtest-kit";
async onSignalActivePing(payload: BrokerActivePingPayload) {
const order = await this.exchange.getOrderById(payload.signalId);
if (order?.status === "filled" && order.kind === "take_profit") {
await commitCreateTakeProfit(payload.symbol, { id: order.id });
} else if (order?.status === "filled" && order.kind === "stop_loss") {
await commitCreateStopLoss(payload.symbol, { id: order.id });
} else if (order?.status === "no_counterparty") {
await commitClosePending(payload.symbol, { id: order.id });
}
}
Called on every live tick while the strategy is idle (no pending or scheduled signal). Purely informational.
MANUAL WIRING — EVENT-BASED: no signal is active, so there is nothing to commit; use it for idle heartbeats / housekeeping. A throw does not affect strategy state.
Called when a pending position is closed (reason: take_profit / stop_loss / time_expired / closed).
Manual wiring — EVENT-BASED (tearing down the position)
Outbound side — the framework has already removed the pending signal, so there is nothing to
commitClosePending here; instead flatten the real position and cancel leftover TP/SL orders by
payload.signalId, and record final PnL. payload.closeReason says which path closed it. If you
need to FORCE the close yourself (e.g. no counterparty), do it earlier in onSignalActivePing.
Called when a pending position is opened (new signal / immediate / scheduled or user activation). Purely informational lifecycle hook for the active phase of a signal.
Manual wiring — EVENT-BASED (placing entry + protective orders)
Fires ONCE at open — place the real entry confirmation and protective TP/SL orders (tag them with
payload.signalId). Drive the rest per-tick from onSignalActivePing. This hook does not gate
the position; for a true entry gate use onOrderSync (signal-open).
Called when a scheduled signal is cancelled before it ever activated (reason: timeout / price_reject / user).
Manual wiring — EVENT-BASED (tearing down the resting order)
Outbound side — the framework has already dropped the scheduled signal, so there is nothing to
commitCancelScheduled here; instead cancel the real resting order you placed in
onSignalScheduleOpen (look it up by payload.signalId). payload.reason tells you why.
Called when a new scheduled signal is created and starts waiting for priceOpen activation.
The scheduled -> active transition is reported via onOrderOpenCommit, not here.
Manual wiring — EVENT-BASED (placing the resting order)
Fires ONCE at creation — place the real resting/limit order (tag it with payload.signalId so
onSignalSchedulePing can poll it later). If it resolves immediately, promote it with
commitActivateScheduled(symbol, { id }); if rejected, drop it with
commitCancelScheduled(symbol, { id }). Use onSignalSchedulePing for ongoing polling.
import { commitActivateScheduled, commitCancelScheduled } from "backtest-kit";
async onSignalScheduleOpen(payload: BrokerScheduleOpenPayload) {
const order = await this.exchange.placeLimitOrder({
id: payload.signalId,
symbol: payload.symbol,
side: payload.position,
price: payload.priceOpen,
});
if (order.status === "filled") await commitActivateScheduled(payload.symbol, { id: order.id });
else if (order.status === "rejected") await commitCancelScheduled(payload.symbol, { id: order.id });
}
Called on every live tick while a scheduled signal is monitored (waiting for priceOpen activation). Purely informational.
Manual wiring — EVENT-BASED (driving the scheduled phase from real exchange state)
Per-tick event-based hook (a throw does NOT veto anything — react and decide imperatively).
Poll your real resting/limit order and translate it via the commit-functions from
src/function/strategy.ts (deferred to the next tick):
commitActivateScheduled(symbol, { id }) — resting order filled/resolved → activate now,
without waiting for VWAP to reach priceOpen (surfaces as onOrderOpenCommit next tick).commitCancelScheduled(symbol, { id }) — resting order cancelled/rejected externally → drop it.import { commitActivateScheduled, commitCancelScheduled } from "backtest-kit";
async onSignalSchedulePing(payload: BrokerSchedulePingPayload) {
const order = await this.exchange.getOrderById(payload.signalId);
if (order?.status === "filled" || order?.status === "resolved") {
await commitActivateScheduled(payload.symbol, { id: order.id });
} else if (order?.status === "cancelled" || order?.status === "rejected") {
await commitCancelScheduled(payload.symbol, { id: order.id });
}
}
Called when a trailing stop update is committed.
Called when a trailing take-profit update is committed.
Called once before first use. Connect to exchange, load credentials, etc.
RECOMMENDED: run an ORPHAN SWEEP here. After a fatal exit (transient-budget
exhaustion — see OrderTransientError) the exchange may hold artifacts the engine
has already forgotten: a FILLED entry order under clientOrderId = signalId
whose open was never confirmed (dropped signal), or a still-open position the
engine force-closed on its side. waitForInit runs before the first event of a
fresh process — the one moment to reconcile: list open orders/positions, match
clientOrderIds against the engine state (getStrategyStatus / getPendingSignal),
then either flatten the orphans on the exchange or re-adopt a live position via
commitCreateSignal so it comes back under TP/SL management. Skipping the sweep
risks trading a fresh signal ON TOP of an unmanaged orphan position.
TIMING CAVEATS for the re-adoption branch (commitCreateSignal):
waitForInit is LAZY — it is awaited before the FIRST proxied hook call, not at
enable(). If the strategy emits a signal on its very first tick, that first call
is the open gate with the retry slot already PRE-ARMED, and commitCreateSignal
throws "a rejected open is awaiting retry".getStrategyStatus first — adopt only when pendingSignalId,
retryOpenSignal, closedSignal and createdSignal are all clear; otherwise
restrict the sweep to flattening exchange-side orphans.
Broker adapter interface for live order execution.
Implement this interface to connect the framework to a real exchange or broker. All methods are called BEFORE the corresponding DI-core state mutation, so if any method throws, the internal state remains unchanged (transaction semantics).
In backtest mode all calls are silently skipped by BrokerAdapter — the adapter never receives backtest traffic.
Example