Context
We founded a market-making firm, which meant everything had to be built from the ground up: the engine, the trading algorithms and the infrastructure around them. As co-founder and CTO I designed and wrote the whole system, and it now runs across more than thirty clients in production.
The platform is large. The diagram below shows the whole system and the people who operate it; this case study zooms in on a single part, the core trading engine (the Fortuna brick highlighted in blue below). The observability stack and the AI copilot have their own case studies.
The problem
Market making is unforgiving, and every client arrives with a precise, demanding mandate.
- Liquidity has to be provided 24/7, across many exchanges at once, with capital continuously at risk.
- Each mandate is different: target spread, depth, tokens, venues, and bespoke constraints.
- Requirements are exacting and change often, so the system has to adapt quickly.
- Forking the codebase for every client or strategy would have been impossible to operate or keep correct.
Objectives
- Provide continuous, 24/7 liquidity at a controlled spread across many venues.
- Stand up a new client mandate or strategy fast, without writing new code.
- Reach a wide range of exchanges, centralized and decentralized, behind one interface.
- Guarantee financial-grade correctness and safe, controllable operation.
My approach
I built Fortuna, a base library that treats every strategy as a composition of small, independently tested components. Instead of coding a new bot per client, an operator assembles one from a catalog of 110+ building blocks, declared in a configuration file. The architecture is layered: a thin core runtime, a catalog of pluggable modules, and a unified exchange layer.
The technical solution
Every building block, from an exchange adapter to a full strategy, is a component with an async load / unload lifecycle. At the heart of the framework is a small dependency-injection container: it resolves each component by name to its implementation, discovered as a plugin through standard Python packaging entry points, so new modules, even shipped in a separate package, can be added without touching the core. The container then instantiates the components and wires them together.
That wiring is fully declarative. A whole service is described in a single YAML file: shared variables, then the list of components, each with its type and its arguments. Any argument that points to another component (or to a variable) is resolved recursively, which is exactly how dependencies get injected. A dedicated decimal type keeps monetary values exact from the very first parse, with no floating-point rounding.
# a cross-exchange arbitrage bot, described as data service: "@arbitrage" vars: profit_threshold: !decimal "0.0015" trade_amount: !decimal "0.05" components: binance: id: exchanges.ccxt.binance mexc: id: exchanges.ccxt.mexc exchange_a_adapter: id: arbitrage.adapters.cex args: exchange: "@binance" symbol: "BTC/USDT" exchange_b_adapter: id: arbitrage.adapters.cex args: exchange: "@mexc" symbol: "BTC/USDC" converter: id: arbitrage.converters.simple args: exchange: "@binance" arbitrage: id: arbitrage args: exchange_a: "@exchange_a_adapter" exchange_b: "@exchange_b_adapter" converter: "@converter" threshold: "@profit_threshold" trade_amount: "@trade_amount"
The container turns those references into a wired graph of live components:
The runtime around that graph is built for long-running, around-the-clock operation. When a service starts, every component is loaded in order; on shutdown they are unloaded in reverse, so sessions and connections are torn down cleanly. Strategies run on a tick-based service that executes their logic on a fixed interval, with an interruptible sleep so a stop signal (Ctrl+C or SIGTERM) leads to a clean, graceful shutdown rather than an abrupt kill.
Beyond the core, the value is in the catalog. The same component model is used to build a wide range of trading building blocks, so an operator composes a mandate by picking from them rather than writing code:
- Market making: tick-based bid/ask placement with interchangeable strategies (ladder, band, skewed, price-threshold, and a multi-strategy combiner) and pluggable executors, including a dry-run mode for safe rehearsal.
- Execution algorithms: TWAP, participation-of-volume (POV), and a pegger that keeps a limit order glued to the best bid or ask, with on-chain variants for DEXs.
- Arbitrage: compares two venues through one normalized adapter and trades the spread once it clears a configurable threshold.
- Price providers: a large family of sources, from exchange tickers and on-chain pools (Uniswap v2/v3, PancakeSwap, QuickSwap, Raydium) to aggregators (Jupiter, DexScreener), plus composite providers that combine them (fallback, weighted average, VWAP, cross-rate, or a min/max/median over a window).
- Market-data listeners: periodic fetchers for balances, order books, trades, tickers, OHLCV candles, and open or closed orders.
- Kill switches: first-class safety components, from inventory thresholds to an aggregator that halts trading the moment any condition trips.
- Metrics and sinks: collectors that persist balances, spreads, depth, and fills to PostgreSQL, feeding the observability stack covered in its own case study.
Because money is on the line, correctness is non-negotiable: values are kept as exact decimals from the first parse onward (never floats), structured logging preserves them, and network I/O uses pooled async HTTP sessions tied to each component's lifecycle. Kill switches are first-class components, so risk controls are part of the composition rather than bolted on.
The exchange layer is what keeps strategies venue-agnostic. Centralized exchanges are wrapped over CCXT with arbitrary-precision decimals instead of floats, and around two dozen of them are individually tuned where their APIs misbehave, for example to reconstruct the complete set of closed orders or to work around venue-specific market quirks. Decentralized venues are first-class too: Uniswap v2 and v3 and PancakeSwap on EVM chains, QuickSwap on Polygon, Jupiter on Solana, and xExchange on MultiversX, each quoting or reading pool state directly on-chain. For arbitrage, a centralized and a decentralized venue are collapsed behind one normalized adapter exposing the same quote and trade methods, so a strategy never has to know which kind of market it is touching. Independent calls, whether quotes across venues or writes to several sinks, run concurrently.
Results
- A single engine powers 30+ client mandates in production, running 24/7.
- 100+ venues reachable, centralized and decentralized, through one interface.
- A new client strategy is stood up by writing a configuration file, not new code.
- Financial-grade correctness (Decimal everywhere) and safe operation (lifecycle, kill switches) by construction.
Key takeaways
Treating strategies as declarative compositions of small, tested components turned bespoke client demands into configuration work. That single decision is what let one codebase scale to many clients and venues without fragmenting, and it kept the system correct and operable while it grew.