Order Router
a client for the ccxt order-routing service and a multi-venue execution engine for plans you build yourself. NOT an exchange: it does not extend Exchange,…
OrderRouter
Kind: global class
- constructor
- fetchRoute
- fetchHealth
- fetchReadiness
- fetchVersion
- fetchSymbols
- fetchExchangesStatus
- fetchCachedOrderBook
- watchRoute
- fetchRouteWithBalances
- loadBalances
- invalidateBalances
- marketsOf
- renderBalances
- buildExecutionPlan
- checkExecutionPlanSafety
- reconcileExecutionStep
- buildUnwindPlan
- execute
OrderRouter
a client for the ccxt order-routing service and a multi-venue execution engine for plans you build yourself. NOT an exchange: it does not extend Exchange, has no unified methods, and is constructed directly. The whole pipeline is two calls — fetchRoute, then execute
OrderRouter.OrderRouter ()constructor
creates a client for the CCXT order-router service
Kind: instance method of OrderRouter
Returns: OrderRouter - a router client
| Param | Type | Required | Description |
|---|---|---|---|
| config | object | Yes | client configuration |
| config.apiKey | string | No | optional. The router service is public and rate-limits by IP, so no key is needed; one supplied here is still sent as the x-api-key header, which a keyless server ignores |
| config.baseUrl | string | No | router base url, defaults to https://docs.ccxt.com/router/api |
| config.venues | object | No | exchangeId to a ccxt exchange instance. Routes are filtered to these, and execute sends orders to them |
| config.trackBalances | bool | No | read those venues' wallets and route on what you can actually fund, default false. Off, fetchRoute never touches a venue and stays a single HTTP request |
| config.timeoutMs | int | No | request timeout in milliseconds, defaults to 30000 |
| config.maxNotionalUsd | float | No | optional per-trade USD notional guardrail. Omitted or 0 means NO cap and no notional check at all; any positive value is honoured exactly, never clamped |
OrderRouter.constructor (config)fetchRoute
asks the router how to convert one asset into another, over the venues and bridges it has live books for
Kind: instance method of OrderRouter
Returns: object - a RouteResult — an unroutable pair comes back as a RouteResult with an unroutableReason, not as an exception
See: https://docs.ccxt.com/router/api
| Param | Type | Required | Description |
|---|---|---|---|
| fromAsset | string | Yes | the asset being spent, e.g. USDT |
| toAsset | string | Yes | the asset being acquired, e.g. BTC |
| params | object | Yes | request parameters |
| params.amountIn | float | No | exact amount of fromAsset to spend — supply this OR amountOut, never both |
| params.amountOut | float | No | exact amount of toAsset to acquire — supply this OR amountIn, never both |
| params.strategy | string | No | best_single, split_optimal or split_capped |
| params.maxVenues | int | No | per-hop venue cap for split_capped |
| params.exchanges | string, Array<string> | No | venue allowlist |
| params.bridges | string, Array<string> | No | intermediary assets to consider |
| params.balances | string | No | what you hold, as [exchangeId.]ASSET:amount entries |
| params.balanceMode | string | No | cap (default) or require |
| params.includeQuotes | bool | No | return the per-venue diagnostic |
| params.includeFees | bool | No | rank on fee-adjusted price, default true |
| params.certified | bool | No | restrict to CCXT-certified venues |
| params.requireFullFill | bool | No | refuse partial fills |
| params.hopPenaltyBps | float | No | how much better a bridged route must be per extra hop |
| params.minLegNotional | float | No | suppress legs below this quote notional |
| params.requestId | string | No | a caller-chosen audit id, sent as the x-request-id header so your log and the router's decision log can be joined. The service mints one when this is absent, and caps it at 200 characters |
| params.requireBalancesApplied | bool | No | when balances are sent, throw unless the router echoed that it read them. Default true — a server that predates the feature IGNORES balances and answers byte-identically to one that never received any |
OrderRouter.fetchRoute (fromAsset, toAsset, params)fetchHealth
liveness only — answers 200 from the first millisecond of boot, before a single venue has connected. Use fetchReadiness to decide whether the router can actually price anything
Kind: instance method of OrderRouter
Returns: object - status and uptimeSec
See: https://docs.ccxt.com/router/openapi.yaml // GET /health
OrderRouter.fetchHealth ()fetchReadiness
whether the router has enough fresh books to rank on, measured with the same staleness cutoff /route uses. NOT_READY IS A NORMAL ANSWER: the service replies 503 with the same body it returns on 200, and this method returns it rather than throwing, because a caller asking "are you ready" needs the counts that say why not
Kind: instance method of OrderRouter
Returns: object - status (ready or not_ready), bookCount, freshCount, staleCount, minFreshBooksForReady and staleBookMs
See: https://docs.ccxt.com/router/openapi.yaml // GET /ready
OrderRouter.fetchReadiness ()fetchVersion
build provenance of the running process. This is what a deploy pipeline asserts against — health answers 200 from the OLD process just as happily when a deploy silently no-ops, and commit is the only field that tells the two apart
Kind: instance method of OrderRouter
Returns: object - version, commit, commitShort, builtAt, builtBy, startedAt and uptimeSec
See: https://docs.ccxt.com/router/openapi.yaml // GET /version
OrderRouter.fetchVersion ()fetchSymbols
the unified symbols the router currently holds a cached book for. A pair absent from this list cannot be routed no matter how it is spelled
Kind: instance method of OrderRouter
Returns: Array<string> - the cached symbols
See: https://docs.ccxt.com/router/openapi.yaml // GET /symbols
OrderRouter.fetchSymbols ()fetchExchangesStatus
per-venue connection health. A venue can hold an open socket while its subscription is silently dead, so read the per-venue update age and not only the connected flag
Kind: instance method of OrderRouter
Returns: Array<object> - one health record per venue
See: https://docs.ccxt.com/router/openapi.yaml // GET /exchanges/status
OrderRouter.fetchExchangesStatus ()fetchCachedOrderBook
the router's own cached L2 book for one venue and symbol — the exact depth a route was ranked on, which is what makes a surprising route auditable
Kind: instance method of OrderRouter
Returns: object - the cached book
See: https://docs.ccxt.com/router/openapi.yaml // GET /orderbook/{exchange}/{symbol}
| Param | Type | Description |
|---|---|---|
| exchangeId | string | the venue, e.g. binance |
| symbol | string | the unified symbol, e.g. BTC/USDT |
OrderRouter.fetchCachedOrderBook (exchangeId, symbol)watchRoute
the same route as fetchRoute, pushed over a websocket whenever any market it depends on moves. Every leg of every candidate path is watched, so a bridged route does not miss half the price changes that alter its answer. BLOCKS until the stream ends: the hook is how you read it
Kind: instance method of OrderRouter
Returns: object - the last route seen, or an empty dict if the stream ended before any frame
See: https://docs.ccxt.com/router/openapi.yaml // GET /stream/route
| Param | Type | Description |
|---|---|---|
| fromAsset | string | the asset being spent, e.g. USDT |
| toAsset | string | the asset being acquired, e.g. BTC |
| params | object | the same parameters fetchRoute accepts, EXCEPT balances and balanceMode, which this endpoint refuses — a socket outlives the holdings it was opened with |
| onRoute | function | called with each RouteResult as it arrives, stamped with the client-side keys fetchRoute stamps. Return 'stop' to close the socket cleanly and return; any other value keeps watching. It runs between frames, so do no slow work in it — the service pushes up to ten times a second |
OrderRouter.watchRoute (fromAsset, toAsset, params, onRoute)fetchRouteWithBalances
reads the live balances of the supplied venues, sends them to the router, and returns a route you can actually fund
Kind: instance method of OrderRouter
Returns: object - the RouteResult, with the client-side keys balancesUsed and balancesDropped added
| Param | Type | Required | Description |
|---|---|---|---|
| fromAsset | string | Yes | the asset being spent |
| toAsset | string | Yes | the asset being acquired |
| venues | object | No | a dictionary of exchangeId to a ccxt exchange instance; defaults to the venues the router was constructed with |
| params | object | Yes | the same parameters fetchRoute accepts, minus balances which this method builds |
| params.requireBalancesApplied | bool | No | throw when the router did not echo balancesApplied, default true |
OrderRouter.fetchRouteWithBalances (fromAsset, toAsset, venues?, params)loadBalances
reads the venues' wallets once and caches the result, so a quote stays a single HTTP request. Called for you by fetchRoute; call it yourself to prime the cache at start-up, or with reload to refresh it
Kind: instance method of OrderRouter
Returns: string - the rendered balances string, empty when the router holds no venues
| Param | Type | Required | Description |
|---|---|---|---|
| reload | bool | No | true re-reads the wallets even when they are already cached |
OrderRouter.loadBalances (reload?)invalidateBalances
drops the cached balances, so the next quote re-reads the wallets. Called for you after any run that reached a venue
Kind: instance method of OrderRouter
OrderRouter.invalidateBalances ()marketsOf
loads each venue's markets and keys them by exchange id, which is the shape checkExecutionPlanSafety wants. execute does this for you; this is for calling the check yourself
Kind: instance method of OrderRouter
Returns: object - exchangeId to that venue's markets
| Param | Type | Description |
|---|---|---|
| venues | object | a dictionary of exchangeId to a ccxt exchange instance |
OrderRouter.marketsOf (venues)renderBalances
turns whatever a caller wrote for balances into the router's wire form. Accepts the rendered string itself, a list of entries, a per-venue wallet ({ mexc: { USDT: 100 } }) and a flat single-venue wallet ({ USDT: 100 })
Kind: instance method of OrderRouter
Returns: string - the [exchangeId.]ASSET:amount string the service reads
| Param | Type | Description |
|---|---|---|
| value | object, string | the holdings, in any accepted shape |
OrderRouter.renderBalances (value)buildExecutionPlan
turns a RouteResult into an ordered list of concrete orders. PURE — no I/O
Kind: instance method of OrderRouter
Returns: object - an execution plan
| Param | Type | Required | Description |
|---|---|---|---|
| route | object | Yes | a RouteResult, ideally one fetchRoute returned so the request can be checked against the answer |
| options | object | No | plan options |
| options.slippageBps | float | No | how far the limit sits from the expected price, default 25 |
| options.reconcileToleranceRatio | float | No | the shortfall ratio reconcileExecutionStep halts on, default 0.02 |
OrderRouter.buildExecutionPlan (route, options?)checkExecutionPlanSafety
checks a plan against per-venue market rules and the hard per-trade USD notional cap. PURE — no I/O. A step that cannot be valued in USD BLOCKS; it is never skipped, because a cap that silently disappears when a rate is missing is not a cap
Kind: instance method of OrderRouter
Returns: Array<object> - the violations, each with stepIndex, code, blocking, actual, limit and a constant message. An empty array means the plan passed
| Param | Type | Required | Description |
|---|---|---|---|
| plan | object | Yes | a plan from buildExecutionPlan |
| markets | object | Yes | a dictionary of exchangeId to that exchange's markets dictionary, i.e. markets[exchangeId][symbol] |
| options | object | No | check options |
| options.usdRates | object | No | a dictionary of currency code to its USD price. USD itself is 1 implicitly; nothing else is assumed |
| options.maxNotionalUsd | float | No | per-trade cap for this call, overriding the client's own in either direction. Omitted falls back to the client's; 0 or absent on both means no cap and no notional check |
| options.precisionMode | string | No | tick_size (default) or decimal_places, matching the venue's precisionMode |
OrderRouter.checkExecutionPlanSafety (plan, markets, options?)reconcileExecutionStep
compares what a step actually produced against what the route predicted, resizes every downstream hop, and returns the proceed-or-halt verdict. PURE — no I/O. The halt decision lives here rather than in the execution loop because it is a money decision, and six separate loops is six chances to omit it
Kind: instance method of OrderRouter
Returns: object - the verdict, with expectedOut, realisedOut, shortfall, shortfallRatio, scale, verdict, reason and resizedSteps
| Param | Type | Description |
|---|---|---|
| plan | object | the plan, with any earlier resizes already applied to its steps |
| stepIndex | int | the step that just completed |
| realisedOut | float | what it actually produced, in that step's output asset — base for a buy, quote for a sell |
OrderRouter.reconcileExecutionStep (plan, stepIndex, realisedOut)buildUnwindPlan
given a halted execution report, computes the reverse orders that sell each stranded residual back toward the original from-asset, on the venue that actually holds it. PURE — no I/O. NEVER automatic: the result carries requiresConfirmation and nothing in this class executes it
Kind: instance method of OrderRouter
Returns: object - the unwind plan, with steps[] in reverse execution order and unresolved[] for residuals that cannot be reversed
| Param | Type | Description |
|---|---|---|
| report | object | an execution report from execute |
OrderRouter.buildUnwindPlan (report)execute
executes a plan against live exchange instances. THE ONLY IMPURE METHOD, and IT PLACES ORDERS: calling it is the instruction, there is no permission flag beside it. Pass options.dryRun true to rehearse instead, which makes not one call against a venue
Kind: instance method of OrderRouter
Returns: object - an execution report with per-step results, openOrders, errors and the halt verdict
| Param | Type | Required | Description |
|---|---|---|---|
| plan | object | Yes | a RouteResult from fetchRoute, a plan from buildExecutionPlan, or a caller-assembled plan of the same shape — this method never assumes it came from the routing service. A route is turned into a plan here, so the simple path is fetchRoute then execute; build the plan yourself when you want to inspect or change it first |
| venues | object | Yes | a dictionary of exchangeId to a ccxt exchange instance |
| options | object | No | execution options |
| options.strategy | string | No | HOW the orders go out: sequential (the default), parallel_within_hop, limit_protected, best_effort or atomic_ish. Whether they go out at all is options.dryRun |
| options.slippageBps | float | No | only when a route is passed: how far the limit sits from the expected price, default 25 |
| options.reconcileToleranceRatio | float | No | only when a route is passed: the shortfall ratio reconcileExecutionStep halts on, default 0.02 |
| options.dryRun | bool | No | exactly true rehearses: the plan is built, checked and reported on, and not one call is made against a venue. Anything else, including absent, PLACES ORDERS |
| options.usdRates | object | No | currency code to USD price, required when live because the notional cap cannot be enforced without it |
| options.allowMarketOrders | bool | No | permit a market order when the venue cannot do IOC, default false |
| options.maxOrders | int | No | hard order-count cap, required by best_effort |
| options.acknowledgeDispersion | bool | No | required by best_effort, which can leave you holding an unintended asset mix |
| options.orderTimeoutMs | int | No | how long limit_protected leaves an order resting, default 20000 |
| options.pollIntervalMs | int | No | how often limit_protected checks a resting order, default 1000 |
| options.orderParams | object | No | extra params merged into every createOrder call |
| options.idempotencyKey | string | No | the identity of this execution, required when the plan carries no requestId; it keys the re-execution guard, and OVERRIDES the plan's requestId when both are given |
| options.allowReexecution | bool | No | must be exactly true to run a plan this instance has already executed live; the DEFAULT is refusal |
| options.retryFailedSteps | int | No | how many times to re-place a step the venue DEFINITIVELY REJECTED, default 0. An outcome_unknown step is never retried at any setting: it may already be a live position, and re-placing it is the double-fill this class exists to prevent. The router sets no client order id, so a retry is a fresh order to the venue |
| options.retryDelayMs | int | No | how long to wait before a retry, default 1000 |
| options.onStep | function | No | called after each step completes and reconciles, never mid-order, with one event object describing that step. Return 'halt' to stop the route cleanly (haltReason becomes halted_by_on_step); any other value continues. It can only STOP a route, never resume one already halted. Do NO network I/O here — it sits between orders on the money path. A hook that throws is recorded as on_step_hook_failed and the run continues, because losing the report would destroy the only account of orders that are already live |
OrderRouter.execute (plan, venues, options?)