Architecture
OpenOMS is one program. Its core owns all state in Postgres. Two gateways inside the same program are its only contact with the outside world: one for orders and fills, one for prices.
- an order going out
- order and fill events
- prices
- strategies
- kill and limits
The order path
Section titled “The order path”- An order arrives from the cockpit, the trader UI, or a strategy over FIX or REST (path ①).
- Order API + Risk checks it against the portfolio’s limits and current positions, then stores it.
- The order gateway sends it to the venue through that venue’s connector.
- The venue’s executions come back through the same connector as plain
ExecEvents. - The order engine applies them. It is the only place that books a fill, and it books each fill once, however often it is reported.
- Every change is appended to the event store, then published on the event bus.
Order recon asks the gateway for open orders and order status after every reconnect, and lets the order engine heal anything missed while a stream was down.
The price path
Section titled “The price path”- The market data gateway subscribes to the instruments the OMS holds, on the crypto venues’ public feeds and on OPRA for options.
- It turns each venue’s messages into one shared quote.
- Pricing keeps the best live source per instrument and fails over when it goes stale.
Prices never wait for orders and orders never wait for prices: the two paths share no queue.
The event bus
Section titled “The event bus”The bus is in memory and has two lanes:
| Lane | Carries | Guarantee |
|---|---|---|
| Order lane | order events, fills, position changes | published only after the event is stored; a subscriber that falls behind re-reads the event store |
| Price lane | marks | latest value wins; a slow subscriber skips ahead and slows nothing |
Positions & P&L, UI updates (one WebSocket to every open screen) and the strategy reports subscribe to it. The Kafka outbox reads the event store rather than the bus, so other systems catch up after an outage. Kafka is optional; without it the OMS runs fully.
Strategies
Section titled “Strategies”-
Path ①, through the OMS. The strategy sends its orders to the OMS over FIX 4.4 (TLS) or REST, and every order passes the same checks as a trader’s. See FIX and REST.
-
Path ②, co-located. A latency-sensitive strategy runs on its own server next to the venue and trades its own venue account directly. It links three crates from this repository:
colo-riskchecks every order in memory;colo-linkholds the control link and stops the strategy when the OMS goes quiet;gatewayprovides the venue connector.
The OMS follows that account with its own key and books every fill, so the strategy’s positions and P&L appear moments later. Kill tells the strategy, and also cancels everything on the account with the OMS’s key, without waiting for the strategy. See Co-located strategies.
Instrument model
Section titled “Instrument model”One canonical instrument catalog sits in the middle, with two independent
mappings off it: a broker’s tradable handle (broker_instrument, a table) and a
data feed’s pricing symbol (FeedSymbology, derived in code). Whether an
instrument can be priced and whether it can be traded are independent.
| What it is | |
|---|---|
Master — instrument |
Canonical identity, keyed (symbol, venue). Asset and instrument class, microstructure, FIGI. Option legs live in instrument_derivative. |
Execution — broker_instrument |
The tradable mapping: one row per instrument per broker, with broker_symbol, native_id and limits. Written by oms setup sync-broker from the broker adapter’s instrument list. A row existing means the instrument is tradable through that broker. |
Market data — FeedSymbology |
The pricing mapping, one-to-many — and a pure function, not a table. candidates() picks the slice of the catalog a feed covers; to_feed_symbol() renames each hit into the feed’s own symbol. One symbol can price the same pair on several venues. Evaluated at subscribe time. |
End to end
Section titled “End to end”One catalog in the middle, an adapter at each end. Brokers write it:
sync-broker asks each broker what it lists and stores that as an instrument
plus a broker_instrument routing row. Feeds only read it: at subscribe
time, FeedSymbology.candidates() picks the slice of the catalog that feed
covers and to_feed_symbol() renames each instrument into whatever that feed
calls it.
That second mapping is computed, not stored — there is no feed table anywhere. It is rebuilt from the catalog on every subscription, which is why a broker sync that adds instruments needs no follow-up step to make them priceable, and why nothing can go stale. Each adapter owns its own symbology, so neither end knows the other exists.
flowchart LR
ALP["Alpaca<br/>equities · options"]:::exec
BNB["Binance<br/>crypto spot"]:::exec
IP["InstrumentProvider<br/>list_instruments()"]:::exec
subgraph STORE["the catalog — two Postgres tables, survive restarts"]
direction TB
INST[("instrument<br/><b>symbol @ venue</b><br/>what exists, one row each")]:::core
BI[("broker_instrument<br/>how to route it,<br/>one row per broker")]:::exec
end
subgraph MEM["no table — recomputed on every subscribe"]
FS["FeedSymbology<br/>candidates() picks the slice<br/>to_feed_symbol() renames it"]:::feed
end
DBN["Databento<br/>OPRA options"]:::feed
BNF["Binance<br/>spot book"]:::feed
BYF["Bybit<br/>spot book"]:::feed
ALP --> IP
BNB --> IP
IP -- "sync-broker writes" --> INST
IP -- "writes" --> BI
INST -. "1:n" .- BI
INST -- "read per subscribe" --> FS
FS -- "feed's own symbols" --> DBN
FS --> BNF
FS --> BYF
Tables and relationships
Section titled “Tables and relationships”Foreign keys are shown. broker_instrument references the master
instrument. There is no feed table: the pricing mapping is derived in code.
Two links are soft, by code rather than foreign key: broker_code →
broker_connection, and feed_code, which is ranked for failover in
provider_feed_policy.
erDiagram
VENUE ||--o{ INSTRUMENT : "lists (MIC)"
CURRENCY ||--o{ INSTRUMENT : "quoted in"
INSTRUMENT ||--o| INSTRUMENT_DERIVATIVE : "option legs"
INSTRUMENT ||--o{ BROKER_INSTRUMENT : "tradable via"
BROKER_CONNECTION ||--o{ ACCOUNT : "routing target"
INSTRUMENT {
bigint id PK
text symbol "UNIQUE(symbol,venue)"
text venue FK
text currency FK
text asset_class
text instrument_class
text figi
}
INSTRUMENT_DERIVATIVE {
bigint instrument_id PK,FK
text underlying_symbol
text option_kind
numeric strike_price
date expiry_date
}
BROKER_INSTRUMENT {
bigint instrument_id FK
text broker_code "ALPACA|BINANCE"
text broker_symbol
text native_id
bool is_tradeable
numeric min_quantity
}
VENUE { text code PK "XNAS, OPRA, BINANCE" }
CURRENCY { text code PK "USD, USDT" }
BROKER_CONNECTION {
text code PK
text broker_code
text environment "PAPER|LIVE"
}
ACCOUNT { text code PK }
Runtime — the two paths
Section titled “Runtime — the two paths”Pricing derives its symbols through FeedSymbology; order routing reads
broker_instrument. The two never cross — a feed can price an instrument no
broker trades, and a broker can trade an instrument no feed prices.
flowchart TB
subgraph PRICE["Pricing path (market data)"]
direction LR
LF["Live feeds<br/>Databento · Binance · Bybit"]:::feed
QF["subscribe held<br/>via FeedSymbology"]:::feed
MR["mark_router<br/>ranked by provider_feed_policy"]:::feed
MS[("MarkStore")]:::feed
PL["positions · M2M P/L"]:::core
LF --> QF --> MR --> MS --> PL
end
subgraph EXEC["Execution path (orders)"]
direction LR
ORD["Order"]:::core
ACC["account -> broker_connection<br/>(broker_code, env)"]:::exec
BIx["broker_instrument<br/>broker_symbol / native_id"]:::exec
REG["BrokerRegistry adapter"]:::exec
API["Broker API"]:::exec
ORD --> ACC --> REG
ORD -. "resolve handle" .-> BIx --> REG --> API
end
See also the API reference.