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.
waitForInit: () => Promise<void>
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.onOrderCloseCommit: (payload: BrokerOrderClosePayload) => Promise<void>
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).
onOrderOpenCommit: (payload: BrokerOrderOpenPayload) => Promise<void>
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).
onOrderActiveCheck: (payload: BrokerOrderCheckPayload) => Promise<void>
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.
onOrderScheduleCheck: (payload: BrokerOrderCheckPayload) => Promise<void>
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.
onSignalActivePing: (payload: BrokerActivePingPayload) => Promise<void>
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.onSignalSchedulePing: (payload: BrokerSchedulePingPayload) => Promise<void>
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.onSignalIdlePing: (payload: BrokerIdlePingPayload) => Promise<void>
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.
onSignalScheduleOpen: (payload: BrokerScheduleOpenPayload) => Promise<void>
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.
onSignalScheduleCancelled: (payload: BrokerScheduleCancelledPayload) => Promise<void>
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.
onSignalPendingOpen: (payload: BrokerPendingOpenPayload) => Promise<void>
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).
onSignalPendingClose: (payload: BrokerPendingClosePayload) => Promise<void>
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.
onPartialProfitCommit: (payload: BrokerPartialProfitPayload) => Promise<void>
Called when a partial profit close is committed.
onPartialLossCommit: (payload: BrokerPartialLossPayload) => Promise<void>
Called when a partial loss close is committed.
onTrailingStopCommit: (payload: BrokerTrailingStopPayload) => Promise<void>
Called when a trailing stop update is committed.
onTrailingTakeCommit: (payload: BrokerTrailingTakePayload) => Promise<void>
Called when a trailing take-profit update is committed.
onBreakevenCommit: (payload: BrokerBreakevenPayload) => Promise<void>
Called when a breakeven stop is committed (stop loss moved to entry price).
onAverageBuyCommit: (payload: BrokerAverageBuyPayload) => Promise<void>
Called when a DCA (average-buy) entry is committed.