CCXT
JavaScript Examples

Order Router Stream

Order Router Stream — CCXT JavaScript code example.

// @NO_AUTO_TRANSPILE
// OrderRouter — watch a route, and ask the service about itself.
//
// Two things this covers that the other order-router examples do not:
//
//   1. watchRoute: the same request as fetchRoute, held open over a WebSocket.
//      The service pushes a fresh RouteResult whenever any market the route
//      depends on moves — every leg of every candidate path, so a bridged route
//      does not miss half the price changes that alter its answer.
//
//   2. The read-only endpoints: whether the router is up, whether it can
//      actually price anything yet, which commit is deployed, and the exact
//      cached book a route was ranked on.
//
// This example PLACES NOTHING. watchRoute is read-only — it asks for a
// recommendation repeatedly, it does not trade.
//
// Usage:
//   npm run tsBuild && node js/examples/ts/order-router-stream.js
//
// The router service is public: no API key, no signup.
import ccxt from '../../js/ccxt.js';
async function main() {
    const router = new ccxt.OrderRouter();
    // ---------------------------------------------------------------------
    // Is it up, and can it price anything?
    // ---------------------------------------------------------------------
    // These are different questions and the difference matters. /health answers
    // 200 from the first millisecond of boot, before a single websocket has
    // connected — gate a deploy on it and traffic arrives at a router whose only
    // possible answer is "all books stale". fetchReadiness is the one to gate on.
    //
    // Both are unauthenticated, so they work before a key is injectable.
    const health = await router.fetchHealth();
    console.log('health   ', health['status'], 'up for', Math.round(health['uptimeSec']), 'seconds');
    // NOT an exception when the answer is no: the service replies 503 carrying
    // the same body it returns on 200, and a caller asking "are you ready" needs
    // the counts that say why not.
    const readiness = await router.fetchReadiness();
    console.log('readiness', readiness['status'], readiness['freshCount'], 'of', readiness['bookCount'], 'books fresh');
    if (readiness['status'] !== 'ready') {
        console.log('the router cannot rank a route yet — try again shortly');
        return;
    }
    // Build provenance. /health answers 200 from the OLD process just as happily
    // when a deploy silently no-ops; commit is the only field that tells them apart.
    const version = await router.fetchVersion();
    console.log('version  ', version['version'], version['commitShort']);
    // ---------------------------------------------------------------------
    // Watch a route
    // ---------------------------------------------------------------------
    // watchRoute BLOCKS for the life of the stream, and the hook is how you read
    // it. Return 'stop' to close the socket cleanly; the call then returns the
    // last route it saw.
    //
    // Two endpoint rules differ from fetchRoute, and neither is this client's
    // choice:
    //
    //   - balances and balanceMode are REFUSED. A socket is held open for minutes
    //     and carries no channel to update the holdings it was opened with, so
    //     every frame after the first would price a portfolio you may already have
    //     traded away. Use fetchRoute when you need a funded-aware route.
    //   - includeQuotes defaults to FALSE here and true on REST. One socket
    //     measured 658 frames/sec at 9.3KB, almost all of it that diagnostic.
    let frames = 0;
    const last = await router.watchRoute('USDT', 'BTC', {
        'amountIn': 20,
        'strategy': 'split_optimal',
    }, (route) => {
        frames = frames + 1;
        console.log('frame', frames, 'rate', route['effectiveRate'], 'impactBps', route['impactBps'], 'hops', route['hops'].length);
        // Stop after a handful so the example terminates. A real consumer would
        // decide on the numbers — say, stop once the price impact is acceptable.
        return (frames >= 5) ? 'stop' : 'continue';
    });
    // Every frame is stamped exactly as fetchRoute stamps its answer, so the last
    // one can go straight into the plan builder and still be checked against the
    // question this client actually asked.
    console.log('stopped after', frames, 'frames; last rate', last['effectiveRate']);
    // A route the stream pushed plans like any other. Nothing is placed: execute
    // defaults to dry_run, and this example never calls it.
    const plan = router.buildExecutionPlan(last, {});
    console.log('a plan from the last frame has', plan['stepCount'], 'steps over', plan['hopCount'], 'hops');
    // ---------------------------------------------------------------------
    // The exact book a route was ranked on
    // ---------------------------------------------------------------------
    // This is what makes a surprising route auditable: not "the router says
    // mexc", but the depth it actually walked.
    const firstStep = plan['steps'][0];
    if (firstStep !== undefined) {
        const book = await router.fetchCachedOrderBook(firstStep['exchangeId'], firstStep['symbol']);
        console.log('cached book on', firstStep['exchangeId'], firstStep['symbol'], '— best bid', book['bids'][0], 'best ask', book['asks'][0]);
    }
    // Which venues the router is actually connected to. A venue can hold an open
    // socket while its subscription is silently dead, so read the per-venue update
    // age rather than the connected flag alone.
    const venues = await router.fetchExchangesStatus();
    console.log('the router is tracking', venues.length, 'venues');
}
main();