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 is monitored, BEFORE TP/SL/time evaluation.
Query the exchange by payload.signalId and THROW ONLY when the order is NOT FOUND by that id
— the framework will then close the position with closeReason "closed". Return normally to keep
monitoring.
CRITICAL: swallow transient/network errors (timeout, 5xx, rate limit, disconnect) — return normally instead of throwing, otherwise a connectivity blip would wrongly close an open position. Throw exclusively on a confirmed "order not found by id" result.
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 onOrderCheck(payload: BrokerSignalPendingPayload) {
let order: Order | null;
try {
order = await this.exchange.getOrderById(payload.signalId);
} catch (networkError) {
return; // transient — keep the position open, retry next tick
}
if (!order) {
throw new Error(`Order ${payload.signalId} not found`); // confirmed gone -> close "closed"
}
}
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 onOrderCheck).
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 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. This is the confirmed-close commit; like onSignalSync (signal-close) it
shares the gate semantics — a THROW means "the exchange did not close the position" and the
framework SKIPS the close, leaving the position open and retrying on the next tick. Return
normally to let the close proceed. Backtest short-circuits this (no live exchange), so the gate is
live-only.
This differs from onSignalPendingClose, which is the informational lifecycle hook that fires
AFTER the close is committed (and cannot veto it).
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 signal is being opened (position entry). Emitted via syncSubject BEFORE the framework mutates strategy state, so it is also the open gate.
MANUAL WIRING — EXCEPTION-BASED: place the real entry order here (tag the exchange order with
payload.signalId so later onOrderCheck / onSignalActivePing can find it). Like onSignalSync
(signal-open) it shares the gate semantics — a THROW means "the exchange did not fill the entry"
(e.g. limit order rejected) and the framework ROLLS BACK the open: the pending signal returns to
idle (a scheduled activation is cancelled) and is retried on the next tick. Return normally to let
the open proceed. Also the point where a scheduled signal's activation surfaces. Backtest
short-circuits this, so the gate is live-only.
This differs from onSignalPendingOpen, which is the informational lifecycle hook that fires
AFTER the open is committed (and cannot veto it).
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 onSignalSync (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 onSignalOpenCommit, 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 onSignalOpenCommit 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.
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