The live broker adapter inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/ai-trading-mcp/llms.txt
Use this file to discover all available pages before exploring further.
modules/live.module.ts was written after a real incident post-mortem. Binance spot has a specific set of failure modes — frozen balances, duplicate clientOrderId collisions, race conditions between fills and cancels — that naive adapters trip over silently. This page explains the three-commit methodology that eliminates those failure modes one by one, and the tuning constants that control timing across each phase.
Why OCO Instead of Two Separate Sell Orders
The most common naive implementation places a take-profit limit sell and a stop-loss sell as two independent orders. On Binance spot, this cascade-fails every time:| Naive approach | What Binance actually does | This adapter |
|---|---|---|
| Two independent TP + SL sell orders | First sell freezes the coins; second sell gets InsufficientFunds — this is the cascade root | One OCO: TP + stop-limit SL in a single freeze |
| Check free balance to confirm entry | After entry, coins sit frozen inside the OCO; free ≈ 0 → buys again (position doubling) | Checks free + used (fetchTotalQty) |
Retry entry POST over a live NEW order | -2010 duplicate clientOrderId — terminal drop while your own order is still on book | Cancel first (frees the id), then re-enter |
| Treat a failed cancel as a failure | Order filled between last poll and cancel (-2011) — that is a fill | cancelOrderSafe re-reads the order, returns "filled" |
| Market-sell the unwind qty directly | Tries to sell what its own TP order froze → unwind fails | Cancel-sweep → verify clean book → sell free balance |
privatePostOrderOco) creates both legs atomically against one freeze. The coins can only be released once — either by the TP triggering, the SL triggering, or an explicit cancel-sweep.
The Three-Commit Methodology
Every open/close lifecycle passes through exactly three commits. Each is responsible for one phase and each handles its own failure modes internally.commit_buy — Guaranteed Entry
Places a limit buy at the signal price, polls for a fill, and falls back to a market top-up for any unfilled remainder. The order never lingers on the book after this step completes.The
cancelOrderSafe wrapper handles the fill-vs-cancel race: if the cancel throws for any reason, it re-reads the order status; if the order is "closed", it returns "filled" — that is not an error, it is a fill at the last moment.commit_trade — Atomic Brackets
Places TP and stop-limit SL in a single OCO order. One call, one freeze, both levels armed atomically.The bracket quantity is taken from
fetchFreeQty at the moment of placement — not from the original order qty — so partial fills during the limit+market entry phase are handled correctly. If the OCO itself fails, unwindPosition runs: cancel-sweep → verify clean book → market-sell free balance, then re-throws the original error typed.commit_cancel — Verified Close
Cancels every open order for the symbol with up to
CANCEL_ROUNDS retries, then verifies the book is empty before selling. Selling over a live sell order is InsufficientFunds — the verification loop is not optional.After the book is confirmed clean, the adapter sells the entire free balance of the base coin — not just the engine’s tracked position size. This sweeps any orphan tranches from partial fills or leftover dust alongside the main close. If the remaining quantity is below minNotional, the close is confirmed as dust and returns without placing an order.Idempotent Recovery via clientOrderId = signalId
Every entry order is placed with clientOrderId set to the engine’s signalId. This makes recovery unconditional — not just on explicit retries — because engine revalidation can deliver the same signalId with attempt = 0.
New signal ID
fetchEntryByClientId gets -2013 from Binance (Order does not exist), returns null, and the adapter proceeds to place the entry normally. The check costs one API call.Known signal ID, filled
executedQty > 0 — the previous POST executed but the process crashed before brackets were placed. The adapter checks fetchTotalQty (free + used — checking only free would show ≈ 0 while coins are frozen in an existing OCO), and places brackets if no open orders exist.Known signal ID, still live
Status is
NEW or PARTIALLY_FILLED — a prior attempt’s order is on the book. The adapter cancels it first (freeing the clientOrderId so the next POST won’t get -2010), waits CANCEL_SETTLE_MS, then re-enters.Known signal ID, filled at cancel flag
cancelOrderSafe returned "filled" — the order filled in the race window between the last poll and the cancel call. Brackets are placed immediately and the open returns success.The
fetchTotalQty distinction matters precisely because of OCO bracket behavior. After a successful entry, the coins are frozen inside the OCO and free ≈ 0. An adapter that checks only free would see an empty balance, conclude no position exists, and buy again — doubling the position. This was the root cause of the incident that prompted the post-mortem.Tuning Constants
All timing and price-shaping constants are defined at the top ofmodules/live.module.ts. They are not configuration — they are hardcoded after post-mortem calibration.
| Constant | Value | Purpose |
|---|---|---|
FILL_POLL_INTERVAL_MS × FILL_POLL_ATTEMPTS | 10 s × 10 | Limit entry gets ~100 s to fill before the market top-up fires |
CANCEL_SETTLE_MS | 2 s | Exchange settle time after cancel before re-reading the filled qty |
CANCEL_ROUNDS | 10 | Cancel-sweep retries when clearing the book on close |
STOP_LIMIT_SLIPPAGE | 0.995 | Stop-limit price parked just below the SL trigger to guarantee execution |
TRADE_SELL_LOWER_PERCENT | 0.999 | Close limit priced a hair under market to fill fast without crossing the spread |
Spot Sizing
Entry cost is capped atmin(signalCost, freeUSDT × 0.98) — a 2 % buffer for fees and lot-size rounding. If the effective cost falls below the symbol’s minNotional, the adapter throws OrderRejectedError immediately rather than spamming retry attempts every tick:
OrderRejectedError) — the engine will not retry it. Insufficient funds is not a transient condition.