Skip to main content

mts1b-foundation

Pure types + schemas. Zero runtime deps. The foundation every MTS1B repo depends on.

Repo: github.com/MTS1B/mts1b-foundation (currently private) Layer: 0 (the bottom) Depends on: pydantic, typing-extensions — nothing else Audience: every other repo, plus plugin authors

What it is

A tiny library of:

  • Pydantic modelsOrder, Fill, Position, Quote, Bar, Trade, MarketSnapshot, RiskEnvelope, FundConfig, Signal, ...
  • Python ProtocolsBrokerProtocol, MarketDataProtocol, RiskGate, Sizer, Allocator, FactorFn
  • NATS subject schemas — typed wrappers for mts.v1.<repo>.<noun>.<verb>
  • OpenAPI exports — JSON-Schema for every model, re-exportable to FastAPI

That's it. No I/O, no network, no DB, no logging. If it has side effects, it's not foundation.

Why one type repo

The single biggest problem in the source monorepo was type drift across services. Two services for the same broker (IBKR) had subtly different Position shapes. We discovered this at runtime.

With foundation:

  • One Position model. Validated by pydantic. Type-checked by mypy.
  • Cross-repo refactors are gated: change a field, every consumer's CI fails until they update.
  • Plugins import the Protocol they care about and don't drag in a service dep.

Module layout

mts1b_foundation/
├── __init__.py
├── orders.py # Order, Fill, OrderType, Side, TimeInForce
├── positions.py # Position, PositionLot, TaxLot
├── market_data.py # Quote, Bar, Trade, MarketSnapshot, UniversePanel
├── signals.py # Signal, TargetWeight
├── risk.py # RiskEnvelope, OrderRejection, HaltRequest
├── funds.py # FundConfig, NavSnapshot
├── symbology.py # Symbol, AssetClass, Venue, Currency
├── time.py # Session, MarketCalendar, TimeInForce
├── nats.py # subject helpers + version negotiation
├── protocols/
│ ├── broker.py # BrokerProtocol
│ ├── marketdata.py # MarketDataProtocol
│ ├── risk.py # RiskGate
│ ├── portfolio.py # Sizer, Allocator
│ └── factors.py # FactorFn
└── openapi.py # JSON-Schema export helpers

Top APIs

Order and Fill

class Order(BaseModel):
order_id: str
idempotency_key: str
symbol: Symbol
side: Side
quantity: Decimal
order_type: OrderType
limit_price: Decimal | None
stop_price: Decimal | None
tif: TimeInForce
fund_id: str
strategy_id: str
broker: str
actor: str
extended_hours: bool
nav_usd: Decimal | None
thesis: str | None
created_at: datetime
submitted_at: datetime | None
accepted_at: datetime | None
canceled_at: datetime | None
rejected_reason: str | None


class Fill(BaseModel):
fill_id: str
order_id: str
symbol: Symbol
side: Side
quantity: Decimal
price: Decimal
fees: Decimal
venue: str
timestamp: datetime
raw: dict # original broker payload

BrokerProtocol

@runtime_checkable
class BrokerProtocol(Protocol):
@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]: ...

Every broker adapter (mts1b-brokers) implements this. CI verifies via isinstance(adapter, BrokerProtocol).

RiskEnvelope

class RiskEnvelope(BaseModel):
envelope_id: str
fund_id: str
nav_usd: Decimal
max_gross_exposure_pct: float
max_position_pct: float
daily_loss_halt_pct: float
enable_shorting: bool
allowed_brokers: list[str]
allowed_order_types: list[OrderType]
# ... see concepts/risk-envelopes for full schema

UniversePanel

@dataclass
class UniversePanel:
close: np.ndarray # (T, A)
high: np.ndarray | None
low: np.ndarray | None
open: np.ndarray | None
volume: np.ndarray | None
dates: np.ndarray # (T,) datetime64[D]
symbols: list[str] # (A,)
asset_class: str
market_cap: np.ndarray | None
sector: np.ndarray | None
country: np.ndarray | None

Universal panel shape across mts1b-research, mts1b-quantkit, mts1b-GPUbacktester.

Versioning

Foundation follows SemVer strictly:

ChangeSemVerMigration
Add optional fieldminorNone
Add required fieldmajor6-month default + migration shim
Rename fieldmajorAlias kept for 6 months
Tighten validationmajorAudit consumers first
Remove fieldmajorOnly after alias period

Consumers pin a range: mts1b-foundation>=1.0,<2.0. CI enforces compatibility.

NATS schema versioning

Subjects are versioned in the path:

mts.v1.oms.orders.created ← v1 schema
mts.v2.oms.orders.created ← v2 schema (parallel; producers emit both during transition)

The negotiation protocol:

  1. Producer starts up, queries consumer manifests on mts.meta.consumers.
  2. Picks highest version supported by all consumers.
  3. Emits on that subject.

This means rolling upgrades work: spin up v2 services, wait for all to be online, retire v1 subjects.

Build + test

git clone https://github.com/MTS1B/mts1b-foundation
cd mts1b-foundation
pip install -e ".[dev]"

pytest # full test suite
pytest --cov=src # with coverage
mypy --strict src/ # type check
ruff check . && ruff format --check .

# Verify zero runtime deps
pip-compile --strict pyproject.toml | wc -l
# → ≤ 2 (pydantic + typing-extensions)

CI gates

Every PR runs:

  • pytest (target: 100% coverage on this repo)
  • mypy --strict
  • ruff lint + format
  • License audit (pip-licenses --fail-on=GPL-3.0;AGPL-3.0)
  • Zero-deps check (the pip-compile count above)
  • Backwards-compat check (JSON-Schema diff against previous tag)

Roadmap

VersionItems
0.1 (Wave 1)Core models: Order, Fill, Position, Quote, Bar, Signal, RiskEnvelope, FundConfig + 5 protocols
0.2 (Wave 2)AltData, MacroData, CryptoData schemas
0.3 (Wave 2)Treasury (NavSnapshot, TransferRequest), Operations (HaltRequest, AuditEntry)
1.0 (LTS, month 18)Frozen API; all subsequent changes opt-in via SemVer minor