Skip to content

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.

OpenOMS architectureFrom top to bottom: users and strategies; the OMS core with the order engine, event store and event bus; the order and market data gateways; the brokers, crypto venues and data vendors they connect to. At the bottom, a co-located strategy trades a crypto venue directly while the OMS follows its account and controls it over the control link.ONE OMS PROGRAM · one binary, one processOMS CORE · owns all stateGATEWAYS · no database, no KafkaUSERSOUTSIDE② CO-LOCATED SERVER · a separate program, next to the venueNewOrder · CancelExecEventKafkafollowed fillsQuotedirect orderscontrol link · kill · limits · positionsCockpitadmin, risk, blotterTrader UItickets, positionsStrategies ①FIX 4.4 over TLS, or RESTOther systemsrisk, accounting…Order API + Riskchecks, then storesOrder enginethe only place fills bookEvent storeappend-only, PostgresOrder reconasks the gateway on reconnectKafka outboxoptional · from the storeEVENT BUS · in memoryorder lane · after commitprice lane · latest winsStrategy registrylimits · Kill · EnablePositions & P&Lfrom fills and marksPricingbest live source → markCo-located hubbooks fills · link · alarmsUI updatesbus → open screens, liveOrder gatewayorders and fills only · sessions, reconnect, healthAlpacaIBKR · FIXBinanceBybitOKXCoinbasefollow streams · co-located accountsMarket data gatewayprices only · instruments the OMS holdsBinanceBybitOKXCoinbaseOPRA · US optionsBrokersIBKR · AlpacaCrypto venuesBinance · Bybit · OKX · Coinbase · orders and pricesData vendorsOPRA · pricesStrategy programmarket maker, arbitrage…never waits on the OMScolo-risklimits · orders/sec · haltcolo-linkdead man's switchgatewayvenue connector, no FIXCrates from this repository, linked into the program. It trades its own venue account;the OMS follows that account with its own key, books every fill, and can kill it unaided.
  • an order going out
  • order and fill events
  • prices
  • strategies
  • kill and limits
  1. An order arrives from the cockpit, the trader UI, or a strategy over FIX or REST (path ①).
  2. Order API + Risk checks it against the portfolio’s limits and current positions, then stores it.
  3. The order gateway sends it to the venue through that venue’s connector.
  4. The venue’s executions come back through the same connector as plain ExecEvents.
  5. 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.
  6. 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 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 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.

  • 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-risk checks every order in memory;
    • colo-link holds the control link and stops the strategy when the OMS goes quiet;
    • gateway provides 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.

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.

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

Brokers write the catalog; feeds read it and rename in memory.

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 }
Tables and foreign keys, with instrument at the centre.

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
The pricing path and the execution path never cross.

See also the API reference.