Historical tick-by-tick L2 orderbook reconstruction and backtesting primitives for Polymarket. Spread, impact, imbalance, and every trade, delivered as a DataFrame.
Project description
MarketLens Python SDK
Historical and real-time prediction market data — full L2 orderbook reconstruction, microstructure analytics, and backtesting primitives for Polymarket.
pip install marketlens
from marketlens import MarketLens
client = MarketLens(api_key="mk_...") # or set MARKETLENS_API_KEY env var
Order Book Replay
Replay full L2 book state for any market. Each tick yields (Market, OrderBook) — one line to go from market ID to book-level analysis.
for market, book in client.orderbook.walk(market_id, after=start, before=end):
print(f"mid={book.midpoint} spread={book.spread_bps():.0f}bps")
# Or as a DataFrame
df = client.orderbook.walk(market_id, after=start, before=end).to_dataframe()
Rolling Series
Walk every market in a rolling series chronologically — same orderbook.walk() interface, just pass a series slug instead of a market ID.
for market, book in client.orderbook.walk("btc-up-or-down-5m", status="resolved"):
if (spread := book.spread_bps()) and spread < 200:
entry = book.impact("BUY", "100")
Structured Products
Walk a structured product series (multi-strikes, neg-risk, barrier). All sibling strike markets are replayed in parallel — walk.books always holds the latest book for every strike, and walk.surface() fits the implied probability distribution at every tick.
walk = client.orderbook.walk("btc-multi-strikes-weekly")
for market, book in walk:
surface = walk.surface()
if not surface:
continue
strikes = surface.survival_strikes()
curve = " ".join(f"${s.strike:,.0f}={s.fitted_prob:.3f}" for s in strikes)
print(f"[{walk.event.title}] mean=${float(surface.implied_mean):,.0f} {curve}")
Walk properties available during iteration:
| Property | Description |
|---|---|
walk.books |
{market_id: OrderBook} — latest book for every sibling strike |
walk.markets |
{market_id: Market} — all strike markets in the current event |
walk.event |
Current Event (transitions automatically between events) |
walk.series |
Resolved Series |
walk.surface() |
Surface fitted from current book midpoints (same format as API) |
Backtesting
Define a strategy by subclassing Strategy and implementing event hooks. Run it against any market, rolling series, structured product, or multiple series at once — the engine replays L2 book data tick-by-tick with realistic execution simulation.
from marketlens.backtest import Strategy
class BuyOnTightSpread(Strategy):
def on_book(self, ctx, market, book):
if ctx.position().side == "FLAT" and book.spread_bps() and book.spread_bps() < 200:
ctx.buy_yes(size="100")
# Single series
result = client.backtest(BuyOnTightSpread(), "btc-up-or-down-5m",
initial_cash="10000.0000",
after="2026-03-05T10:00Z", before="2026-03-05T10:05Z")
# Multi-series portfolio — shared capital across assets
result = client.backtest(BuyOnTightSpread(),
["btc-up-or-down-5m", "eth-up-or-down-5m", "sol-up-or-down-5m"],
initial_cash="10000.0000",
after="2026-03-05T10:00Z", before="2026-03-05T10:30Z")
result.trades_df() # per-fill DataFrame
result.settlements_df() # per-market settlement P&L
result.by_series() # per-series PnL attribution
result.equity_df() # equity curve over time
Strategy hooks
| Hook | Called when |
|---|---|
on_book(ctx, market, book) |
Every book state change (snapshot or delta) |
on_trade(ctx, market, book, trade) |
Every historical trade |
on_fill(ctx, market, fill) |
Your order is filled |
on_market_start(ctx, market, book) |
A new market begins in the walk |
on_market_end(ctx, market) |
A market's data is exhausted, before settlement |
For structured products and multi-market backtests, ctx.books gives the latest book for every active market — the same cross-market view as walk.books.
For markets with a crypto underlying, ctx.reference_price() returns the Binance spot price at the current tick — useful for computing moneyness, basis, or filtering by distance-to-strike.
Execution realism
| Parameter | Default | Description |
|---|---|---|
initial_cash |
required | Starting capital (e.g. "10000.0000") — buy orders exceeding cash are cancelled |
latency_ms |
50 |
Order-to-fill delay — orders fill against the book state N ms after submission |
queue_position |
False |
CLOB-realistic queue modeling — tracks each order's position in the book and fills only when queue-ahead is fully drained by trades and cancellations |
limit_fill_rate |
0.1 |
Flat fraction of trade size that fills your limit order. Ignored when queue_position=True |
slippage_bps |
0 |
Extra price penalty on market order fills (on top of L2 book walk) |
fees |
"polymarket" |
Fee model — auto-detects per category (crypto vs sports). Set to None for zero fees |
max_fill_fraction |
1.0 |
Max fraction of each book level consumed per order |
include_trades |
True |
Fetch trade data (required for limit order fills and on_trade) |
For full control, use BacktestEngine with BacktestConfig directly.
Implied Probability Surfaces
Implied distributions extracted from multi-strike prediction markets — survival curves, density functions, and barrier probabilities, fitted via isotonic regression. Updated every 5 minutes via the API, or recomputed at every tick via walk.surface().
for surface in client.signals.surfaces(underlying="BTC"):
if surface.surface_type == "survival":
for s in surface.survival_strikes():
print(f" K={s.strike:>10,.0f} P(above)={s.fitted_prob:.3f}")
elif surface.surface_type == "density":
for b in surface.density_buckets():
print(f" ${b.lower:,.0f}-${b.upper:,.0f} p={b.normalized_prob:.3f}")
elif surface.surface_type == "barrier":
for b in surface.barrier_strikes():
print(f" {b.direction} ${b.strike:,.0f} P={b.fitted_prob:.3f}")
| Type | Source | Fitting | Stats |
|---|---|---|---|
survival |
Multi-strike "above $X" markets | PAVA monotone decreasing | implied_mean, implied_cv, implied_skew |
density |
Neg-risk range + tail markets | Normalized probabilities | implied_mean, implied_cv, implied_skew |
barrier |
Hit-price reach/dip markets | PAVA per direction | implied_peak, implied_peak_cv, implied_trough, implied_trough_cv |
Reference Prices
Binance spot prices for crypto underlyings (BTC, ETH, SOL, XRP, BNB, DOGE, LINK, HYPE, ENA) at 1-second resolution. Markets with a recognized underlying expose it via market.underlying.
# Direct access
for candle in client.reference.candles("BTC", after=start, before=end):
print(candle.timestamp, candle.close)
# In a backtest — spot price at the current tick
class MyStrategy(Strategy):
def on_book(self, ctx, market, book):
spot = ctx.reference_price() # Binance close for market's underlying
OrderBook Analytics
Every OrderBook — live snapshot or replayed — carries the same analytical methods:
book = client.orderbook.get(market_id)
book.microprice() # size-weighted mid from best level
book.weighted_midpoint(n=3) # n-level weighted mid
book.spread_bps() # spread in basis points
book.imbalance() # full-book bid/ask imbalance [-1, 1]
book.imbalance(levels=3) # top-of-book imbalance
book.impact("BUY", "1000") # VWAP execution price for $1k market buy
book.slippage("BUY", "1000") # slippage from mid for $1k order
book.depth_within("0.02") # (bid_depth, ask_depth) within 2c of mid
Resources
| Namespace | Methods |
|---|---|
client.markets |
list() get() trades() candles() |
client.events |
list() get() markets() |
client.series |
list() get() markets() walk() events() |
client.orderbook |
get() history() metrics() walk() |
client.signals |
surfaces() surface() history() |
client.reference |
candles() |
client.exports |
download() download_range() |
All list methods return auto-paginating iterators with .to_list() and .to_dataframe().
df = client.markets.candles(market_id, resolution="1h").to_dataframe()
trades = client.markets.trades(market_id, after=start, before=end).to_list()
top = client.markets.list(status="active", sort="-liquidity", limit=5).first_page()
Bulk Data Export
Download full-day Parquet exports of order book snapshots and deltas — one file per market per day, no pagination required.
# Single day
path = client.exports.download(market_id, table="deltas", date="2026-03-07")
# Date range
paths = client.exports.download_range(
market_id, table="snapshots", after="2026-03-01", before="2026-03-08",
)
Files are saved to the current directory by default. Pass path="./data" to choose a destination. Returns Path objects pointing to the downloaded .parquet files.
| Parameter | Description |
|---|---|
table |
"snapshots" or "deltas" |
date |
YYYY-MM-DD (must be before today) |
after / before |
Date range — inclusive start, exclusive end |
path |
Output directory (default: .) |
Async
Every resource, iterator, and replay helper has an async counterpart.
from marketlens import AsyncMarketLens
async with AsyncMarketLens() as client:
async for market, book in await client.orderbook.walk(market_id, after=start, before=end):
print(book.microprice(), book.imbalance(levels=3))
Examples
| Example | Description |
|---|---|
execution_cost.py |
Live book depth, spread, impact and slippage across order sizes |
microstructure.py |
Rolling series feature matrix — does imbalance predict outcome? |
implied_surfaces.py |
Implied probability surfaces — survival, density, and barrier |
event_strikes.py |
Structured product walk — parallel books with live surface fitting |
backtest_basic.py |
Single-series backtest — spread-timing strategy with settlement |
backtest_limit_orders.py |
Market-making with limit orders and on_fill exit |
backtest_surface.py |
Surface mispricing — PAVA regression with spot-distance filter via reference prices |
backtest_portfolio.py |
Multi-series portfolio — imbalance strategy across three assets |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file marketlens-0.5.0.tar.gz.
File metadata
- Download URL: marketlens-0.5.0.tar.gz
- Upload date:
- Size: 63.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01b721efbbc8292b380c1f958bc00aff37d8ed8fe4b52eb3a45ebe8ef63e00aa
|
|
| MD5 |
2a1a769b3b4924a782f73b82404e93df
|
|
| BLAKE2b-256 |
31ececcd2cdc9c92abc7db18dde6fc4095186947d6216c7c1e225400ead0d6d1
|
File details
Details for the file marketlens-0.5.0-py3-none-any.whl.
File metadata
- Download URL: marketlens-0.5.0-py3-none-any.whl
- Upload date:
- Size: 54.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3091c4d5d3d16b724f9ec2ad6abc4624d6964f66a09d2491185c7352d9fcdb9
|
|
| MD5 |
2dc07c9633ce3bffabf98dd35c719c5e
|
|
| BLAKE2b-256 |
e9615717b361352f6561f22d509bd5b07e37ecf99c2a4081f690276953710d06
|