Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/theonetrade/ai-trading-mcp/llms.txt

Use this file to discover all available pages before exploring further.

The live broker adapter in modules/live.module.ts was written after a real post-mortem on a set of failure modes that a naïve ccxt wrapper exposes the moment it touches Binance spot. Every section of the module has a named cause. This page walks through the three commits that make up the methodology and explains the exact code behind each one.

The Three-Commit Methodology

Every order lifecycle is decomposed into exactly three atomic operations. Each commit solves one class of failure and the three compose cleanly — the output of commit_buy is the input precondition of commit_trade, and commit_cancel tears down whatever commit_trade assembled.

commit_buy

Guaranteed entry. A limit order is posted and polled. If the poll times out the order is cancelled (race condition handled) and a market top-up buys the remainder. The order never lingers on the book.

commit_trade

Atomic brackets. Take-profit and stop-loss are placed as a single OCO order — one freeze, not two independent sells. On spot, the first sell freezes the coins and the second dies with InsufficientFunds.

commit_cancel

Verified flat. A cancel sweep removes every open order for the symbol, a second verification loop confirms the book is empty, and then the entire free balance is sold. Orphan tranches are swept along.

Why Naïve Spot Adapters Fail

The table below (reproduced from the project README) maps each common adapter mistake to the exact Binance behaviour it triggers, and shows what the adapter does instead.
A naïve adapter doesWhat Binance actually doesThis adapter
Places TP and SL as two independent sellsThe first order freezes the coins, the second dies with InsufficientFunds — the cascade rootOne OCO: TP + stop-limit SL in a single freeze
Answers “is the position bought?” from free balanceAfter a successful entry the coins sit frozen inside the OCO, free ≈ 0 → it buys again (position doubling)Checks free + used (fetchTotalQty)
Retries the entry POST over a live NEW order-2010 duplicate clientOrderId → terminal drop while your own order is still on the bookCancel first (frees the id), then re-enter
Treats a failed cancel as a failureThe order filled between the last poll and the cancel (-2011) — that is a fillcancelOrderSafe re-reads the order, returns "filled"
Market-sells the unwind qty directlyIt tries to sell what its own TP order froze → the unwind itself fails, the raw error masks the verdictCancel-sweep → verify clean book → sell free balance; the original error always reaches the engine typed
Retries every error forever (or drops every error)Network hiccups and exchange verdicts are different beastsccxt.NetworkError → OrderTransientError (bounded retry), ccxt.ExchangeError → OrderRejectedError (permanent)

Guaranteed Entry (commit_buy)

The entry sequence is a limit order followed by a fill poll. If the poll times out, the order is cancelled with fill-race handling and a market order buys whatever quantity did not fill.
const order = await exchange.createOrder(symbol, "limit", "buy", qty, price, { clientOrderId: signalId });

let last = order;
if (last.status !== "closed") {
  let filled = false;
  for (let i = 0; i !== FILL_POLL_ATTEMPTS; i++) {
    await sleep(FILL_POLL_INTERVAL_MS);
    last = await exchange.fetchOrder(order.id, symbol);
    if (last.status === "closed") { filled = true; break; }
  }
  if (!filled) {
    if ((await cancelOrderSafe(exchange, order.id, symbol)) === "filled") return;  // filled at the flag
    await sleep(CANCEL_SETTLE_MS);
    const final = await exchange.fetchOrder(order.id, symbol);
    const remainder = truncateQty(exchange, symbol, qty - (final.filled ?? 0));
    if (remainder > 0) await exchange.createOrder(symbol, "market", "buy", remainder);
  }
}
cancelOrderSafe catches the -2011 error that Binance throws when a cancel races a fill. It re-fetches the order and returns "filled" rather than propagating an exception, so a real fill is never mistaken for a drop.
The full buyLimitGuaranteed helper in the module wraps this loop with the FILL_POLL_INTERVAL_MS and FILL_POLL_ATTEMPTS constants. After the poll the CANCEL_SETTLE_MS pause lets the exchange settle before the filled quantity is re-read.

Idempotent Recovery

Reconciliation by clientOrderId = signalId runs unconditionally — not only on retries. This is a critical detail: after an engine revalidation, a fresh row of the same signal id can arrive with attempt = 0, and an attempt-guard would happily buy twice. For a new id the reconciliation costs exactly one call (-2013 → not found → post away).
const prior = await fetchEntryByClientId(exchange, symbol, signalId);
if (prior && prior.executedQty > 0) {
  const totalQty = await fetchTotalQty(exchange, symbol);          // free + used!
  if (totalQty * openPrice >= minNotional) {
    if (!(await exchange.fetchOpenOrders(symbol)).length) await confirmWithBrackets();
    return;                                                        // entry confirmed — do NOT buy again
  }
} else if (prior && (prior.status === "NEW" || prior.status === "PARTIALLY_FILLED")) {
  if ((await cancelOrderSafe(exchange, prior.orderId, symbol)) === "filled") { /* ... */ }
}
The position check must use fetchTotalQty (free + used), never just free. After a successful entry the coins are frozen inside the OCO bracket — free ≈ 0 — and a free-only check would incorrectly conclude the position is absent and buy again, doubling the exposure.

Verified Close (commit_cancel)

The close sequence has three distinct phases executed in strict order.
1

Cancel sweep

cancelSweepAndVerify iterates over all open orders for the symbol and cancels each one, retrying up to CANCEL_ROUNDS times. A per-order sleep(1_000) gives the exchange time to settle between individual cancels.
2

Verify clean book

After the sweep a second loop re-fetches open orders and confirms the list is empty. Selling over a live sell order returns InsufficientFunds — the verification loop catches this before the sell is attempted.
3

Sell entire free balance

The adapter sells the whole free balance, not only the quantity the engine recorded for the position. Orphan tranches from partial fills are swept along. Dust below minNotional confirms the close as-is.
If cancelSweepAndVerify fails (for example due to a network error mid-sweep), the engine retries the close on the next tick. Because the next onOrderCloseCommit call starts with another cancel sweep, the operation is naturally idempotent.

Tuning Constants

All timing and pricing constants are declared at the top of the module so they can be adjusted without hunting through logic code.
ConstantValueWhy
FILL_POLL_INTERVAL_MS × FILL_POLL_ATTEMPTS10 s × 10A limit entry gets ~100 s to fill before the market top-up fires
CANCEL_SETTLE_MS2 sLets the exchange settle after a cancel before the filled quantity is re-read
CANCEL_ROUNDS10Cancel-sweep retry budget when clearing the book on close
STOP_LIMIT_SLIPPAGE0.995Stop-limit price parked just below the SL trigger to guarantee execution
TRADE_SELL_LOWER_PERCENT0.999Close limit priced a hair under market to fill fast

Error Classification

Every error that escapes a ccxt call is routed through toTypedError before it reaches the engine. The classification determines how the engine responds.

ccxt.NetworkError → OrderTransientError

Covers RequestTimeout, ExchangeNotAvailable, DDoSProtection, and similar transient conditions. The engine retries with the same signalId — the idempotency logic above ensures no double-buy results.

ccxt.ExchangeError → OrderRejectedError

Covers InsufficientFunds, InvalidOrder, minimum-notional violations, and any other permanent business verdict from Binance. The engine drops the signal without retrying.
Short positions are also classified as a permanent OrderRejectedError — spot trading does not support shorts, so the adapter rejects them immediately without reaching the network.

Build docs developers (and LLMs) love