CCXT
TypeScript Examples

Order Router

Order Router — CCXT TypeScript code example.

// @NO_AUTO_TRANSPILE

// OrderRouter — ask the router how to convert one asset into another.
//
// The router holds live L2 books from many venues and answers "what is the
// cheapest way to turn X into Y right now, and on which venues" — book-walked
// to your actual size, fee-adjusted, and split across venues when that beats
// any single one.
//
// THE WHOLE PIPELINE IS TWO CALLS: fetchRoute, then execute. execute takes the
// route directly, and does the rest itself — building the plan, loading each
// venue's markets, and checking every order against that venue's real rules
// before anything is sent. The pieces are all public (buildExecutionPlan,
// checkExecutionPlanSafety) for when you want to inspect or change what happens
// in between; see order-router-custom-plan.ts. You just don't have to.
//
// THIS EXAMPLE PLACES REAL ORDERS. execute trades by default, exactly like
// createOrder does — there is no permission flag beside it. As written it builds
// its venues with no credentials, so an exchange will refuse the order; put your
// keys in and it will go through. Add a dryRun option to rehearse instead, which
// makes not one call against a venue.
//
// Usage:
//   npm run tsBuild && node js/examples/ts/order-router.js
//
// The router service is public: no API key, no signup.

import ccxt from '../../js/ccxt.js';
import type { Dict } from '../../js/src/base/types.js';

async function main () {
    const router = new ccxt.OrderRouter ();

    // ---- 1. what is the cheapest way to do this? ---------------------------
    // Exactly one of amountIn or amountOut — never both, and never neither.
    // They are different book traversals, not a unit conversion: amountIn walks
    // until the money runs out, amountOut walks until the size is reached.
    const route = await router.fetchRoute ('USDT', 'BTC', {
        'amountIn': 20,
        'strategy': 'split_optimal',
    });

    // An unroutable pair comes back as a result with a reason, NOT an exception.
    // Refusing to quote is a deliberate outcome, not an error.
    if (route['unroutableReason'] !== undefined && route['unroutableReason'] !== null) {
        console.log ('unroutable:', route['unroutableReason']);
        return;
    }

    console.log (route['amountIn'], route['from'], '->', route['amountOut'], route['to']);
    console.log ('effective rate  ', route['effectiveRate']);
    console.log ('price impact    ', route['impactBps'], 'bps');  // positive is worse
    console.log ('fill ratio      ', route['fillRatio']);

    // One hop is a direct conversion; more than one means it was bridged
    // (e.g. SOL -> USDT -> BTC), and each hop is a separate order.
    const hops = route['hops'];
    for (let i = 0; i < hops.length; i++) {
        const hop = hops[i];
        console.log ('hop', i + 1, hop['pair'], hop['side'], '-', hop['legs'].length, 'venue(s)');
        const legs = hop['legs'];
        for (let j = 0; j < legs.length; j++) {
            const leg = legs[j];
            console.log ('   ', leg['exchangeId'], leg['amount'], '@', leg['effectivePrice']);
        }
    }

    // ---- 2. do it ----------------------------------------------------------
    // execute needs the exchange instances themselves, keyed by the id the route
    // names — that part is yours, because these are the objects that will carry
    // your API credentials. Everything else it derives.
    const venues: Dict = {};
    for (let i = 0; i < hops.length; i++) {
        const legs = hops[i]['legs'];
        for (let j = 0; j < legs.length; j++) {
            const exchangeId = legs[j]['exchangeId'];
            if (venues[exchangeId] === undefined) {
                venues[exchangeId] = new (ccxt as Dict)[exchangeId] ({});
            }
        }
    }

    // This places real orders with real money. Read the strategy table in
    // wiki/Manual.md first, and keep maxNotionalUsd on: the cap is opt-in and
    // honoured exactly as passed, and usdRates is what lets it be evaluated at
    // all. The run also needs an identity, which the route's requestId supplies,
    // so a re-run cannot re-place a filled order.
    const report = await router.execute (route, venues, {
        'strategy': 'sequential',
        'usdRates': { 'USDT': 1 },
        'maxNotionalUsd': 25,
        //  Called after each step completes and reconciles, never mid-order. Return 'halt' to
        //  stop the route; anything else continues. It can only stop a route, never resume one
        //  the reconciliation already halted. Do no network I/O here: it sits between orders.
        'onStep': (event: Dict) => {
            console.log ('  step', event['stepIndex'], event['status'], '->', event['outAmount'], event['outAsset']);
            return '';
        },
    });

    console.log ('strategy        ', report['strategy'], report['dryRun'] ? '(rehearsal)' : '(LIVE)');
    console.log ('would place     ', report['wouldPlaceOrders'], 'order(s)');
    // -1 means the route carried no calculatedAt: unknown, not fresh.
    console.log ('plan age        ', report['planAgeMs'], 'ms');

    await closeAll (venues);
}

// Every ccxt instance holds a keep-alive socket pool; without this the process
// hangs after main() returns instead of exiting.
async function closeAll (venues: Dict) {
    const ids = Object.keys (venues);
    for (let i = 0; i < ids.length; i++) {
        await venues[ids[i]].close ();
    }
}

await main ();