mts1b-marketdata — public API surface
Adapters implement MarketDataProtocol. Use the registry to switch providers transparently.
Registry
from mts1b_marketdata import PROVIDER_REGISTRY
PROVIDER_REGISTRY
# {"fmp": FmpClient, "polygon": PolygonClient, "finnhub": FinnhubClient,
# "thetadata": ThetaDataClient, "alphavantage": AlphaVantageClient,
# "tiingo": TiingoClient, "yfinance": YfinanceClient}
cls = PROVIDER_REGISTRY["fmp"]
async with cls(api_key="...") as md:
q = await md.get_quote(Symbol("AAPL"))
Multi-source router
from mts1b_marketdata.routing import MultiSourceProvider
md = MultiSourceProvider(
providers=["fmp", "polygon", "alphavantage"],
strategy="failover", # try in order; first success wins
# or "best_quote" — query all; return tightest spread
# or "load_balance" — round-robin within rate limits
# or "cheapest" — pick provider with most free quota left
)
quote = await md.get_quote(Symbol("AAPL"))
The MarketDataProtocol interface
@runtime_checkable
class MarketDataProtocol(Protocol):
@property
def name(self) -> str: ...
async def get_quote(self, symbol: Symbol) -> Quote: ...
async def get_bars(
self, symbol: Symbol, interval: str,
start: datetime, end: datetime | None = None,
) -> list[Bar]: ...
async def get_trades(self, symbol: Symbol, asof: datetime) -> list[Trade]: ...
async def stream_quotes(self, symbols: list[Symbol]) -> AsyncIterator[Quote]: ...
Provider capabilities matrix
| Provider | Bars | Quotes | Trades | Options | Fx | Futures | WS |
|---|---|---|---|---|---|---|---|
fmp | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
polygon | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
finnhub | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
thetadata | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
alphavantage | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
tiingo | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
yfinance | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ |
Common usage
from datetime import datetime, timezone
from mts1b_marketdata import PROVIDER_REGISTRY
from mts1b_foundation.symbology import Symbol
async with PROVIDER_REGISTRY["fmp"](api_key="...") as md:
# Latest quote
q = await md.get_quote(Symbol("AAPL"))
print(q.bid, q.ask)
# Daily bars
bars = await md.get_bars(
Symbol("AAPL"), interval="1d",
start=datetime(2024, 1, 1, tzinfo=timezone.utc),
end=datetime(2024, 6, 1, tzinfo=timezone.utc),
)
print(f"got {len(bars)} bars")
# Intraday
minutes = await md.get_bars(Symbol("AAPL"), interval="1m",
start=datetime(2024, 5, 23, 14, 0, tzinfo=timezone.utc),
end=datetime(2024, 5, 23, 16, 0, tzinfo=timezone.utc))
# Recent trades
trades = await md.get_trades(Symbol("AAPL"),
asof=datetime.now(timezone.utc))
# Live stream (Polygon)
async with PROVIDER_REGISTRY["polygon"](api_key="...") as md:
async for quote in md.stream_quotes([Symbol("AAPL"), Symbol("MSFT")]):
print(quote)
Options chains (ThetaData)
from mts1b_marketdata.thetadata import ThetaDataClient
async with ThetaDataClient(api_key="...") as md:
chain = await md.options_chain(
underlying="SPY",
expiry=date(2026, 8, 23),
)
for opt in chain:
print(opt.strike, opt.option_type, opt.bid, opt.ask)
Caching
The router transparently caches behind Redis:
| Type | TTL |
|---|---|
| Quotes | 1-5s |
| Bars intraday | until next bar close |
| Bars daily | overnight |
| Options chains | 30s |
Cache hits don't consume provider rate-limit budget.
Rate limits per provider
| Provider | Free | Paid |
|---|---|---|
| FMP | 250/day | 30k+/day |
| Polygon | 5/min | unlimited |
| Finnhub | 60/min | 300+/min |
| ThetaData | varies (sub-based) | sub-based |
| AlphaVantage | 25/day | 75-1200/min |
| Tiingo | 50/hr | 500-5000/hr |
| YFinance | unofficial | n/a |
MultiSourceProvider respects each provider's limit via mts1b_platform.ratelimit.
Adding a new provider
Use mts1b-pluginsdk:
mts1b-plugin new --type marketdata --name my-feed
Implement MarketDataProtocol, register via entry points.