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 models —
Order,Fill,Position,Quote,Bar,Trade,MarketSnapshot,RiskEnvelope,FundConfig,Signal, ... - Python Protocols —
BrokerProtocol,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
Positionmodel. 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:
| Change | SemVer | Migration |
|---|---|---|
| Add optional field | minor | None |
| Add required field | major | 6-month default + migration shim |
| Rename field | major | Alias kept for 6 months |
| Tighten validation | major | Audit consumers first |
| Remove field | major | Only 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:
- Producer starts up, queries consumer manifests on
mts.meta.consumers. - Picks highest version supported by all consumers.
- 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 --strictrufflint + format- License audit (
pip-licenses --fail-on=GPL-3.0;AGPL-3.0) - Zero-deps check (the
pip-compilecount above) - Backwards-compat check (JSON-Schema diff against previous tag)
Roadmap
| Version | Items |
|---|---|
| 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 |