If you've built a market maker on a framework and kept adding your own pricing, this may sound familiar. Your quotes come to depend on fair prices from other venues or your own models, on reactions to individual book changes, and on hedges placed on other exchanges. All three needs run through the loop that joins market data, account state and quote replacement.
That's where we ended up after starting with Hummingbot on Binance, and it's why we decided to own that loop. We built it as an event-driven Python runtime on asyncio and CCXT Pro. Below, I explain why the framework stopped fitting, how the runtime is organized, and how one external price update replaces a live quote. This piece belongs to our technology series on the software behind our trading.
We started with Hummingbot, and it worked
Hummingbot gave us a working market maker. Its pure market-making strategy quotes both sides of a book, and its WebSocket connector architecture handled exchange plumbing we didn't want to write ourselves. We still use it for some tasks.
The first changes were small. We tuned parameters, then added models of price differences between venues, then fed in fair prices computed by our own Python code. In effect, the quoting bot became a test bench for our pricing ideas.
I walked through that progression in our August 2021 conversation with the Hummingbot team, which they featured in their September 2021 newsletter. Every useful idea pushed our strategies further from the framework's defaults.
Three requirements that land in the core loop
You'll probably recognize at least one of these. Three needs kept coming up for us:
- External pricing. Our fair price often comes from another venue's book or a model, not from the local midpoint.
- Faster updates. The Hummingbot version we ran then refreshed strategies on a one-second cycle. We wanted to react to individual book changes.
- Hedge coordination. Positions opened on one exchange get hedged on others, and the economics depend on rebates and typical spreads.
Each one can be patched in on its own, and we discussed the update cycle with the Hummingbot team, as you can hear in the full interview. Together, though, they change what the core loop has to do. A new external price may call for replacing quotes, while a fill or cancellation changes the funds every other order and hedge can draw on.
Why we owned the loop instead of patching it
Those events arrive asynchronously and in no fixed order. The core loop becomes a set of reactions to market and account events over shared state, rather than a strategy refreshed on a cycle. That loop decides what we quote and when, so we built it ourselves.
Owning it gave us a few practical things we'd otherwise have bolted on piece by piece:
- a single readiness gate that blocks quoting until every required input exists;
- order sizes tied to real balances, coordinated across asynchronous fills and cancellations;
- common objects across venues, so strategy logic doesn't change per exchange;
- one replacement path that new pricing inputs can reuse.
Each of these is a direct answer to one of the three requirements above. The next section shows how they work.
How our runtime is organized
The system is plain Python with asyncio. Strategies, exchange adapters and order tracking are separate components that talk through a shared set of common objects, so a strategy never has to speak a venue's protocol directly.
Lifecycle, events and housekeeping
Every strategy inherits one asyncio lifecycle. It picks the configured exchange implementation, then runs three separate stages:
- setup;
- processing;
- termination.
Each strategy has its own handler for stream errors, so a broken feed doesn't silently stall quoting. A one-asset strategy subscribes to book, balance and order events. On each book update it refreshes its local copy, can strip out our own visible orders, and ignores configured "dust" levels too small to matter before choosing prices.
This isn't a timer-free engine. A periodic step still runs alongside the callbacks for housekeeping, at a configurable interval. Events drive quoting; the timer keeps things tidy.
Readiness before quoting
A strategy won't quote until every input it needs exists. The readiness check requires:
- completed setup;
- balances;
- a local order book;
- instrument metadata, such as tick size and minimums;
- a reference price.
The external-price strategy adds its external book and an external bid reference. The gate checks that these inputs have arrived, not that they're fresh. Callbacks keep balances, books and orders updated; the gate stops the strategy from quoting before those inputs exist.
Prices and quantities are Decimals checked against instrument precision and exchange minimums, because a sensible quote that breaks venue rules simply gets rejected.
Sizing from balances, not fixed amounts
Pure market making computes buy and sell quantities from available or total balances, capped by a configured fraction of capital and an absolute maximum order size. Quotes are tied to the money we actually have, not to a fixed number in a config file.
Balance callbacks clear side-specific "waiting" flags before the strategy places more orders on that side. Fills and cancellations arrive asynchronously, and this coordination helps keep the same funds from backing two orders at once. That matters even more once hedges on other venues draw on the same accounts.
Exchange adapters
CCXT Pro provides the WebSocket and API layer. Our adapters around it turn venue-specific data into common objects, and they cover:
- Binance spot, USD-margined futures and coin-margined futures;
- Binance Swap;
- Ascendex;
- Kraken and Bybit.
On Binance, the stream adapter converts CCXT Pro book updates into our own order-book events. Placement validates notional, price and status, then maps the order onto spot, margin or futures endpoint parameters.
Common objects and local order state
Every component works with the same four kinds of object:
- order books;
- balances;
- instruments;
- orders.
An order manager stores our common order objects and answers questions such as "which sides are active?" from their statuses. A successful response updates the exchange ID, status and timestamp.
A failure comes back as a common error object carrying the affected order. Strategies receive errors through the same interface on every venue, although what a particular error means, and how to recover from it, can still differ between venues.
One external price update, step by step
Here's the core mechanism in a single quote replacement. The numbers are illustrative, not production settings, and the optional limit-price, jumping and placement-optimization adjustments are switched off for this example.
The pricing rule
The external-price strategy opens a second exchange connection and subscribes to its book. Each update gets dust-filtered, and the new best prices replace the stored ones only when either moves by more than a configured tolerance. Small wiggles don't churn our orders.
The strategy then picks reference prices. With external enforcement off, the bid uses the lower of the local and external references, and the ask uses the higher. Both quotes sit on the conservative side of either venue. With enforcement on, it uses the external reference directly.
The numbers
Say our stored local references are 100.20 bid and 100.40 ask. A qualifying external update arrives with 100.00 bid and 100.60 ask. The lower bid is 100.00 and the higher ask is 100.60, so those become our references.
With a 0.1% bid spread and a 0.2% ask spread:
- bid: 100.00 × 0.999 = 99.90
- ask: 100.60 × 1.002 = 100.8012
Both prices then get rounded to the instrument's tick size. The external change passed the tolerance check and the readiness gate has all its inputs, so the strategy schedules a forced replacement of its existing orders at the new prices.
The flow
external book ──► dust filter ──► tolerance check ──► readiness gate
│
local book ──► lower bid / higher ask ◄───────────────────┘
│
▼
spreads + balance-limited size ──► forced replace
│
▼
exchange adapter ──► order manager ◄── balance + order events
One external price update: filtered and checked, combined with the local book, priced and sized, then sent through the adapter while account events update local state.
What the separation buys you
We keep strategy intent, exchange protocol handling and local order state in separate places. Common state and price objects let a strategy respond the same way while exchange protocols and the timing of asynchronous updates differ from venue to venue.
The external-price strategy shows what that boundary buys. Its second book arrives as another stream, but the reference it produces passes through the same readiness gate, sizing and replacement path as a local price. A new pricing input can reuse that path instead of building its own route to the exchange, though, like the external book, it may bring readiness conditions of its own.
Python gives us one language for strategy callbacks, API calls and streamed events, alongside our research code. Your pricing models and your execution code can live in the same place.
Where to go from here
If your framework is collecting patches for external pricing, faster reactions or hedging, check whether those patches all touch the same loop. If they do, that loop is the piece worth owning, and the readiness inputs your strategies can't quote without are a good place to start designing it.
To hear how our own progression unfolded, listen to the interview with the Hummingbot team. Lower-latency execution design is a separate topic, covered in our execution engine article.