cutemarkets
cutemarkets-python
Pythonโœจ New

Official Python client for the CuteMarkets options market-data API. Sync + async, typed Pydantic models, auto-pagination, typed errors, and rate-limit introspection.

Last updated Jul 9, 2026
30
Stars
0
Forks
0
Issues
+1
Stars/day
Attention Score
61
Language breakdown
No language data available.
โ–ธ Files click to expand
README

CuteMarkets Python SDK for Real-Time and Historical Options Data

The official Python options API client for CuteMarkets. Use it to query real-time and historical options data from Python, including options chain API snapshots, historical contracts with asof, quotes, trades, aggregates, expirations, and ticker search.

cutemarkets wraps the CuteMarkets v1 REST API in a typed, namespaced, Pythonic interface. Sync and async clients share the same method surface, response models are pydantic v2 classes that preserve the raw payload on .raw, every list endpoint ships with both one-page and auto-paginating variants, and every error path maps to a specific exception class so you can handle plan gating, rate limiting, and missing resources cleanly.

Quick links:


Table of contents


Use Cases

  • Build an options chain scanner for liquid contracts and spread filters.
  • Reconstruct historical contracts with as_of for backtests and event studies.
  • Estimate implied move around earnings from the ATM straddle.
  • Inspect quote quality before you trust an options backtest.

Explore Examples

Features

  • Sync (CuteMarkets) and async (AsyncCuteMarkets) clients with identical surfaces.
  • Namespaced methods that mirror the docs: client.options.chain(...), client.options.aggs.range(...), client.options.indicators.sma(...), client.tickers.search(...), ...
  • Typed pydantic v2 models for every response, with .raw preserving the original JSON so new server fields never block you.
  • Short-name preservation on abbreviated wire payloads (LastTrade.T/p/s/..., Aggregate.o/h/l/c/v/vw/t/n) plus readable property aliases (.ticker, .price, .open, .close, ...).
  • One-shot list(...) returns a Page[T] (results, nexturl, requestid, ratelimit, .next()). Auto-paginating iterlist(...) / iter_*(...) generators walk every page for you.
  • Automatic date / datetime / bool / Enum coercion and underscore-to-dot rewriting for range filters (strikepricegte=100 โ†’ strike_price.gte=100).
  • Typed exception hierarchy (AuthenticationError, ForbiddenError, LookbackExceededError, NotFoundError, RateLimitError, ...) selected from the error.code envelope field.
  • Rate-limit header introspection via page.ratelimit, client.lastratelimit, and client.lastrequest_id.
  • Opt-in exponential-backoff retries on 429, 5xx, and transient network errors.
  • Custom httpx.Client / httpx.AsyncClient / httpx.BaseTransport injection for testing.

Why this SDK instead of raw HTTP requests

Raw HTTP calls work for quick experiments, but they force you to rebuild the same glue in every project: authentication headers, pagination, date coercion, query serialization, response typing, and error mapping. This SDK centralizes that work so your research code can focus on chain selection, contract filtering, quote quality, and analytics rather than transport plumbing.

That matters even more for historical options data. Workflows like contract reconstruction with as_of, quote-aware filtering, and earnings event studies are easier to maintain when the API surface is typed, consistent, and reusable across both sync and async code.


Installation

pip install cutemarkets

Requires Python 3.9 or newer. The only runtime dependencies are httpx and pydantic.

Development extras:

pip install "cutemarkets[dev]"

Quick start

from cutemarkets import CuteMarkets

client = CuteMarkets(apikey="cm...")

chain = client.options.chain("NFLX", limit=5) for contract in chain: print(contract.details.ticker, contract.breakevenprice, contract.greeks.delta if contract.greeks else None)

print("request id:", chain.request_id) print("remaining this minute:", chain.ratelimit.remainingminute)

Async equivalent:

import asyncio
from cutemarkets import AsyncCuteMarkets

async def main(): async with AsyncCuteMarkets(apikey="cm...") as client: chain = await client.options.chain("NFLX", limit=5) for contract in chain: print(contract.details.ticker)

asyncio.run(main())


Authentication

API keys are prefixed cm_... and are passed as a Bearer token on the Authorization header.

Three ways to provide your key, evaluated in this order:

  • Constructor argument:
client = CuteMarkets(apikey="cm...")
  • CUTEMARKETSAPIKEY environment variable:
export CUTEMARKETSAPIKEY=cm_xxxxxxxxxxxxxxxx
client = CuteMarkets()  # auto-picks up the env var
  • Runtime setter:
client.setapikey("cm_...")

Get a free key at cutemarkets.com/signup. Every request's requestid is echoed back in responses and errors, so include it in support conversations.

Unauthenticated endpoint

client.status() hits /v1/status/ and does not require an API key. It works even before you configure credentials, so it's handy for readiness probes.


Client options

CuteMarkets(
    api_key: str | None = None,
    *,
    base_url: str = "https://api.cutemarkets.com",
    timeout: float = 30.0,
    max_retries: int = 2,
    headers: dict[str, str] | None = None,
    transport: httpx.BaseTransport | None = None,
    http_client: httpx.Client | None = None,
)

| Argument | Purpose | | --- | --- | | apikey | Override or supply the API key. Overrides CUTEMARKETSAPI_KEY. | | base_url | Point the client at a different host (e.g. a staging or proxy URL). | | timeout | Per-request timeout in seconds (applies to connect + read). | | max_retries | Retry attempts on 429, 5xx, and transient network errors. 0 disables retries. | | headers | Extra headers merged onto every request (useful for tracing or proxy auth). | | transport | Pass an httpx.MockTransport (or any httpx.BaseTransport) for tests. | | http_client | Bring your own httpx.Client when you want custom transport, event hooks, or connection pooling. |

AsyncCuteMarkets accepts the same kwargs, with transport: httpx.AsyncBaseTransport and http_client: httpx.AsyncClient.

Both classes support the context-manager protocol:

with CuteMarkets(apikey="cm...") as client:
    ...

async with AsyncCuteMarkets(apikey="cm...") as client: ...


Resource reference

Every method below links to the matching page under docs/. Every response is a pydantic model โ€” dotted attribute access, .modeldump() for a dict, .raw for the untouched payload.

client.status()

Poll the public health endpoint. No API key required.

status = client.status()
status.status            # "ok" or "degraded"
status.is_ok             # bool
status.services["api"].status
status.services["database"].latency_ms

client.tickers.search(query=..., limit=...)

docs/ticker-search.md

for row in client.tickers.search(query="NFLX", limit=8):
    print(row.symbol, row.name)

client.tickers.expirations(ticker)

docs/expirations.md

exp = client.tickers.expirations("NFLX")
exp.results  # ["2026-04-02", "2026-04-10", ...]

client.options.chain(ticker, **filters)

docs/option-chain.md

Returns a Page[ContractSnapshot]. Accepts strikeprice, expirationdate, contracttype, the range filters (strikepricegte, expirationdate_lt, ...), sort, order, limit, and page.

chain = client.options.chain(
    "NFLX",
    c,
    strikepricegte=90,
    strikepricelte=110,
    sort="strike_price",
    order="asc",
    limit=50,
)
for c in chain:
    print(c.details.ticker, c.breakevenprice, c.implied_volatility)

client.options.iterchain("NFLX", ...) auto-follows nexturl.

client.options.snapshot(underlying, option_contract)

docs/option-contract-snapshot.md

snap = client.options.snapshot("NFLX", "O:NFLX260410C00060000")
snap.greeks.delta
snap.underlying_asset.price

client.options.contracts.list(filters) / .get(optionsticker, asof=None) / .iter_list(filters)

docs/contracts.md

# Paged list
page = client.options.contracts.list(
    underlying_ticker="NFLX",
    expirati,
    limit=1000,
)

Walk every contract across pages

for c in client.options.contracts.iterlist(underlyingticker="NFLX"): print(c.ticker)

Detail for one contract, optionally as-of a historical date

from datetime import date detail = client.options.contracts.get("O:NFLX260402C00075000", as_of=date(2026, 1, 15))

client.options.trades.list(ticker, **filters) / .last(ticker) / .iter_list(...)

docs/trades.md

# Historical trades
page = client.options.trades.list(
    "O:NFLX260402C00075000",
    timestamp_gte="2026-03-01",
    timestamp_lte="2026-03-31",
    sort="timestamp",
    order="desc",
    limit=1000,
)

Compact "last trade" shape (abbreviated keys + readable aliases)

last = client.options.trades.last("O:NFLX260410C00060000") last.price # property alias for last.p last.size # property alias for last.s last.ticker # property alias for last.T last.raw # the untouched {"T": ..., "p": ..., "s": ..., ...}

client.options.quotes.list(ticker, **filters) / .iter_list(...) โ€” Expert Plan only

docs/quotes.md

Non-Expert keys receive HTTP 403, surfaced as ForbiddenError:

from cutemarkets.errors import ForbiddenError

try: quotes = client.options.quotes.list("O:NFLX260402C00075000", limit=500) except ForbiddenError as exc: print("upgrade required:", exc.message)

client.options.aggs.range(ticker, multiplier, timespan, fromdate, todate, **opts)

docs/aggregates.md

from datetime import date
page = client.options.aggs.range(
    "O:NFLX260402C00075000",
    multiplier=1,
    timespan="day",
    from_date=date(2026, 1, 1),
    to_date=date(2026, 4, 1),
    adjusted=True,
    sort="desc",
    limit=1000,
)
for bar in page:
    print(bar.timestamp, bar.open, bar.high, bar.low, bar.close, bar.volume)

client.options.aggs.iterrange(...) auto-follows nexturl.

client.options.aggs.previous(ticker, adjusted=None)

prev = client.options.aggs.previous("O:NFLX260402C00075000")
print(prev.close, prev.vwap, prev.trade_count)

client.options.aggs.openclose(ticker, date, adjusted=None) / client.options.openclose(...)

Note: grouped under aggs for discoverability, but the underlying route is /v1/options/open-close/.... The payload is flat (no results envelope).

from datetime import date
oc = client.options.aggs.open_close("O:NFLX260402C00075000", date(2026, 3, 10))
oc.open, oc.close, oc.high, oc.low, oc.volume
oc.pre_market            # property alias for oc.preMarket
oc.after_hours           # property alias for oc.afterHours
oc.from_date             # property alias for the reserved-keyword "from"

client.options.indicators.sma(ticker, ...) / .ema(...) / .macd(...) / .rsi(...)

docs/indicators-sma.md, docs/indicators-ema.md, docs/indicators-macd.md, docs/indicators-rsi.md

sma = client.options.indicators.sma(
    "O:NFLX260402C00075000",
    timespan="day",
    window=20,
    series_type="close",
    limit=100,
)
for point in sma.values:
    print(point.timestamp, point.value)

macd = client.options.indicators.macd( "O:NFLX260402C00075000", timespan="day", short_window=12, long_window=26, signal_window=9, ) for point in macd.values: print(point.timestamp, point.value, point.signal, point.histogram)

Pass expand_underlying=True to populate result.underlying with the aggregate bars used in the calculation, plus an absolute URL to the same contract's aggregates range.


Models

Every response model:

  • Inherits from cutemarkets.CuteBase (a pydantic.BaseModel with extra="allow").
  • Preserves the untouched JSON payload on .raw, so new or undocumented fields are always reachable:
lt = client.options.trades.last("O:NFLX260410C00060000")
  lt.raw["futurefieldwedidntknow_about"]
  • Supports .modeldump(byalias=True) / .modeldumpjson() for round-tripping.
  • Keeps abbreviated wire keys as the primary field names (matching the docs) and exposes readable property aliases:
- LastTrade: T/p/s/t/x/c/y/f/r/i/q/e/z/ds โ†’ ticker/price/size/siptimestamp/exchange/conditions/participanttimestamp/trftimestamp/trfid/tradeid/sequencenumber/correction/tape/decimal_size. - Aggregate: o/h/l/c/v/vw/t/n โ†’ open/high/low/close/volume/vwap/timestamp/trade_count.
  • OpenClose is a flat envelope (no results key): the top-level payload is the model. from is mapped to from_date because from is a Python keyword.

Pagination

Page[T] holds one server page plus the metadata needed to fetch more:

page = client.options.contracts.list(underlying_ticker="NFLX", limit=1000)

page.results # list[Contract] page.next_url # full URL for the next page, or None page.request_id # server-assigned request id page.rate_limit # RateLimitInfo parsed from headers page.has_next # bool nextpage = page.next() # refetches nexturl verbatim with the same Authorization

Walk every item across every page:

for contract in page.iter_all(): ...

Every list endpoint also ships an iter_<name>(...) generator that does the pagination for you from the first request:

for contract in client.options.contracts.iterlist(underlyingticker="NFLX"):
    ...

The client follows next_url verbatim, with the same Authorization header. Don't reconstruct the URL yourself.


Filters and range queries

Range filters in the CuteMarkets API use a <field>.<op> naming convention (strike_price.gte=100). Python doesn't allow dots in keyword arguments, so this client accepts an underscore spelling and rewrites it server-side:

| You write | Sent on the wire | | --- | --- | | strikepricegte=100 | strike_price.gte=100 | | strikepricelt=500 | strike_price.lt=500 | | expirati | expiration_date.gte=2026-04-01 | | timestamp_lte=1770872400000 | timestamp.lte=1770872400000 |

You can combine them freely:

client.options.chain(
    "NFLX",
    c,
    strikepricegte=90,
    strikepricelte=110,
    expirati,
    expirati,
    sort="strike_price",
    order="asc",
    limit=100,
)

Any kwarg whose name does not end in gte / gt / lte / lt is passed through verbatim. That way future API filters work without a client upgrade.


Dates, enums, booleans

Values are auto-coerced to what the API expects:

| Python value | Wire form | | --- | --- | | datetime.date(2026, 1, 15) | "2026-01-15" | | datetime.datetime(2026, 1, 15, 9, 30) | "2026-01-15" | | True / False | "true" / "false" | | MyEnum.VALUE (subclass of enum.Enum) | .value then coerced | | list[int] / tuple[str, ...] | emitted as repeated key=value&key=value | | None | dropped โ€” useful for defaulting to server behavior |

Millisecond / nanosecond timestamps should be passed as int (or a decimal string); the client does not guess the unit. Timestamp filters (timestamp, timestamp_gte, ...) accept either a date or an int/string Unix timestamp depending on the endpoint โ€” see the doc page linked in each resource section.


Errors

All exceptions inherit from cutemarkets.CuteMarketsError.

CuteMarketsError
โ”œโ”€โ”€ ConfigurationError        # missing API key, bad settings
โ”œโ”€โ”€ TransportError            # network / timeout / connection error
โ””โ”€โ”€ APIError                  # any non-2xx response from the API
    โ”œโ”€โ”€ BadRequestError       # 400 bad_request
    โ”‚   โ””โ”€โ”€ InvalidPageTokenError
    โ”œโ”€โ”€ AuthenticationError   # 401 unauthorized
    โ”œโ”€โ”€ ForbiddenError        # 403 forbidden
    โ”‚   โ””โ”€โ”€ LookbackExceededError
    โ”œโ”€โ”€ NotFoundError         # 404 not_found
    โ””โ”€โ”€ RateLimitError        # 429 ratelimitexceeded

The specific subclass is chosen by HTTP status and the error.code field from the response envelope:

| HTTP | error.code | Exception | | --- | --- | --- | | 400 | bad_request | BadRequestError | | 400 | invalidpagetoken | InvalidPageTokenError | | 401 | unauthorized | AuthenticationError | | 403 | forbidden | ForbiddenError | | 403 | lookback_exceeded | LookbackExceededError | | 404 | not_found | NotFoundError | | 429 | ratelimitexceeded | RateLimitError |

Every APIError exposes:

  • status_code (HTTP status)
  • code (machine-readable code from the envelope)
  • message (human-readable message)
  • request_id (for support)
  • response (the decoded JSON body, for diagnostics)
  • rate_limit (RateLimitInfo parsed from headers)
from cutemarkets.errors import LookbackExceededError, RateLimitError, ForbiddenError

try: client.options.contracts.list(underlyingticker="SPY", asof="2015-01-01") except LookbackExceededError as exc: print(f"{exc.code}: {exc.message} (requestid={exc.requestid})")

try: client.options.quotes.list("O:SPY260402C00500000") except ForbiddenError as exc: print(f"Upgrade required: {exc.message}")


Rate limits

Free keys are capped at 10 requests/minute; Developer and Expert plans are unlimited. Each response includes rate-limit headers that this client parses into a RateLimitInfo object:

page = client.options.chain("NFLX", limit=5)
page.rate_limit.plan                # "Free" | "Developer" | "Expert"
page.ratelimit.limitminute        # "10" or "unlimited"
page.ratelimit.remainingminute    # int
page.ratelimit.limitday
page.ratelimit.remainingday

The same object is attached to APIError.rate_limit on failures, so you can inspect remaining quota after a 429.

When maxretries is non-zero the client retries 429 responses (as well as 5xx and transient network errors) with exponential backoff โ€” but note that retries share the same plan quota, so aggressive retries on Free keys can compound 429s. For Free keys you usually want maxretries=0 and your own backoff.


Async usage

AsyncCuteMarkets mirrors the sync surface; methods that perform I/O are async def and iterators are async:

import asyncio
from cutemarkets import AsyncCuteMarkets

async def main(): async with AsyncCuteMarkets(apikey="cm...") as client: status = await client.status() chain = await client.options.chain("NFLX", limit=5) async for contract in client.options.iter_chain("NFLX", limit=5): print(contract.details.ticker)

last = await client.options.trades.last("O:NFLX260410C00060000") print(last.price)

asyncio.run(main())

Async pagination:

page = await client.options.contracts.list(underlying_ticker="NFLX", limit=1000)
async for contract in page.iter_all():
    ...

Don't share a single AsyncCuteMarkets across event loops.


Recipes

Fetch the full option chain across pages

calls = []
for contract in client.options.iter_chain("NFLX", c, limit=100):
    calls.append(contract)

Build a daily OHLC series and compare to the previous day

from datetime import date, timedelta

ticker = "O:NFLX260402C00075000" today = date.today()

bars = list( client.options.aggs.iter_range( ticker, 1, "day", today - timedelta(days=365), today, sort="asc" ) ) prev = client.options.aggs.previous(ticker) print(f"last daily close: {bars[-1].close}, previous session close: {prev.close}")

Pull the last trade for every contract in a chain

for contract in client.options.iter_chain("NFLX", c, limit=100):
    last = client.options.trades.last(contract.details.ticker)
    print(contract.details.strike_price, last.price)

Stream tick-level trades for a day

for trade in client.options.trades.iter_list(
    "O:NFLX260402C00075000",
    timestamp_gte="2026-03-10",
    timestamp_lt="2026-03-11",
    sort="timestamp",
    order="asc",
    limit=10000,
):
    ...

Detect an SMA/EMA crossover

sma = client.options.indicators.sma(ticker, timespan="day", window=50, limit=200)
ema = client.options.indicators.ema(ticker, timespan="day", window=20, limit=200)

smabyts = {p.timestamp: p.value for p in sma.values} for p in ema.values: s = smabyts.get(p.timestamp) if s is not None and p.value is not None: print(p.timestamp, "above" if p.value > s else "below")

Resolve an expiration + strike to a contract ticker

page = client.options.chain(
    "NFLX",
    expirati,
    strike_price=60,
    c,
    limit=1,
)
if page.results:
    print(page.results[0].details.ticker)

Testing your integration

Every network call goes through httpx, so you can swap in an httpx.MockTransport for unit tests:

import httpx
from cutemarkets import CuteMarkets

def handler(request: httpx.Request) -> httpx.Response: if request.url.path == "/v1/status/": return httpx.Response(200, json={"status": "ok", "requestid": "cmtest", "services": {}}) return httpx.Response(404, json={"status": "ERROR", "error": {"code": "not_found", "message": "x"}})

client = CuteMarkets(apikey="cmtest", transport=httpx.MockTransport(handler)) assert client.status().is_ok

The test suite in this repo uses the same pattern via a makeclient fixture โ€” see tests/conftest.py for a worked example.

For live integration checks, the example scripts cover the common developer workflows in this repo:

  • python examples/quickstart.py
  • python examples/historicalcontractsas_of.py
  • python examples/buildoptionschain_scanner.py
  • python examples/earningsimpliedmove.py
  • python examples/quotequalityfilter.py
  • python examples/smoke_test.py

Versioning and compatibility

  • SemVer: minor versions may add new resources and fields; breaking changes bump the major.
  • Supports Python 3.9, 3.10, 3.11, 3.12, 3.13.
  • Requires pydantic>=2.6 (pydantic v1 is not supported).
  • Requires httpx>=0.27.

Development

git clone https://github.com/cutemarkets/cutemarkets-python
cd cutemarkets-python
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

pytest ruff check src tests mypy src

Running the live smoke test

cp .env.example .env

fill in CUTEMARKETSAPIKEY=cm_...

python examples/smoke_test.py

The smoke test runs one minimal call against every resource group against https://api.cutemarkets.com, spaced out to stay within the Free plan's 10 req/min limit. It tolerates ForbiddenError on quotes (Expert Plan only) so Free / Developer keys can still use it.

Additional live recipes:

  • python examples/historicalcontractsas_of.py for historical contract reconstruction
  • python examples/buildoptionschain_scanner.py for chain and liquidity filtering
  • python examples/earningsimpliedmove.py for ATM straddle implied move estimation
  • python examples/quotequalityfilter.py for historical quote-quality checks

License

MIT - see LICENSE.

๐Ÿ”— More in this category

ยฉ 2026 GitRepoTrend ยท cutemarkets/cutemarkets-python ยท Updated daily from GitHub