Skip to main content

mts1b-oms

Order Management System: state machine, broker routing, fill normalization, position store.

Repo: github.com/MTS1B/mts1b-oms Layer: 5 Depends on: foundation, platform, brokers, riskengine, oms-algos Audience: any service that wants to send an order (research, treasury, tradingview)

What it is

The state machine that owns every order from creation to terminal state. All order I/O — from any source (research strategy, manual override, TradingView webhook, treasury allocator) — goes through OMS.

┌────────────────────────────────────────────────────────┐
│ OrderService (mts1b-oms) │
│ │
┌────────┐ │ ┌───────────┐ ┌─────────────┐ ┌──────────────┐ │ ┌─────────┐
│client │─────▶│ │idempotent │───│riskengine │───│broker │ │ │broker │
└────────┘ │ │dedupe │ │CheckOrder │ │routing │──┼─▶│adapter │
│ └───────────┘ └─────────────┘ └──────────────┘ │ └─────────┘
│ ↓ │ │
│ ┌─────────────┐ │ │
│ │state machine│◀────────fills──────┼───────┘
│ └─────────────┘ │
│ ↓ │
│ ┌─────────────┐ │
│ │position │ │
│ │store │ │
│ └─────────────┘ │
└────────────────────────────────────────────────────────┘

State machine

CREATED ──┐

PENDING_RISK ──── REJECTED (by risk gate)


ACCEPTED ──┐
│ │
▼ ▼
SUBMITTED CANCELED (before submit)

┌────┴────┐
▼ ▼
FILLED PARTIAL
│ │
│ ▼
│ FILLED (or expired)

CLOSED (terminal)

Transitions are owned exclusively by OMS. No other service can mutate Order state. Brokers report fills via stream_fills() and OMS does the state transition + position store update.

API source: services/trading/src/mts/trading/modules/orders/service.py:OrderService.

Module layout

mts1b_oms/
├── service.py # OrderService (orchestrator)
├── state_machine.py # transitions + invariants
├── routing/
│ ├── selector.py # picks broker based on fund + asset class
│ └── splitter.py # splits orders to execalgos when size > threshold
├── positions/
│ └── store.py # Postgres-backed position store, NATS K/V cache
├── fills/
│ └── normalizer.py # broker-native fill → foundation.Fill
├── api/
│ ├── rest.py # FastAPI
│ ├── grpc.py # protobuf interface
│ └── nats.py # subscribes mts.v1.oms.*
└── workers/
├── retry_unfilled.py
└── stale_order_cleanup.py

API

REST (FastAPI)

POST /v1/orders
{
"order_id": "ord-abc",
"idempotency_key": "strat-v7-2026-05-23-AAPL-long",
"symbol": "AAPL",
"side": "buy",
"quantity": "100",
"order_type": "limit",
"limit_price": "180.50",
"tif": "day",
"fund_id": "paper-momentum",
"strategy_id": "momentum_v1",
"broker": "paper",
"actor": "research_signal_executor"
}
→ 202 Accepted { "order_id": "ord-abc", "state": "PENDING_RISK" }

gRPC

service Oms {
rpc Submit(Order) returns (Order);
rpc Cancel(CancelRequest) returns (Order);
rpc GetOrder(OrderId) returns (Order);
rpc GetPositions(FundId) returns (PositionList);
rpc StreamUpdates(FundId) returns (stream OrderUpdate);
}

NATS subjects

  • Subscribes: mts.v1.brokers.<broker>.fills.raw (broker fills come in)
  • Publishes: mts.v1.oms.orders.{created,accepted,submitted,filled,partial,canceled,rejected,closed}

Routing

Per-fund broker is set in FundConfig. OMS picks the broker for each order:

broker_name = order.broker or fund_config.broker
broker = BROKER_REGISTRY[broker_name](**vault_secret(f"mts1b/brokers/{broker_name}"))

If the order is large (configurable threshold), the routing/splitter sends it through mts1b-oms-algos instead of directly to the broker. Example:

if order.quantity * estimated_price > 100_000 and order.order_type == "market":
return await execalgo("vwap", order, params={"horizon_minutes": 30})

Splitter writes child orders back to OMS, each with the parent's strategy_id + a new idempotency_key.

Position store

class Position(BaseModel):
fund_id: str
symbol: Symbol
quantity: Decimal # signed
avg_price: Decimal
market_value_usd: Decimal
unrealized_pl_usd: Decimal
last_updated: datetime
tax_lots: list[TaxLot]

Postgres-backed for durability; NATS K/V for hot reads (mts.v1.oms.positions.<fund_id>.<symbol> snapshot).

Updates atomically on every fill:

async def apply_fill(self, fill: Fill):
async with db.transaction():
pos = await get_position(fill.fund_id, fill.symbol)
new_pos = update_position(pos, fill) # avg-cost or HIFO
await save_position(new_pos)
await save_tax_lot(fill.fund_id, fill)
await publish_position_update(new_pos)

Idempotency

Every order has idempotency_key. OMS checks the recent dedupe table (configurable window, default 5 min). Duplicates return the existing Order without resubmitting.

This means callers can safely retry. Example:

order = Order(idempotency_key=f"strat-{strategy_id}-{asof}-{symbol}-{side}", ...)
# Safe to call repeatedly — only one order is created
result = await oms.submit(order)

Audit

Every state transition writes to the audit chain in mts1b-platform/audit:

order_id=ord-abc | actor=mts1b-oms | action=transition | from=PENDING_RISK | to=ACCEPTED
order_id=ord-abc | actor=mts1b-brokers | action=submitted | broker=paper
order_id=ord-abc | actor=mts1b-brokers | action=filled | fill_id=fill-xyz | qty=100 | price=180.45

Append-only, Merkle-hashed, replayable.

Build + test

pip install -e ".[dev]"
pytest -v

Test coverage targets:

  • State machine: 100% (every transition + every invalid transition)
  • Routing: 100%
  • Position store: 95%+
  • Workers: 80%+ (resilience tests, less code-path-y)

Roadmap

VersionItems
0.1 (Wave 1)State machine, REST + gRPC + NATS interfaces, position store, idempotency
0.2 (Wave 2)OCO + bracket orders
0.3 (Wave 2)Multi-prime / give-up support
0.4 (Wave 3)FIX gateway (community plugin via pluginsdk)
0.5 (Wave 3)Position store split-out if scale demands
1.0 (LTS)Stable interface

See also