Trading technology

Inside the Chronicle trading engine: queues, orders and latency

How our Java trading engine keeps asynchronous order decisions consistent by recording intent locally, and why we measure processing, transmission and exchange confirmation as separate intervals.

If you're building an asynchronous trading system, your strategy decides while the exchange still answers earlier requests. A quote you choose now depends on orders whose fate arrives later, over REST, a private WebSocket stream or FIX. Ignore an in-flight order and you risk double-committing funds or sending duplicates; fold every delay into one latency number and you can't tell your code from the exchange.

Our Java trading engine sits at the centre of our trading technology, and it handles this with two decisions. We record order intent locally before anything leaves the process, and we time each leg of an order's journey separately. Here's why we made those choices, how one order travels from a price update through acknowledgement and a partial fill, and what our latency numbers do and don't measure.

The loop never treats a sent request as a live order

Every trading decision passes through the same closed loop. Exchange responses feed back into the state that drives the next decision, so the engine never assumes that a sent request has become a live order.

  1. Market data and account updates arrive and are converted into normalized events.
  2. Shared trackers for prices, books, balances and orders absorb those events.
  3. Subscribed strategies run and express the liquidity they want.
  4. The order management system (OMS) reconciles that wish with known orders and funds.
  5. Venue adapters send requests, and their responses re-enter step 2.

Configured boundaries between services are Chronicle queues. The strategy-to-OMS step is the exception: the strategy runner owns its OMS directly, so that handoff is an ordinary in-process call with no queue hop.

What Chronicle gives us and what we built

The engine started from Chronicle's Services framework and its demonstration project. Chronicle supplies the foundations, and everything trading-specific is ours.

Chronicle's foundations

From Chronicle, we use:

  • the Services runtime,
  • Chronicle Queue and Wire for transport and serialization,
  • event-loop utilities,
  • low-level event types.

We kept current with its releases, most recently upgrading in March 2024. The engine stands on vendor work we're glad to credit.

Our trading layer

On top of those foundations, we built:

  • the normalized exchange model and reference-data ingestion,
  • subscriptions and strategies,
  • quote reconciliation and exposure tracking,
  • the execution adapters.

Alexander Temchenko led the Java infrastructure work, including the architecture, OEMS, strategy framework and deployment pipeline. The engine is still a team effort.

Getting each event to the right strategy

Each event in the loop has to reach the right strategy, carrying the right identity, and land after the shared state already reflects it.

Integer identities

A reference-data service refreshes asset names, assets, symbols and instruments from our reference API. It uses incremental cursors, so it fetches only what changed since the last refresh. Every instrument gets a numeric ID; the ID identifies the instrument, and its reference record supplies the venue, account type, precision, minimum size and minimum notional.

When a Binance ticker arrives, we resolve it by venue, account type and exchange symbol together. Symbols without a mapping are dropped rather than forwarded. Order books follow a stricter path:

  1. request a REST depth snapshot,
  2. buffer stream updates until the snapshot arrives,
  3. check that each update correctly follows the snapshot's sequence,
  4. forward level changes only after that check passes.

Update first, then dispatch

The strategy runner updates the global trackers before routing an event to any strategy. A strategy reacting to a price, fill or balance event therefore already sees shared state that includes it.

Subscriptions are keyed by what each event concerns:

  • balances and positions by account,
  • prices, trades and books by instrument,
  • orders by account and instrument combined.

Strategies mark themselves as having work, and a polling pass runs the marked ones, although some callbacks act immediately.

Keeping the hot path quiet

Our design target is 0.1 to 1 ms wire-to-wire. Two habits serve it: we keep data small, and we keep processing on known threads.

Compact events and reused objects

Price tickers extend Chronicle's TriviallyCopyableEvent and hold only primitives: instrument ID, prices, quantities and a timestamp. That keeps serialization cheap and removes object references from the hottest messages. Tickers, orders and text buffers are reused, and orders come from pools.

When an order passes to an adapter, the adapter receives its own pooled copy, because the caller's reusable object may change a moment later. These choices reduce allocation; they don't eliminate it. Some paths, such as cross-price evaluation, still allocate collections.

One loop, one CPU, deliberate waiting

Services can share a Chronicle event group bound to a chosen CPU. The waiting strategy is configurable:

  • busy waiting by default,
  • a balanced mode for debugging,
  • yielding for IO-heavy services.

Busy polling deliberately spends CPU to cut waiting and scheduling jitter. Socket work, such as Netty WebSocket messages, doesn't touch trading state on network threads. A small executor hands it back to the event loop, so trackers and orders change on one thread, in one sequence, without locking around shared state.

How the OMS turns quotes into orders

Strategies can emit either desired quotes or explicit orders. On the quote-reconciliation path, they state the bid and ask they want, and the OMS works out how to get there from the current orders.

Desired quotes, reconciled into orders

The OMS compares the desired prices with tracked orders and replaces an order only when the difference exceeds a tolerance. It sizes new liquidity from free balance after deducting amounts already locked in open orders, then applies precision and minimum notional. A zero-sized quote therefore means "cancel what's there," which is how strategies withdraw when stream health checks fail.

The OMS also checks a rate limiter before creating liquidity. If the limiter would block a replacement, the OMS keeps the existing favorable order instead of cancelling it and leaving nothing behind.

Recording intent before sending

Creating an order resets a pooled object and gives it the SCHEDULED status. It also receives an instrument ID, a price, a precision-adjusted quantity and a client ID taken from Chronicle's nanosecond clock.

Sending then follows a fixed order, so the tracker learns about the order before the adapter does:

  1. pass the order limiter,
  2. insert the order into the tracker,
  3. timestamp it,
  4. only then hand it to the adapter.

Cancellations follow the same pattern: the order becomes CANCELING in the tracker before the cancel request goes out. That status records our intent; it doesn't confirm that the exchange has removed the order or released its funds. The next reconciliation pass sees pending intent while the exchange is still silent, which prevents duplicate requests and two orders competing for the same funds.

Venues shape the adapters

Adapters translate requests into each venue's protocol and pick a path by instrument and account type. Binance and CommEX connect directly. Spot orders go over WebSocket where that's available and over REST otherwise, and linear futures and Binance's conversion path use REST.

The first time the OMS quotes an account and instrument, it clears old orders. Binance and CommEX accept a cancel-all request, while OKX orders are cancelled one by one.

Our FIX route reaches XTRD through the OnixS engine. Standard XTRD execution accepts market orders because its balance feed doesn't separate locked from free funds, and without that split we can't safely size resting orders.

A separate production-oriented route accepts taker orders for OKX and WhiteBIT and applies its own send limiter.

Tracing one maker order

This trace is illustrative, with synthetic quantities and timings. It follows a market-making bid from a price update to a partial fill, so you can see both choices at work.

From price update to pending replacement

A ticker update moves the best bid. The trackers absorb it, and the market-making strategy asks for a new bid. The OMS finds the current bid outside its tolerance and marks it CANCELING before sending the cancel request.

The replacement is conditional, not automatic. The cancelling bid's funds still count as locked until the exchange confirms. The OMS schedules a new bid only if free quote balance and the rate limiter allow it; otherwise the next reconciliation pass tries again with fresher state.

Two independent replies

Once the replacement passes the limiter and enters the tracker, the adapter sends it, and answers return on two separate channels.

price update ─► trackers ─► strategy: desired bid
                                 │
                                 ▼
                 OMS: local SCHEDULED order created
                                 │
                                 ▼
                          limiter passed
                                 │
                                 ▼
               tracker updated, order timestamped
                                 │
                                 ▼
                     adapter ─► exchange
                                 │
              ┌──────────────────┴──────────────────┐
              ▼                                     ▼
     REST response (OPEN)            private stream (NEW, fills)
              └──────────────────┬──────────────────┘
                                 ▼
               trackers: order and balance state
                                 │
                                 ▼
                       next quote decision

The REST reply and the private-stream confirmation travel independently, and either can arrive first. Because the pending order was recorded at once, every reconciliation in the meantime already treats its funds as committed.

A partial fill changes the remaining capacity

Suppose the new bid is for 10 units and the private stream reports a fill of 4. The fill event updates the tracked order to 6 unfilled units. The account event moves balances:

  • quote currency spent,
  • base asset received,
  • funds for the remaining 6 still locked.

Once both updates have arrived, the trackers show 6 unfilled units and the revised balances. Each reconciliation uses the order and balance state received so far, so one after both updates sees one live order for 6 units, not 10 and not zero. New liquidity is bounded by remaining tracked orders and available funds, so the OMS neither re-places the filled quantity blindly nor commits the same money twice.

If the price moves again, the remainder goes through the same CANCELING path, and any replacement again waits on capacity. What to do with the 4 units just acquired is an economic choice, not an OMS one: our market-making and arbitrage strategies decide how inventory shapes the next quote and whether to hedge.

Measuring three intervals instead of one

A single "order latency" number would blend our own code, the network and the exchange. Our analyzer records order states in HdrHistograms, one per state, so each part of that journey shows up in its own distribution.

Three cumulative intervals

Every interval starts at the same moment: order creation. Each ends when a different state is first observed:

  • creation to SCHEDULED_SENT, covering local scheduling and transmission,
  • creation to OPEN, adding the network round trip and the REST acknowledgement,
  • creation to NEW, adding the network round trip and private-stream confirmation.

These are cumulative intervals from one start, not stages you can add together. The OPEN interval already contains the time spent reaching SCHEDULED_SENT.

How each interval is calculated

The client ID doubles as the creation timestamp, so the analyzer subtracts it from each state's earliest observed time. In our synthetic order, SCHEDULED_SENT arrives 50,000 ns after creation and adds 50 µs to its histogram. OPEN arrives 2,000,000 ns after creation and adds 2,000 µs to its own, a figure that already includes those 50 µs.

NEW adds its own sample whenever the private stream delivers it. By default the analyzer skips the first 10,000 observations as warm-up and excludes market orders. It reports sample counts and percentiles, including the 99.9th and 99.99th, in microseconds.

What the numbers mean

The first interval is mostly ours. The other two include it, but they're dominated by the network and the venue. Separately, JMH microbenchmarks time components such as:

  • signing,
  • rate limiting,
  • pool operations,
  • batches of 200 book updates.

Those benchmarks describe the parts, not the round trip. Neither they nor the histograms turn the 0.1 to 1 ms wire-to-wire target into a single measured figure; they show where time goes along the path.

How the engine got here

The engine continues the path we described in moving from Hummingbot to custom trading, and it moved quickly from adapter integration into a full trading application:

  • May 2022: project start, followed by balances, listen keys and order parsing.
  • July 2022: packaged deployment and fixes from production.
  • August–September 2022: order-sent events and the latency analyzer.
  • January 2023: production deployment hotfixes for balances, reference data and instrument loading.
  • October 2023–March 2024: the OnixS FIX route, hedging and hedge-aware quoting, production configuration and the Chronicle upgrade.

Try the two choices on your own loop

The loop updates state first and records intent ahead of every request, so each acknowledgement, fill and balance change returns to the state that drives the next decision. Cumulative histogram intervals, measured from the shared creation timestamp, keep transmission, acknowledgement and private-stream confirmation apart. Our team kept building on the foundations Chronicle supplies.

If you're building a similar asynchronous system, start with the same two choices. Put each order in your tracker before its request leaves the process, and stamp its creation time where every later state can measure from it. Your reconciliation will then see pending intent while the exchange is silent, and your latency numbers will separate your own time from the network's and the venue's.