Called when a new DCA entry is added to the active position.
Triggered explicitly after all validations pass, before strategyCoreService.averageBuy().
currentPrice is the market price at which the new averaging entry is placed.
cost is the dollar amount of the new DCA entry.
Default implementation: Logs average buy event.
Average buy details: symbol, currentPrice, cost, context, backtest
Called when the stop-loss is moved to breakeven (entry price).
Triggered explicitly after all validations pass, before strategyCoreService.breakeven().
newStopLossPrice equals effectivePriceOpen — the position's effective entry price.
newTakeProfitPrice is unchanged by breakeven.
Default implementation: Logs breakeven event.
Breakeven details: symbol, currentPrice, newStopLossPrice, newTakeProfitPrice, context, backtest
Called on every live tick while a pending signal (open position) is monitored, BEFORE TP/SL/time evaluation.
Override to query the exchange for the order by payload.signalId. The default
implementation logs and returns normally, which keeps the position under normal TP/SL
monitoring. Throw semantics: OrderDeletedError = confirmed "order not found by id"
(filled, cancelled, or liquidated externally) — the framework closes the position with
closeReason "closed" at once; a plain Error / OrderTransientError (timeout, 5xx, rate
limit, disconnect) is TOLERATED as a transient failure up to
CC_ORDER_CHECK_RETRY_ATTEMPTS consecutive times (payload.attempt increments, a
successful check resets it) before the framework acts terminally — a connectivity blip
no longer closes a live position on the spot.
Manual wiring — EXCEPTION-BASED VARIANT: the throw-driven alternative to the imperative
commit-function wiring in onSignalActivePing. See IBroker.onOrderActiveCheck for the
full comparison and example.
Pending ping details: symbol, signalId, position, prices, pnl, attempt, context, backtest
Called when a position is being closed (SL/TP hit or manual close).
Triggered automatically via syncSubject when a pending signal is closed. Use to place the exit order and record final PnL.
Default implementation: Logs signal-close event.
Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state.
Throw semantics: a plain Error / OrderTransientError SKIPS the close — the position
stays open and the close retries next tick (payload.attempt increments) up to
CC_ORDER_CLOSE_RETRY_ATTEMPTS, then the engine force-closes its state with the
original closeReason; OrderRejectedError force-closes terminally at once. Return
normally to let it close. Live-only (backtest short-circuits). See
IBroker.onOrderCloseCommit for the full semantics.
Signal close details: symbol, cost, position, currentPrice, pnl, totalEntries, totalPartials, attempt, context, backtest
async onOrderCloseCommit(payload: BrokerOrderClosePayload) {
super.onOrderCloseCommit(payload); // Keep parent logging
const res = await this.exchange.closePosition(payload.symbol);
if (res.noCounterparty) {
throw new OrderRejectedError(`No buyer for ${payload.symbol}`); // terminal -> force-close at once
}
if (!res.filled) {
throw new OrderTransientError(`Exit not filled for ${payload.symbol}`); // keep position open, bounded retry
}
await this.db.recordTrade({ symbol: payload.symbol, pnl: payload.pnl });
}
Called when a position is being opened (signal activated).
Triggered automatically via syncSubject when a scheduled signal's priceOpen is hit. Use to place the actual entry order on the exchange.
Default implementation: Logs signal-open event.
Manual wiring — EXCEPTION-BASED GATE: emitted BEFORE the framework mutates state.
Throw semantics: a plain Error / OrderTransientError rolls back the open and retries
identity-stably (same signalId, payload.attempt increments, pre-armed so a crash
mid-attempt still counts) up to CC_ORDER_OPEN_RETRY_ATTEMPTS; OrderRejectedError
drops the open terminally without arming the retry. Return normally to let it open.
Tag orders with clientOrderId = payload.signalId and RECONCILE at attempt > 0
(query the prior order BEFORE re-sending — Binance's duplicate guard does not cover
instantly-filled orders). Live-only (backtest short-circuits). See
IBroker.onOrderOpenCommit for the full semantics.
Signal open details: symbol, cost, position, priceOpen, priceTakeProfit, priceStopLoss, attempt, context, backtest
async onOrderOpenCommit(payload: BrokerOrderOpenPayload) {
super.onOrderOpenCommit(payload); // Keep parent logging
if (payload.attempt > 0) {
// A prior POST may have reached the exchange — reconcile BEFORE re-sending
const prior = await this.exchange.getOrderByClientId(payload.signalId);
if (prior?.filled) return; // already filled -> confirm the open
}
const order = await this.exchange.placeMarketOrder({
clientOrderId: payload.signalId, // idempotency key across retries
symbol: payload.symbol,
side: payload.position === "long" ? "BUY" : "SELL",
quantity: payload.cost / payload.priceOpen,
});
if (order.rejectedByExchange) {
throw new OrderRejectedError(`Entry refused for ${payload.symbol}`); // terminal, no retry
}
if (!order.filled) {
throw new OrderTransientError(`Entry not filled for ${payload.symbol}`); // bounded identity-stable retry
}
}
Called on every live tick while a scheduled signal (resting entry order) is monitored, BEFORE timeout/price-activation evaluation.
Override to query the exchange for the resting order by payload.signalId. The default
implementation logs and returns normally. Throw semantics: OrderDeletedError =
confirmed "order not found by id" — the framework cancels the scheduled signal with
reason "user" at once (a FILLED resting order must be confirmed via
commitActivateScheduled, not by throwing); a plain Error / OrderTransientError
(timeout, 5xx, rate limit, disconnect) is TOLERATED as a transient failure up to
CC_ORDER_CHECK_RETRY_ATTEMPTS consecutive times (payload.attempt increments, a
successful check resets it) before the framework acts terminally.
Manual wiring — EXCEPTION-BASED VARIANT: the throw-driven alternative to the imperative
commit-function wiring in onSignalSchedulePing. See IBroker.onOrderScheduleCheck for
the full comparison and example.
Pending ping details: symbol, signalId, position, prices, pnl, attempt, context, backtest
Called when a partial close at loss is executed.
Triggered explicitly from strategy.ts / Live.ts / Backtest.ts after all validations pass,
before strategyCoreService.partialLoss(). If this method throws, the DI mutation is skipped.
Use to partially close the position on the exchange at the loss level.
Default implementation: Logs partial loss event.
Partial loss details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest
Called when a partial close at profit is executed.
Triggered explicitly from strategy.ts / Live.ts / Backtest.ts after all validations pass,
before strategyCoreService.partialProfit(). If this method throws, the DI mutation is skipped.
Use to partially close the position on the exchange at the profit level.
Default implementation: Logs partial profit event.
Partial profit details: symbol, percentToClose, cost (dollar value), currentPrice, context, backtest
Called on every live tick while a pending (open) signal is monitored.
Purely informational mirror of the active-ping lifecycle — unlike onOrderActiveCheck, a throw here
does NOT close the position. Override to mirror live monitoring state into your own systems.
The default implementation logs.
Manual wiring — EVENT-BASED: this is the primary per-tick hook to drive an open position from real exchange
state (commitCreateTakeProfit / commitCreateStopLoss / commitClosePending). See the
IBroker.onSignalActivePing contract docs for the full guidance and example.
Active ping details: symbol, signalId, position, prices, pnl, context, backtest
Called on every live tick while the strategy is idle (no pending or scheduled signal).
Purely informational. Override to track idle heartbeats. The default logs.
Idle ping details: symbol, currentPrice, context, backtest
Called when a pending position is closed (take_profit / stop_loss / time_expired / closed).
Informational lifecycle hook. Override to mirror the close into your own systems. The default logs.
Manual wiring — EVENT-BASED (outbound): the strategy already removed the pending signal — flatten the real
position and cancel leftover TP/SL orders by payload.signalId. See
IBroker.onSignalPendingClose.
Pending close details: symbol, signalId, position, prices, closeReason, context, backtest
Called when a pending position is opened (new signal / immediate / scheduled or user activation).
Informational lifecycle hook. Override to mirror the open into your own systems. The default logs.
Manual wiring — EVENT-BASED: fires ONCE at open — place entry + protective TP/SL orders (tag with
payload.signalId), then drive per-tick from onSignalActivePing. See
IBroker.onSignalPendingOpen.
Pending open details: symbol, signalId, position, prices, context, backtest
Called when a scheduled signal is cancelled before activation (timeout / price_reject / user).
Override to cancel the resting/limit order on the exchange. The default logs.
Manual wiring — EVENT-BASED (outbound): the strategy already dropped the scheduled signal — cancel the matching
exchange order by payload.signalId. See IBroker.onSignalScheduleCancelled.
Scheduled cancel details: symbol, signalId, position, prices, reason, context, backtest
Called when a new scheduled signal is created and starts waiting for priceOpen activation.
The scheduled -> active transition is reported via onOrderOpenCommit, not here. Override to
place a resting/limit order on the exchange. The default logs.
Manual wiring — EVENT-BASED: fires ONCE at creation — place the real resting order (tag it with
payload.signalId) and optionally commitActivateScheduled / commitCancelScheduled. See
IBroker.onSignalScheduleOpen for full guidance and example.
Scheduled open details: symbol, signalId, position, prices, context, backtest
Called on every live tick while a scheduled signal is monitored (waiting for priceOpen).
Purely informational. Override to mirror scheduled-monitoring state. The default logs.
Manual wiring — EVENT-BASED: per-tick hook to drive a scheduled (resting) order from real exchange state
(commitActivateScheduled / commitCancelScheduled). See IBroker.onSignalSchedulePing
for full guidance and example.
Schedule ping details: symbol, signalId, position, prices, context, backtest
Called when the trailing stop-loss level is updated.
Triggered explicitly after all validations pass, before strategyCoreService.trailingStop().
newStopLossPrice is the absolute SL price — use it to update the exchange order directly.
Default implementation: Logs trailing stop event.
Trailing stop details: symbol, percentShift, currentPrice, newStopLossPrice, context, backtest
Called when the trailing take-profit level is updated.
Triggered explicitly after all validations pass, before strategyCoreService.trailingTake().
newTakeProfitPrice is the absolute TP price — use it to update the exchange order directly.
Default implementation: Logs trailing take event.
Trailing take details: symbol, percentShift, currentPrice, newTakeProfitPrice, context, backtest
Performs async initialization before the broker starts receiving events.
Called once by BrokerProxy via waitForInit() (singleshot) before the first event.
Override to establish exchange connections, authenticate API clients, load configuration.
RECOMMENDED: run an ORPHAN SWEEP here (see IBroker.waitForInit). A prior
run that died on transient-budget exhaustion may have left a filled-but-unconfirmed
entry order (the engine dropped the signal) or a real position the engine already
force-closed. Reconcile before the first tick: cancel/flatten orphans, or re-adopt
a live position via commitCreateSignal to bring it back under TP/SL management —
otherwise a fresh signal may open ON TOP of an unmanaged orphan.
TIMING CAVEATS for re-adoption: waitForInit is LAZY (awaited before the first
proxied hook call, not at enable()) — when the strategy trades on its first tick,
the sweep runs INSIDE the open gate with the retry slot already pre-armed, and
commitCreateSignal throws "a rejected open is awaiting retry". An idle tick is
no guarantee either: rejected opens and rejected user-close drains also emit idle
pings while state is live. Check getStrategyStatus and adopt ONLY when
pendingSignalId / retryOpenSignal / closedSignal / createdSignal are all
clear; otherwise limit the sweep to exchange-side cleanup.
Default implementation: Logs initialization event.
async waitForInit() {
super.waitForInit(); // Keep parent logging
this.exchange = new ExchangeClient(process.env.API_KEY);
await this.exchange.authenticate();
// Orphan sweep: the engine forgets dropped signals — the exchange does not.
for (const order of await this.exchange.getOpenOrders()) {
await this.exchange.cancelOrder(order.id); // resting entries of dropped signals
}
const status = await getStrategyStatus("BTCUSDT");
const engineClean = !status.pendingSignalId && !status.retryOpenSignal
&& !status.closedSignal && !status.createdSignal;
for (const pos of await this.exchange.getPositions()) {
if (engineClean) {
// re-adopt via commitCreateSignal (id = pos.clientOrderId) — back under TP/SL
} else {
await this.exchange.flatten(pos.symbol); // engine busy — exchange-side cleanup only
}
}
}
Base class for custom broker adapter implementations.
Provides default no-op implementations for all IBroker methods that log events. Extend this class to implement a real exchange adapter for:
Key features:
makeExtendableapplied for correct subclass instantiationLifecycle:
waitForInit()called once for async initialization (e.g. exchange login)waitForInitteardown or externallyEvent flow (called only in live mode, skipped in backtest):
onOrderOpenCommit— new position openedonOrderCloseCommit— position closed (SL/TP hit or manual close)onPartialProfitCommit— partial close at profit executedonPartialLossCommit— partial close at loss executedonTrailingStopCommit— trailing stop-loss updatedonTrailingTakeCommit— trailing take-profit updatedonBreakevenCommit— stop-loss moved to entry priceonAverageBuyCommit— new DCA entry added to positionExample
Example