Skip to main content

Foundation types

mts1b-foundation is the only repo with zero runtime dependencies other than pydantic and typing-extensions. Every other repo in the ecosystem depends on it. This page explains why and what's inside.

What's in foundation

CategoryContentsAudience
Pydantic modelsOrder, Fill, Position, Quote, Bar, Candle, Trade, MarketSnapshot, RiskEnvelope, FundConfig, ...Every repo
NATS subject schemasmts.orders.*, mts.fills.*, mts.quotes.*, mts.signals.*, mts.risk.*Any repo using the eventbus
OpenAPI specsOrder, Fund, Strategy schemas re-exportable to FastAPIService repos
Python ProtocolsBrokerProtocol, MarketDataProtocol, RiskGate, Sizer, Allocator, SignalAdapter + plugin authors

Why a separate repo?

Without mts1b-foundation:

  • Every service defines its own Order shape. Migrations become "guess what every other service expects."
  • Two services for the same broker (IBKR) end up with subtly different Position shapes — and you find out at runtime.
  • A plugin needs to re-implement BrokerProtocol because the protocol is buried in a service it doesn't want to depend on.

With mts1b-foundation:

  • One Order model, validated once. Type-checkers + pydantic guarantee shape.
  • Cross-repo refactors are gated by CI: change a field, every consumer's CI fails until they update.
  • Plugin authors import the protocol and that's all they need.

Example: the Order type

mts1b_foundation.orders
from datetime import datetime
from decimal import Decimal
from enum import Enum
from typing import Annotated

from pydantic import BaseModel, Field, PositiveInt
from typing_extensions import Self

from mts1b_foundation.symbology import Symbol


class Side(str, Enum):
BUY = "buy"
SELL = "sell"
SHORT = "short"
COVER = "cover"


class OrderType(str, Enum):
MARKET = "market"
LIMIT = "limit"
STOP = "stop"
STOP_LIMIT = "stop_limit"
MARKETABLE_LIMIT = "marketable_limit"


class TimeInForce(str, Enum):
DAY = "day"
GTC = "gtc"
IOC = "ioc"
FOK = "fok"
OPG = "opg"


class Order(BaseModel):
"""A trading order. All currency amounts in account base currency."""

# Identity
order_id: str = Field(..., description="UUID assigned by mts1b-oms")
idempotency_key: str = Field(..., description="Caller-provided dedupe key")

# What
symbol: Symbol
side: Side
quantity: Decimal = Field(..., gt=0)
order_type: OrderType
limit_price: Decimal | None = None
stop_price: Decimal | None = None
tif: TimeInForce = TimeInForce.DAY

# Routing
fund_id: str
strategy_id: str
broker: str = Field(..., description="paper, ibkr, coinbase, ...")
actor: str = Field(..., description="Who/what created this order (audit trail)")

# Optional
extended_hours: bool = False
nav_usd: Decimal | None = None
thesis: str | None = None

# State (mts1b-oms owns transitions)
created_at: datetime
submitted_at: datetime | None = None
accepted_at: datetime | None = None
canceled_at: datetime | None = None
rejected_reason: str | None = None

The model is frozen for the wire format. Mutations happen via model_copy(update=...), never in place. State transitions are owned by mts1b-oms exclusively.

Example: the Broker Protocol

mts1b_foundation.protocols.broker
from typing import Protocol, runtime_checkable
from mts1b_foundation.orders import Order, Fill, Position


@runtime_checkable
class BrokerProtocol(Protocol):
"""Every broker adapter implements this."""

@property
def name(self) -> str: ...

async def submit(self, order: Order) -> Order: ...
async def cancel(self, order_id: str) -> bool: ...
async def get_open_orders(self) -> list[Order]: ...
async def get_positions(self) -> list[Position]: ...
async def stream_fills(self) -> "AsyncIterator[Fill]": ...

The paper broker, IbkrClient, CoinbaseClient, etc. all implement this — and CI verifies via isinstance(adapter, BrokerProtocol) in mts1b-brokers/tests/.

NATS subject conventions

mts.<repo>.<noun>.<verb>[.fund.<fund_id>]

Examples:

SubjectPayloadPublisher
mts.oms.orders.createdOrdermts1b-oms
mts.oms.orders.filledFillmts1b-oms
mts.research.signals.publishedSignalmts1b-research
mts.risk.envelope.updatedRiskEnvelopemts1b-riskengine
mts.marketdata.quotes.<symbol>Quotemts1b-marketdata
mts.treasury.nav.<fund_id>NavSnapshotmts1b-treasury

Schemas are versioned: mts.v2.oms.orders.created. Consumers must declare which versions they understand; the producer emits the highest version supported by all consumers (negotiated at startup).

Versioning + backwards compatibility

ChangeSemVerMigration
Add optional fieldminorNone
Add required fieldmajorProvide a 6-month default + migration shim
Rename fieldmajorKeep alias for 6 months
Tighten validation (e.g. add regex)majorAudit consumer compliance first
Remove fieldmajorRemoved only after the alias period

CI enforces: every repo declares mts1b-foundation>=A.B,<C and contract-tests against that range. If a consumer pins <2.0 and we ship 2.0, the consumer's CI catches it.

What's NOT in foundation

  • Implementations of anything (those go to mts1b-quantkit, mts1b-portfolio, etc.)
  • Logging, HTTP clients, DB pools (those are mts1b-platform)
  • Broker code (those are mts1b-brokers)
  • Anything that imports a network library

The rule: if it has runtime side effects, it's not foundation. Foundation is types + schemas + protocols, nothing else.

Local development

pip install -e ".[dev]" mts1b-foundation
python -c "from mts1b_foundation.orders import Order; print(Order.model_json_schema())"

The full JSON-Schema export feeds the OpenAPI generation in every service that exposes a REST API.

Where to next