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
| Version | Items |
|---|---|
| 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
mts1b-oms-algos— VWAP / TWAP / Almgren-Chrissmts1b-riskengine— synchronous risk gatemts1b-brokers— broker adapters- Concept: Foundation types — Order, Fill, Position schemas