The broker adapter bridges Backtest Kit’s signal engine to a live exchange (Binance, Bybit, or any CCXT-compatible venue). Without an adapter, backtest and live modes both use simulated fills; once you wire one in, every state-changing commit — opening a position, closing it, adjusting a trailing stop — calls your exchange implementation before any internal state mutation. If the exchange rejects the order, times out, or throws for any reason, the mutation is skipped and the framework retries on the next tick. This gives you transactional semantics without any extra bookkeeping.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-kit/llms.txt
Use this file to discover all available pages before exploring further.
Registration and activation
Register an adapter class withBroker.useBrokerAdapter(), then call Broker.enable() once at startup to subscribe the adapter to the syncSubject lifecycle stream. Signal-open and signal-close events are routed automatically; all other commit methods (partialProfit, trailingStop, breakeven, averageBuy) are called explicitly before the corresponding strategyCoreService mutation.
import { Broker } from "backtest-kit";
Broker.useBrokerAdapter(MyBrokerAdapter); // register
Broker.enable(); // wire syncSubject
Broker.enable() must be called exactly once at startup, after useBrokerAdapter(). Calling it before registration throws immediately. Calling it more than once is safe — it is wrapped in a singleshot guard.The IBroker interface
Your adapter class implements Partial<IBroker> — only override the methods you need. The BrokerBase class ships as a convenience base with default no-op implementations and logging for every method.
| Method | Called when |
|---|---|
waitForInit() | Once before first use — authenticate, load markets |
onSignalOpenCommit(payload) | New position activated (limit order filled) |
onSignalCloseCommit(payload) | Position fully closed (TP/SL or manual) |
onPartialProfitCommit(payload) | Partial close at profit executed |
onPartialLossCommit(payload) | Partial close at loss executed |
onTrailingStopCommit(payload) | Trailing stop-loss updated |
onTrailingTakeCommit(payload) | Trailing take-profit updated |
onBreakevenCommit(payload) | Stop-loss moved to entry (breakeven) |
onAverageBuyCommit(payload) | New DCA entry added to position |
Key payload fields — BrokerSignalOpenPayload
Trading pair symbol, e.g.
"BTCUSDT".Dollar cost of the entry (defaults to
CC_POSITION_ENTRY_COST, usually $100).Activation price — the price at which the signal became active.
Original take-profit price from the signal.
Original stop-loss price from the signal.
Trade direction.
true during a backtest run — adapter should skip real exchange calls. The BrokerAdapter facade skips all commit methods automatically when backtest === true.Transactional rollback
Every commit method fires before the DI-core state mutation. The execution contract is:- Framework calls
adapter.onXxxCommit(payload) - If the method resolves → state mutation proceeds
- If the method throws (network error, order rejection, timeout) → mutation is skipped, no state change
- On the next tick, the framework retries the whole operation
createLimitOrderAndWait helper in the examples below) handles partial fills by rolling them back before throwing, ensuring the exchange is always in a clean state for the retry.
Full implementation examples
- Spot (Binance)
- Futures (Binance)
Full Spot Adapter
Full Spot Adapter
The Spot adapter manages a Binance spot account. It uses
stop_loss_limit orders with a small slippage buffer on the limit leg to avoid non-fills on gap-downs, and polls open orders after every cancel to avoid reading stale exchange state.import ccxt from "ccxt";
import { singleshot, sleep } from "functools-kit";
import {
Broker,
IBroker,
BrokerSignalOpenPayload,
BrokerSignalClosePayload,
BrokerPartialProfitPayload,
BrokerPartialLossPayload,
BrokerTrailingStopPayload,
BrokerTrailingTakePayload,
BrokerBreakevenPayload,
BrokerAverageBuyPayload,
} from "backtest-kit";
const FILL_POLL_INTERVAL_MS = 10_000;
const FILL_POLL_ATTEMPTS = 10;
const CANCEL_SETTLE_MS = 2_000;
const STOP_LIMIT_SLIPPAGE = 0.995;
const getSpotExchange = singleshot(async () => {
const exchange = new ccxt.binance({
apiKey: process.env.BINANCE_API_KEY,
secret: process.env.BINANCE_API_SECRET,
options: { defaultType: "spot", adjustForTimeDifference: true, recvWindow: 60000 },
enableRateLimit: true,
});
await exchange.loadMarkets();
return exchange;
});
function truncateQty(exchange: ccxt.binance, symbol: string, qty: number): number {
return parseFloat(exchange.amountToPrecision(symbol, qty, exchange.TRUNCATE));
}
async function fetchFreeQty(exchange: ccxt.binance, symbol: string): Promise<number> {
const balance = await exchange.fetchBalance();
const base = exchange.markets[symbol].base;
return parseFloat(String(balance?.free?.[base] ?? 0));
}
async function cancelAllOrders(exchange: ccxt.binance, orders: ccxt.Order[], symbol: string): Promise<void> {
await Promise.allSettled(orders.map((o) => exchange.cancelOrder(o.id, symbol)));
}
async function createStopLossOrder(exchange: ccxt.binance, symbol: string, qty: number, stopPrice: number): Promise<void> {
const limitPrice = parseFloat(exchange.priceToPrecision(symbol, stopPrice * STOP_LIMIT_SLIPPAGE));
await exchange.createOrder(symbol, "stop_loss_limit", "sell", qty, limitPrice, { stopPrice });
}
async function createLimitOrderAndWait(
exchange: ccxt.binance,
symbol: string,
side: "buy" | "sell",
qty: number,
price: number,
restore?: { tpPrice: number; slPrice: number }
): Promise<void> {
const order = await exchange.createOrder(symbol, "limit", side, qty, price);
for (let i = 0; i < FILL_POLL_ATTEMPTS; i++) {
await sleep(FILL_POLL_INTERVAL_MS);
const status = await exchange.fetchOrder(order.id, symbol);
if (status.status === "closed") return;
}
await exchange.cancelOrder(order.id, symbol);
await sleep(CANCEL_SETTLE_MS);
const final = await exchange.fetchOrder(order.id, symbol);
const filledQty = final.filled ?? 0;
if (filledQty > 0) {
const rollbackSide = side === "buy" ? "sell" : "buy";
await exchange.createOrder(symbol, "market", rollbackSide, filledQty);
}
if (restore) {
const remainingQty = truncateQty(exchange, symbol, await fetchFreeQty(exchange, symbol));
if (remainingQty > 0) {
await exchange.createOrder(symbol, "limit", "sell", remainingQty, restore.tpPrice);
await createStopLossOrder(exchange, symbol, remainingQty, restore.slPrice);
}
}
throw new Error(`Limit order ${order.id} not filled in time — rolled back, retrying`);
}
Broker.useBrokerAdapter(
class implements IBroker {
async waitForInit(): Promise<void> {
await getSpotExchange();
}
async onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void> {
const { symbol, cost, priceOpen, priceTakeProfit, priceStopLoss, position } = payload;
if (position === "short") throw new Error("Spot does not support short selling");
const exchange = await getSpotExchange();
const qty = truncateQty(exchange, symbol, cost / priceOpen);
if (qty <= 0) throw new Error(`Computed qty is zero — cost=${cost}, price=${priceOpen}`);
const openPrice = parseFloat(exchange.priceToPrecision(symbol, priceOpen));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
await createLimitOrderAndWait(exchange, symbol, "buy", qty, openPrice);
try {
await exchange.createOrder(symbol, "limit", "sell", qty, tpPrice);
await createStopLossOrder(exchange, symbol, qty, slPrice);
} catch (err) {
await exchange.createOrder(symbol, "market", "sell", qty);
throw err;
}
}
async onSignalCloseCommit(payload: BrokerSignalClosePayload): Promise<void> {
const { symbol, currentPrice, priceTakeProfit, priceStopLoss } = payload;
const exchange = await getSpotExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
const qty = truncateQty(exchange, symbol, await fetchFreeQty(exchange, symbol));
if (qty === 0) return;
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
await createLimitOrderAndWait(exchange, symbol, "sell", qty, closePrice, { tpPrice, slPrice });
}
async onPartialProfitCommit(payload: BrokerPartialProfitPayload): Promise<void> {
const { symbol, percentToClose, currentPrice, priceTakeProfit, priceStopLoss } = payload;
const exchange = await getSpotExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
const totalQty = await fetchFreeQty(exchange, symbol);
if (totalQty === 0) throw new Error(`PartialProfit skipped: no open position for ${symbol}`);
const qty = truncateQty(exchange, symbol, totalQty * (percentToClose / 100));
const remainingQty = truncateQty(exchange, symbol, totalQty - qty);
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
await createLimitOrderAndWait(exchange, symbol, "sell", qty, closePrice, { tpPrice, slPrice });
if (remainingQty > 0) {
try {
await exchange.createOrder(symbol, "limit", "sell", remainingQty, tpPrice);
await createStopLossOrder(exchange, symbol, remainingQty, slPrice);
} catch (err) {
await exchange.createOrder(symbol, "market", "sell", remainingQty);
throw err;
}
}
}
async onPartialLossCommit(payload: BrokerPartialLossPayload): Promise<void> {
// identical pattern to onPartialProfitCommit — see source for full implementation
const { symbol } = payload;
const exchange = await getSpotExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
}
async onTrailingStopCommit(payload: BrokerTrailingStopPayload): Promise<void> {
const { symbol, newStopLossPrice } = payload;
const exchange = await getSpotExchange();
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) =>
o.side === "sell" && ["stop_loss_limit", "stop", "STOP_LOSS_LIMIT"].includes(o.type ?? "")
) ?? null;
if (slOrder) { await exchange.cancelOrder(slOrder.id, symbol); await sleep(CANCEL_SETTLE_MS); }
const qty = truncateQty(exchange, symbol, await fetchFreeQty(exchange, symbol));
if (qty === 0) throw new Error(`TrailingStop skipped: no open position for ${symbol}`);
const slPrice = parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice));
await createStopLossOrder(exchange, symbol, qty, slPrice);
}
async onTrailingTakeCommit(payload: BrokerTrailingTakePayload): Promise<void> {
const { symbol, newTakeProfitPrice } = payload;
const exchange = await getSpotExchange();
const orders = await exchange.fetchOpenOrders(symbol);
const tpOrder = orders.find((o) => o.side === "sell" && ["limit", "LIMIT"].includes(o.type ?? "")) ?? null;
if (tpOrder) { await exchange.cancelOrder(tpOrder.id, symbol); await sleep(CANCEL_SETTLE_MS); }
const qty = truncateQty(exchange, symbol, await fetchFreeQty(exchange, symbol));
if (qty === 0) throw new Error(`TrailingTake skipped: no open position for ${symbol}`);
await exchange.createOrder(symbol, "limit", "sell", qty, parseFloat(exchange.priceToPrecision(symbol, newTakeProfitPrice)));
}
async onBreakevenCommit(payload: BrokerBreakevenPayload): Promise<void> {
const { symbol, newStopLossPrice } = payload;
const exchange = await getSpotExchange();
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) =>
o.side === "sell" && ["stop_loss_limit", "stop", "STOP_LOSS_LIMIT"].includes(o.type ?? "")
) ?? null;
if (slOrder) { await exchange.cancelOrder(slOrder.id, symbol); await sleep(CANCEL_SETTLE_MS); }
const qty = truncateQty(exchange, symbol, await fetchFreeQty(exchange, symbol));
if (qty === 0) throw new Error(`Breakeven skipped: no open position for ${symbol}`);
await createStopLossOrder(exchange, symbol, qty, parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice)));
}
async onAverageBuyCommit(payload: BrokerAverageBuyPayload): Promise<void> {
const { symbol, currentPrice, cost, priceTakeProfit, priceStopLoss } = payload;
const exchange = await getSpotExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
const existing = await fetchFreeQty(exchange, symbol);
const minNotional = exchange.markets[symbol].limits?.cost?.min ?? 1;
if (existing * currentPrice < minNotional)
throw new Error(`AverageBuy skipped: no open position for ${symbol}`);
const qty = truncateQty(exchange, symbol, cost / currentPrice);
if (qty <= 0) throw new Error(`Computed qty is zero — cost=${cost}, price=${currentPrice}`);
const entryPrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
await createLimitOrderAndWait(exchange, symbol, "buy", qty, entryPrice, { tpPrice, slPrice });
const totalQty = truncateQty(exchange, symbol, await fetchFreeQty(exchange, symbol));
try {
await exchange.createOrder(symbol, "limit", "sell", totalQty, tpPrice);
await createStopLossOrder(exchange, symbol, totalQty, slPrice);
} catch (err) {
await exchange.createOrder(symbol, "market", "sell", totalQty);
throw err;
}
}
}
);
Broker.enable();
Full Futures Adapter
Full Futures Adapter
The Futures adapter targets Binance USD-M futures. It sets leverage on every open, uses
stop_market orders (no limit leg needed), and passes positionSide so hedge-mode accounts route orders correctly.import ccxt from "ccxt";
import { singleshot, sleep } from "functools-kit";
import {
Broker,
IBroker,
BrokerSignalOpenPayload,
BrokerSignalClosePayload,
BrokerPartialProfitPayload,
BrokerPartialLossPayload,
BrokerTrailingStopPayload,
BrokerTrailingTakePayload,
BrokerBreakevenPayload,
BrokerAverageBuyPayload,
} from "backtest-kit";
const FILL_POLL_INTERVAL_MS = 10_000;
const FILL_POLL_ATTEMPTS = 10;
const CANCEL_SETTLE_MS = 2_000;
const FUTURES_LEVERAGE = 3;
const getFuturesExchange = singleshot(async () => {
const exchange = new ccxt.binance({
apiKey: process.env.BINANCE_API_KEY,
secret: process.env.BINANCE_API_SECRET,
options: { defaultType: "future", adjustForTimeDifference: true, recvWindow: 60000 },
enableRateLimit: true,
});
await exchange.loadMarkets();
return exchange;
});
function truncateQty(exchange: ccxt.binance, symbol: string, qty: number): number {
return parseFloat(exchange.amountToPrecision(symbol, qty, exchange.TRUNCATE));
}
async function fetchContractsQty(exchange: ccxt.binance, symbol: string, side: "long" | "short"): Promise<number> {
const positions = await exchange.fetchPositions([symbol]);
const pos = positions.find((p) => p.symbol === symbol && p.side === side)
?? positions.find((p) => p.symbol === symbol)
?? null;
return Math.abs(parseFloat(String(pos?.contracts ?? 0)));
}
async function cancelAllOrders(exchange: ccxt.binance, orders: ccxt.Order[], symbol: string): Promise<void> {
await Promise.allSettled(orders.map((o) => exchange.cancelOrder(o.id, symbol)));
}
function toPositionSide(position: "long" | "short"): "LONG" | "SHORT" {
return position === "long" ? "LONG" : "SHORT";
}
async function createLimitOrderAndWait(
exchange: ccxt.binance,
symbol: string,
side: "buy" | "sell",
qty: number,
price: number,
params: Record<string, unknown> = {},
restore?: { exitSide: "buy" | "sell"; tpPrice: number; slPrice: number; positionSide: "long" | "short" }
): Promise<void> {
const order = await exchange.createOrder(symbol, "limit", side, qty, price, params);
for (let i = 0; i < FILL_POLL_ATTEMPTS; i++) {
await sleep(FILL_POLL_INTERVAL_MS);
const status = await exchange.fetchOrder(order.id, symbol);
if (status.status === "closed") return;
}
await exchange.cancelOrder(order.id, symbol);
await sleep(CANCEL_SETTLE_MS);
const final = await exchange.fetchOrder(order.id, symbol);
const filledQty = final.filled ?? 0;
if (filledQty > 0) {
const rollbackSide = side === "buy" ? "sell" : "buy";
await exchange.createOrder(symbol, "market", rollbackSide, filledQty, undefined, {
reduceOnly: true,
...(restore ? { positionSide: toPositionSide(restore.positionSide) } : {}),
});
}
if (restore) {
const remainingQty = truncateQty(exchange, symbol, await fetchContractsQty(exchange, symbol, restore.positionSide));
if (remainingQty > 0) {
await exchange.createOrder(symbol, "limit", restore.exitSide, remainingQty, restore.tpPrice, { reduceOnly: true });
await exchange.createOrder(symbol, "stop_market", restore.exitSide, remainingQty, undefined, { stopPrice: restore.slPrice, reduceOnly: true });
}
}
throw new Error(`Limit order ${order.id} not filled in time — rolled back, retrying`);
}
Broker.useBrokerAdapter(
class implements IBroker {
async waitForInit(): Promise<void> {
await getFuturesExchange();
}
async onSignalOpenCommit(payload: BrokerSignalOpenPayload): Promise<void> {
const { symbol, cost, priceOpen, priceTakeProfit, priceStopLoss, position } = payload;
const exchange = await getFuturesExchange();
await exchange.setLeverage(FUTURES_LEVERAGE, symbol);
const qty = truncateQty(exchange, symbol, cost / priceOpen);
if (qty <= 0) throw new Error(`Computed qty is zero — cost=${cost}, price=${priceOpen}`);
const openPrice = parseFloat(exchange.priceToPrecision(symbol, priceOpen));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
const entrySide = position === "long" ? "buy" : "sell";
const exitSide = position === "long" ? "sell" : "buy";
const positionSide = toPositionSide(position);
await createLimitOrderAndWait(exchange, symbol, entrySide, qty, openPrice, { positionSide });
try {
await exchange.createOrder(symbol, "limit", exitSide, qty, tpPrice, { reduceOnly: true, positionSide });
await exchange.createOrder(symbol, "stop_market", exitSide, qty, undefined, { stopPrice: slPrice, reduceOnly: true, positionSide });
} catch (err) {
await exchange.createOrder(symbol, "market", exitSide, qty, undefined, { reduceOnly: true, positionSide });
throw err;
}
}
async onSignalCloseCommit(payload: BrokerSignalClosePayload): Promise<void> {
const { symbol, position, currentPrice, priceTakeProfit, priceStopLoss } = payload;
const exchange = await getFuturesExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
const qty = truncateQty(exchange, symbol, await fetchContractsQty(exchange, symbol, position));
const exitSide = position === "long" ? "sell" : "buy";
if (qty === 0) throw new Error(`SignalClose skipped: no open position for ${symbol}`);
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
await createLimitOrderAndWait(
exchange, symbol, exitSide, qty, closePrice,
{ reduceOnly: true },
{ exitSide, tpPrice, slPrice, positionSide: position }
);
}
async onPartialProfitCommit(payload: BrokerPartialProfitPayload): Promise<void> {
const { symbol, percentToClose, currentPrice, position, priceTakeProfit, priceStopLoss } = payload;
const exchange = await getFuturesExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
const totalQty = await fetchContractsQty(exchange, symbol, position);
if (totalQty === 0) throw new Error(`PartialProfit skipped: no open position for ${symbol}`);
const qty = truncateQty(exchange, symbol, totalQty * (percentToClose / 100));
const remainingQty = truncateQty(exchange, symbol, totalQty - qty);
const closePrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
const exitSide = position === "long" ? "sell" : "buy";
const positionSide = toPositionSide(position);
await createLimitOrderAndWait(exchange, symbol, exitSide, qty, closePrice, { reduceOnly: true }, { exitSide, tpPrice, slPrice, positionSide: position });
if (remainingQty > 0) {
try {
await exchange.createOrder(symbol, "limit", exitSide, remainingQty, tpPrice, { reduceOnly: true, positionSide });
await exchange.createOrder(symbol, "stop_market", exitSide, remainingQty, undefined, { stopPrice: slPrice, reduceOnly: true, positionSide });
} catch (err) {
await exchange.createOrder(symbol, "market", exitSide, remainingQty, undefined, { reduceOnly: true, positionSide });
throw err;
}
}
}
async onPartialLossCommit(payload: BrokerPartialLossPayload): Promise<void> {
// identical structure to onPartialProfitCommit — see source
const { symbol } = payload;
const exchange = await getFuturesExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
}
async onTrailingStopCommit(payload: BrokerTrailingStopPayload): Promise<void> {
const { symbol, newStopLossPrice, position } = payload;
const exchange = await getFuturesExchange();
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) => !!o.reduceOnly && ["stop_market", "stop", "STOP_MARKET"].includes(o.type ?? "")) ?? null;
if (slOrder) { await exchange.cancelOrder(slOrder.id, symbol); await sleep(CANCEL_SETTLE_MS); }
const qty = truncateQty(exchange, symbol, await fetchContractsQty(exchange, symbol, position));
const exitSide = position === "long" ? "sell" : "buy";
if (qty === 0) throw new Error(`TrailingStop skipped: no open position for ${symbol}`);
const slPrice = parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice));
const positionSide = toPositionSide(position);
await exchange.createOrder(symbol, "stop_market", exitSide, qty, undefined, { stopPrice: slPrice, reduceOnly: true, positionSide });
}
async onTrailingTakeCommit(payload: BrokerTrailingTakePayload): Promise<void> {
const { symbol, newTakeProfitPrice, position } = payload;
const exchange = await getFuturesExchange();
const orders = await exchange.fetchOpenOrders(symbol);
const tpOrder = orders.find((o) => !!o.reduceOnly && ["limit", "LIMIT"].includes(o.type ?? "")) ?? null;
if (tpOrder) { await exchange.cancelOrder(tpOrder.id, symbol); await sleep(CANCEL_SETTLE_MS); }
const qty = truncateQty(exchange, symbol, await fetchContractsQty(exchange, symbol, position));
const exitSide = position === "long" ? "sell" : "buy";
if (qty === 0) throw new Error(`TrailingTake skipped: no open position for ${symbol}`);
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, newTakeProfitPrice));
const positionSide = toPositionSide(position);
await exchange.createOrder(symbol, "limit", exitSide, qty, tpPrice, { reduceOnly: true, positionSide });
}
async onBreakevenCommit(payload: BrokerBreakevenPayload): Promise<void> {
const { symbol, newStopLossPrice, position } = payload;
const exchange = await getFuturesExchange();
const orders = await exchange.fetchOpenOrders(symbol);
const slOrder = orders.find((o) => !!o.reduceOnly && ["stop_market", "stop", "STOP_MARKET"].includes(o.type ?? "")) ?? null;
if (slOrder) { await exchange.cancelOrder(slOrder.id, symbol); await sleep(CANCEL_SETTLE_MS); }
const qty = truncateQty(exchange, symbol, await fetchContractsQty(exchange, symbol, position));
const exitSide = position === "long" ? "sell" : "buy";
if (qty === 0) throw new Error(`Breakeven skipped: no open position for ${symbol}`);
const slPrice = parseFloat(exchange.priceToPrecision(symbol, newStopLossPrice));
const positionSide = toPositionSide(position);
await exchange.createOrder(symbol, "stop_market", exitSide, qty, undefined, { stopPrice: slPrice, reduceOnly: true, positionSide });
}
async onAverageBuyCommit(payload: BrokerAverageBuyPayload): Promise<void> {
const { symbol, currentPrice, cost, position, priceTakeProfit, priceStopLoss } = payload;
const exchange = await getFuturesExchange();
const openOrders = await exchange.fetchOpenOrders(symbol);
await cancelAllOrders(exchange, openOrders, symbol);
await sleep(CANCEL_SETTLE_MS);
const existing = await fetchContractsQty(exchange, symbol, position);
const minNotional = exchange.markets[symbol].limits?.cost?.min ?? 1;
if (existing * currentPrice < minNotional) throw new Error(`AverageBuy skipped: no open position for ${symbol}`);
const qty = truncateQty(exchange, symbol, cost / currentPrice);
if (qty <= 0) throw new Error(`Computed qty is zero — cost=${cost}, price=${currentPrice}`);
const entryPrice = parseFloat(exchange.priceToPrecision(symbol, currentPrice));
const tpPrice = parseFloat(exchange.priceToPrecision(symbol, priceTakeProfit));
const slPrice = parseFloat(exchange.priceToPrecision(symbol, priceStopLoss));
const positionSide = toPositionSide(position);
const entrySide = position === "long" ? "buy" : "sell";
const exitSide = position === "long" ? "sell" : "buy";
await createLimitOrderAndWait(exchange, symbol, entrySide, qty, entryPrice, { positionSide }, { exitSide, tpPrice, slPrice, positionSide: position });
const totalQty = truncateQty(exchange, symbol, await fetchContractsQty(exchange, symbol, position));
try {
await exchange.createOrder(symbol, "limit", exitSide, totalQty, tpPrice, { reduceOnly: true, positionSide });
await exchange.createOrder(symbol, "stop_market", exitSide, totalQty, undefined, { stopPrice: slPrice, reduceOnly: true, positionSide });
} catch (err) {
await exchange.createOrder(symbol, "market", exitSide, totalQty, undefined, { reduceOnly: true, positionSide });
throw err;
}
}
}
);
Broker.enable();
Signal open/close events are routed automatically via the
syncSubject subscription installed by Broker.enable(). All other operations (partialProfit, trailingStop, breakeven, averageBuy) are called explicitly before the corresponding state mutation — no manual wiring needed.