Python SDK for Polymarket order book data and backtesting. Tick-level L2 snapshots, billions of deltas, full book reconstruction, and a strategy backtesting engine with realistic execution.
Project description
marketlens
Backtest prediction market strategies on tick-level L2 order book data from Polymarket.
pip install marketlens
Backtest
Define a strategy, run it against any market or series — the engine replays full L2 book state tick-by-tick with realistic execution.
from marketlens import MarketLens
from marketlens.backtest import Strategy
class OpeningFader(Strategy):
def on_market_start(self, ctx, market, book):
self._entered = False
def on_book(self, ctx, market, book):
if self._entered:
return
if book.midpoint < 0.50:
ctx.buy_yes(size=200)
else:
ctx.buy_no(size=200)
self._entered = True
client = MarketLens() # uses MARKETLENS_API_KEY env var
result = client.backtest(
OpeningFader(), "btc-up-or-down-5m",
initial_cash=10_000,
after="2026-04-15T01:45:00Z", before="2026-04-15T02:00:00Z",
)
print(result.summary())
Pass a market ID, series slug, or a list of series for multi-asset portfolios:
Always pass after/before — series and multi-strike runs are otherwise unbounded.
# Single market — replays the full lifetime of the market by default
result = client.backtest(strategy, market_id, initial_cash=10_000)
# Rolling series — walks every market in [after, before)
result = client.backtest(strategy, "btc-up-or-down-5m", initial_cash=10_000,
after="2026-04-15T01:45:00Z",
before="2026-04-15T02:00:00Z")
# Multi-asset portfolio — shared capital across series
result = client.backtest(strategy,
["btc-up-or-down-5m", "eth-up-or-down-5m", "sol-up-or-down-5m"],
initial_cash=10_000,
after="2026-04-15T01:45:00Z", before="2026-04-15T02:00:00Z")
# Structured product — replays every strike market in the matched event(s).
# Pass `after` to pick a single recent event; events are typically week-long,
# so a wide window can pull millions of book events.
result = client.backtest(strategy, "btc-multi-strikes-weekly",
initial_cash=10_000,
after="2026-05-08T00:00:00Z")
Execution realism
| Parameter | Default | Description |
|---|---|---|
latency_ms |
50 |
Order-to-fill delay in milliseconds |
queue_position |
False |
CLOB queue modeling — fills only when queue-ahead is drained by trades |
limit_fill_rate |
0.1 |
Fraction of trade size filling your limit (ignored when queue_position=True) |
slippage_bps |
0 |
Extra price penalty on market order fills |
fees |
"polymarket" |
Auto-detects crypto vs sports fee schedule; 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 fills and on_trade) |
settlement_delay_ms |
5000 |
Delay before filled tokens become sellable (on-chain settlement) |
The portfolio automatically handles CTF merge (opposite-side netting): buying NO while holding YES nets matched pairs at $1 per share. No explicit merge call needed in backtests.
Strategy hooks
| Hook | Called when |
|---|---|
on_book(ctx, market, book) |
Every book state change (snapshot or delta) |
on_trade(ctx, market, book, trade) |
Every executed trade |
on_fill(ctx, market, fill) |
Your order is filled |
on_market_start(ctx, market, book) |
A new market begins |
on_market_end(ctx, market) |
A market ends, before settlement |
ctx provides: buy_yes(), sell_yes(), buy_no(), sell_no(), cancel(), cancel_all(), position(), open_orders, books (all active order books), and reference_price() (Binance spot for crypto underlyings).
Results
result.total_pnl # net P&L
result.total_return # as decimal (0.12 = 12%)
result.win_rate # fraction of profitable settlements
result.sharpe_ratio # per-settlement Sharpe
result.sortino_ratio # downside-adjusted
result.max_drawdown # peak-to-trough as fraction
result.profit_factor # gross wins / gross losses
result.expectancy # avg net P&L per settlement
result.trades_df() # per-fill DataFrame
result.orders_df() # per-order DataFrame
result.settlements_df() # per-market settlement P&L
result.equity_df() # equity curve time series
result.by_series() # per-series P&L attribution
Persist a result to disk and reload it later:
from marketlens.backtest import BacktestResult
result.save("runs/spread-timer") # or overwrite=True
loaded = BacktestResult.load("runs/spread-timer")
loaded.config, loaded.targets # config + run inputs preserved
The directory holds a JSON manifest plus four Parquet files (trades, orders, settlements, equity) — readable directly from pandas/duckdb.
Data
All list methods return auto-paginating iterators with .to_list() and .to_dataframe().
Order book replay
walk() replays full L2 book state for any market or series. Pass a market ID, series slug, or condition ID — the same interface for everything.
walk = client.orderbook.walk(
"btc-up-or-down-5m",
after="2026-04-15T01:45:00Z", before="2026-04-15T01:50:00Z",
)
for market, book in walk:
print(market.question, book.midpoint, book.spread_bps())
# As a DataFrame
df = client.orderbook.walk(
market_id, after=start, before=end,
).to_dataframe()
Candles, trades, markets
candles = client.markets.candles(
market_id, resolution="1m",
after="2026-04-15T01:45:00Z", before="2026-04-15T01:50:00Z",
).to_dataframe()
trades = client.markets.trades(
market_id,
after="2026-04-15T01:45:00Z", before="2026-04-15T01:50:00Z",
).to_list()
active = client.markets.list(status="active", sort="-volume", take=10)
Bulk export
Download full history as Parquet — snapshots, deltas, trades, and reference prices.
# Single market (includes reference trades for the underlying)
data_dir = client.exports.download(market_id)
# All markets in a series — returns a result with ready / pending / failed
result = client.exports.download_series(
"btc-up-or-down-5m", after="2026-03-01", before="2026-03-08")
print(result.ready, result.pending, result.failed, result.events_charged)
Markets are pre-built server-side. If a market isn't ready yet, download(market_id) raises ExportNotReadyError; download_series(...) returns it under result.pending and skips the file.
Offline backtesting
Download once, run many backtests without API calls:
result = client.exports.download_series(
"btc-up-or-down-5m", after="2026-03-01", before="2026-03-08")
backtest = client.backtest(
strategy, "btc-up-or-down-5m",
data_dir=result, # PathLike — passes straight through
after="2026-03-01", before="2026-03-08",
initial_cash=10_000,
)
Structured Products & Surfaces
For multi-strike series (survival, density, barrier), all sibling markets replay in parallel. walk.books holds the latest book for every strike, and walk.surface() fits the implied probability distribution at each tick.
walk = client.orderbook.walk(
"btc-multi-strikes-weekly",
after="2026-05-08T00:00:00Z", # picks the next event ending after this
)
for market, book in walk:
surface = walk.surface()
if surface:
for s in surface.survival_strikes():
print(f"${s.strike:,.0f} P(above)={s.fitted_prob:.3f}")
print(f"implied_mean=${surface.implied_mean:,.0f}")
break # the loop fires per book tick — break to print one fit
| Type | Source | Stats |
|---|---|---|
survival |
"above $X" multi-strike markets | implied_mean, implied_cv, implied_skew |
density |
Neg-risk range + tail markets | implied_mean, implied_cv, implied_skew |
barrier |
Hit-price reach/dip markets | implied_peak, implied_trough |
Pre-computed surfaces updated every 5 minutes are also available via client.signals.surfaces().
OrderBook
Every OrderBook instance — live or replayed — carries analytical methods:
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(levels=3) # bid/ask imbalance [-1, 1]
book.impact("BUY", 1000) # VWAP for $1k market buy
book.slippage("BUY", 1000) # slippage from mid
book.depth_within(0.02) # (bid, ask) depth within 2c of mid
Numeric types
All numeric fields (prices, sizes, volumes, fees, OHLCV, depths, strikes, statistics) are float. Defaults are picked so call sites don't need defensive guards:
- Polymarket prices —
best_bid,best_ask,midpoint,Outcome.last_price— default to0.5(the neutral [0, 1] prior).if book.midpoint < 0.4andif book.best_ask > 0.7both behave correctly when the side is missing. - Sizes & rates —
spread,bid_depth,ask_depth,volume,liquidity,vwap,fee_rate_bps— default to0.0. Absence reads as zero magnitude. - Genuinely optional — an unresolved market's
winning_outcome, a non-structured market'sstrike, and helper methods likebook.spread_bps()/book.impact(...)still returnNonewhen the book itself is empty or insufficient.
book.best_bid * 0.99 # works directly — no Decimal wrap
if book.midpoint < 0.35: # cheap → consider buying YES
ctx.buy_yes(size=200)
Detect a truly empty book with book.bid_levels / book.ask_levels rather than comparing the price defaults against 0:
if book.bid_levels and book.ask_levels:
print(book.spread_bps())
Reference Prices
Binance spot at 1-second resolution for crypto underlyings (BTC, ETH, SOL, XRP, etc.). Available directly or inside backtests via ctx.reference_price().
candles = client.reference.candles(
"BTC",
after="2026-04-15T01:45:00Z", before="2026-04-15T01:50:00Z",
resolution="1s",
)
for candle in candles:
print(candle.timestamp, candle.close)
API Reference
| Resource | 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() trades() |
client.exports |
download() download_series() |
Async: use AsyncMarketLens — every method has an async counterpart.
Examples
| Example | Description |
|---|---|
backtest_basic.py |
Spread-timing strategy on a rolling series |
backtest_limit_orders.py |
Market-making with CLOB queue position simulation |
backtest_surface.py |
Surface mispricing with spot-distance filtering |
backtest_portfolio.py |
Multi-series portfolio with shared capital |
execution_cost.py |
Book depth, spread, impact and slippage |
microstructure.py |
Feature matrix — does imbalance predict outcome? |
implied_surfaces.py |
Survival, density, and barrier surfaces |
event_strikes.py |
Structured product walk with live surface fitting |
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-1.3.3.tar.gz.
File metadata
- Download URL: marketlens-1.3.3.tar.gz
- Upload date:
- Size: 139.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f790a2c85af73a6806d7a3a73103fb26f1b93ff6d3836bb6a7e3d2d208a1f27
|
|
| MD5 |
cda96e7adda492fa348fdd5c5f3cc232
|
|
| BLAKE2b-256 |
8178b167df95a6723b435e553d2aadb978866de656ccfe7826100556f859ace6
|
File details
Details for the file marketlens-1.3.3-py3-none-any.whl.
File metadata
- Download URL: marketlens-1.3.3-py3-none-any.whl
- Upload date:
- Size: 107.0 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 |
2ec03c4ccdd45b08745d9f2e95e8078717191812cdc1fb28de43c9042b010fc9
|
|
| MD5 |
5e30e496ecb82c6385cc73904f2cb104
|
|
| BLAKE2b-256 |
cf88c78cb333ec21828073729030b22f46db94dca4d47e2fb20b7bd6c32dfae5
|