Skip to main content

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

Every error that crosses the broker boundary — from a flaky network hop to an exchange verdict — is classified before it reaches the trading engine. The classification is binary and permanent: an error is either transient (retry it) or rejected (do not retry, ever). Getting this wrong in either direction is expensive. This page covers the two error types, how they are mapped from ccxt exceptions, the cancelOrderSafe fill-race handler, and the HTTP envelope that carries all of this to the agent.

Two Error Categories

The trading engine in backtest-kit defines two error classes that the broker adapter uses to signal outcome:

OrderTransientError

Network errors, rate limits, and temporary exchange unavailability. The engine treats these as recoverable and retries with bounded backoff up to CC_ORDER_CLOSE_RETRY_ATTEMPTS. The position remains open while retries are in flight.Examples: RequestTimeout, ExchangeNotAvailable, DDoSProtection (all ccxt.NetworkError subclasses).

OrderRejectedError

Exchange verdicts — bad parameters, insufficient funds, invalid symbol, duplicate order ID. These are permanent. Retrying wastes time and can make the situation worse. The engine does not retry these; it records the rejection and moves on.Examples: InsufficientFunds, InvalidOrder, BadSymbol, min-notional violations (all ccxt.ExchangeError subclasses).

Mapping ccxt Exceptions

The toTypedError function in modules/live.module.ts is the single translation point. Every exception that escapes the adapter passes through it:
// ccxt.NetworkError → OrderTransientError (bounded retry)
// ccxt.ExchangeError → OrderRejectedError (permanent)

function toTypedError(e: unknown): Error {
  if (e instanceof ccxt.NetworkError) {
    return OrderTransientError.fromError(e as object);
  }
  if (e instanceof ccxt.ExchangeError) {
    return OrderRejectedError.fromError(e as object);
  }
  return e as Error;
}
The mapping mirrors ccxt’s own hierarchy: NetworkError covers anything that went wrong before the exchange processed the request; ExchangeError covers anything the exchange explicitly rejected. Anything that is neither — an unexpected runtime exception — passes through unwrapped so it surfaces as a raw error with its original stack trace.
Retrying a permanent error is not just wasteful — it can actively damage the trading state. The canonical example is a -2010 duplicate clientOrderId error. The order is live on the book. Retrying the POST without first cancelling that order produces another -2010, in a loop, while the original order sits untouched. The adapter handles this by always running fetchEntryByClientId first and cancelling any live prior order before re-entering.

The cancelOrderSafe Fill-Race Handler

Cancelling an order that filled in the window between the last status poll and the cancel call is not an error — it is a fill. Binance signals this with -2011, and a naive adapter that propagates any cancel exception as a failure has just dropped a real entry. cancelOrderSafe handles this race explicitly:
async function cancelOrderSafe(
  exchange: Binance,
  orderId: string,
  symbol: string,
): Promise<"canceled" | "filled"> {
  try {
    await exchange.cancelOrder(orderId, symbol);
    return "canceled";
  } catch (cancelErr) {
    const status = await exchange.fetchOrder(orderId, symbol);
    if (status.status === "closed") return "filled";
    throw toTypedError(cancelErr);
  }
}
When the cancel throws, the function re-reads the order. If it is "closed", the order filled — return "filled" to the caller, which then confirms the entry and places brackets. If it is anything else, re-throw the original cancel error typed through toTypedError. The fill-race path is handled in both the entry phase (buyLimitGuaranteed) and the close phase.

The Original Error Always Reaches the Engine

In the close flow, the adapter runs cancelSweepAndVerify before attempting the sell. If the sell itself fails, the sweep has already happened — brackets are down, the book is clean, and the engine’s soft-SL takes over for the next tick’s retry. The error that caused the failure is re-thrown through toTypedError, so the engine receives a properly typed verdict:
// In onOrderCloseCommit:
} catch (err) {
  // network → transient: engine holds position and retries close next tick
  // (bounded CC_ORDER_CLOSE_RETRY_ATTEMPTS, then force-close)
  // exchange verdict → rejected: permanent, operator verifies real position
  throw toTypedError(err);
}
The same principle applies in unwindPosition. When bracket placement fails and the adapter needs to emergency-unwind, it runs cancel-sweep → verify → market-sell, but always re-throws the original error — the error that caused the bracket failure, not any error from the unwind itself. If the unwind also fails, it is silently swallowed so the original verdict still reaches the engine typed correctly.
async function unwindPosition(
  exchange: Binance,
  symbol: string,
  originalErr: unknown,
): Promise<never> {
  try {
    await cancelSweepAndVerify(exchange, symbol);
    const freeQty = truncateQty(exchange, symbol, await fetchFreeQty(exchange, symbol));
    if (freeQty > 0) {
      await exchange.createOrder(symbol, "market", "sell", freeQty);
    }
  } catch {
    // unwind failed — position requires operator review; the original error matters more
  }
  throw toTypedError(originalErr);  // original error, always typed
}

The HTTP Error Envelope

The MCP bridge (@backtest-kit/mcp) wraps every operation result in a flat envelope before returning it to the stdio MCP server:
// Transport success (HTTP 200) is separate from operation success (empty error field)
{ error: string }  // "" = success; non-empty = the engine's exact error message
An empty error string means the operation succeeded. A non-empty error string is the engine’s exact message, relayed to the agent as an isError: true MCP tool result.
HTTP 200 does not mean the trade succeeded. The HTTP layer only confirms that the bridge received and processed the request. Always inspect the error field in the response envelope — a 200 with a non-empty error means the trade was rejected or failed.
This separation is intentional: the stdio server and the trading process are two independent processes. Transport health and operation health are different concerns, and conflating them (e.g., using HTTP 4xx/5xx for trade rejections) would force the MCP client to handle two different error channels.

What the Agent Sees

The agent receives error information as structured isError MCP tool results. The message is the engine’s exact error string — not a generic wrapper. This means the agent can read the rejection reason and reason about it:
ScenarioError the agent sees
open_position on a symbol not in CC_SYMBOL_LISTEngine rejects with a clear “symbol not in whitelist” message before the broker is called
open_position when a position or pending signal already exists for that symbolDuplicate-open rejection — engine validates before the broker touches the exchange
close_position when nothing is open”nothing to close” error — returned before any cancel or sell attempt
Network error during Telegram get_status fetchThe 90-second timeout tears down the GramJS connection; agent sees "timeout fetching feed messages" and the connection rebuilds on the next call
Insufficient USDT to meet minNotionalOrderRejectedError from the broker: "free USDT X.XX → cost Y.YY < minNotional Z (SYMBOL) — вход пропущен"
Short position requested on spotOrderRejectedError: "SpotBrokerAdapter: short position is not supported on spot"
Because error messages are exact and typed, the agent can distinguish between “this will never work” (OrderRejectedError — stop trying) and “this might work next tick” (OrderTransientError — wait for the engine’s retry). In practice this means the agent’s note on a close or re-open attempt can record whether the prior failure was a network hiccup or an exchange verdict, giving the audit trail a complete picture.

Build docs developers (and LLMs) love