Interface IBroker

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.

class MyBroker implements IBroker {
async waitForInit() {
await this.exchange.connect();
}
async onSignalOpenCommit(payload) {
await this.exchange.placeOrder({ symbol: payload.symbol, side: payload.position });
}
// ... other methods
}

Broker.useBrokerAdapter(MyBroker);
interface IBroker {
    onAverageBuyCommit(payload: BrokerAverageBuyPayload): Promise<void>;
    onBreakevenCommit(payload: BrokerBreakevenPayload): Promise<void>;
    onOrderCheck(payload: BrokerSignalPendingPayload): Promise<void>;
    onPartialLossCommit(payload: BrokerPartialLossPayload): Promise<void>;
    onPartialProfitCommit(payload: BrokerPartialProfitPayload): Promise<void>;
    onSignalActivePing(payload: BrokerActivePingPayload): Promise<void>;
    onSignalCloseCommit(payload: BrokerSignalClosePayload): Promise<void>;
    onSignalIdlePing(payload: BrokerIdlePingPayload): Promise<void>;
    onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void>;
    onSignalPendingClose(payload: BrokerPendingClosePayload): Promise<void>;
    onSignalPendingOpen(payload: BrokerPendingOpenPayload): Promise<void>;
    onSignalScheduleCancelled(
        payload: BrokerScheduleCancelledPayload,
    ): Promise<void>;
    onSignalScheduleOpen(payload: BrokerScheduleOpenPayload): Promise<void>;
    onSignalSchedulePing(payload: BrokerSchedulePingPayload): Promise<void>;
    onTrailingStopCommit(payload: BrokerTrailingStopPayload): Promise<void>;
    onTrailingTakeCommit(payload: BrokerTrailingTakePayload): Promise<void>;
    waitForInit(): Promise<void>;
}

Implemented by

Methods

  • 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:

    • Exception-based (here): THROW → framework closes the position with closeReason "closed". One binary gate, no reason distinction. Good when "order gone" is the only condition you handle.
    • Imperative (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.

    Parameters

    Returns Promise<void>

    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 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.

    Parameters

    Returns Promise<void>

    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).

    Parameters

    Returns 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.

    Parameters

    Returns Promise<void>

  • 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).

    Parameters

    Returns 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.

    Parameters

    Returns Promise<void>

    async onSignalPendingClose(payload: BrokerPendingClosePayload) {
    await this.exchange.flatten(payload.symbol);
    await this.exchange.cancelProtectiveOrders(payload.signalId);
    }
  • 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).

    Parameters

    Returns 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.

    Returns Promise<void>

    async onSignalScheduleCancelled(payload: BrokerScheduleCancelledPayload) {
    await this.exchange.cancelOrderById(payload.signalId);
    }
  • 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.

    Parameters

    Returns Promise<void>

    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.

    Parameters

    Returns Promise<void>

    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 once before first use. Connect to exchange, load credentials, etc.

    Returns Promise<void>