Skip to content

Guides

Markets

As of v1.15 the markets layer is a set of thin delegators over the venue-free core dataset(): markets.kalshi.dataset(ticker) and markets.polymarket.dataset(event_id) own all venue knowledge — station resolution, label-source routing, strike parsing, and the outcome=True binary settlement target — and forward the join to core with zero duplicated join logic. Plus discover (which station settles this contract?) and trades (per-contract timeseries). Both SDKs ship parity.

Kalshi and Polymarket use different identifiers and different stations for the same city. Mostlyright reconciles them.

from mostlyright import discover
df = discover("new york")
print(df)
# city station settles_for
# 0 new york NYC ["kalshi:NYC"]
# 1 new york LGA [] (denylist backstop)
# 2 new york JFK ["polymarket:nyc"]

settles_for is a typed list of <issuer>:<id> strings:

  • "kalshi:NYC" — Kalshi settles NYC contracts using station=NYC (KNYC).
  • "polymarket:nyc" — Polymarket settles nyc contracts using a different station (typically KLGA per Polymarket’s published methodology).
  • [] — Denylist backstop. The station exists but no issuer settles against it.

Point a delegator at a full market identifier and it resolves the settlement station, routes to the venue-correct label recipe, and (optionally) binarizes the day’s settlement value against the contract’s strike. There is zero duplicated join logic — each delegator resolves venue knowledge, then forwards to core dataset().

from mostlyright.markets import kalshi, polymarket
# Kalshi NHIGH/NLOW settle on the NWS CLI product → routes to label="cli".
k = kalshi.dataset("KXHIGHNY-25MAY26-T79", "2025-05-26", "2025-05-26", outcome=True)
print(k[["date", "cli_high_f", "label_outcome"]])
# label_outcome: 1 (YES) when cli_high_f >= 79, else 0; NaN if no CLI report.
# Polymarket resolves on WU/NOAA-WRH extremes → routes to label="daily_extremes".
p = polymarket.dataset("<event_id>", "2025-05-26", "2025-05-26", outcome=True)
print(p[["date", "daily_extremes_tmax_c", "label_outcome"]])

outcome=True — contract-shaped 0/1 targets

Section titled “outcome=True — contract-shaped 0/1 targets”

outcome=True appends a single binary label_outcome column derived from the contract’s own label recipe and its parsed strike — never the other venue’s. Settlement comparison is inclusive, matching the “N° or above” contract wording:

  • Kalshi -T<strike> thresholds settle YES when value >= strike — the strike degree itself is YES. -B<lo>-<hi> ranges are inclusive on both endpoints (Kalshi’s integer-degree buckets tile the degree space contiguously). Malformed tickers raise KalshiTickerError.
  • Polymarket above (YES when observed >= strike), below (observed <= strike), and between (lo <= observed <= hi, both inclusive) mirror the same rule in °C. Malformed strikes raise PolymarketStrikeError.

A day whose label value is missing (e.g. no CLI report) gets a NaN/NA outcome — never a silent 0 or 1.

The delegators forward extra core kwargs verbatim (granularity=, source=, tz_override=, qc=, features=), so everything in the Research API guide composes on top.

Negative-temperature strikes — the sign rule

Section titled “Negative-temperature strikes — the sign rule”

Both strike parsers accept negative-temperature strikes, and both handle the hyphen ambiguity the same way. In a slug the hyphen is both a word-separator (between-70-...) and a numeric minus sign (above -5c). A greedy -?\d+ cannot tell them apart — it would silently turn above -5c into +5 and flip every cold contract. So a leading minus binds to the strike integer:

  • KalshiT-5 (threshold at −5°F), B-5--3 (range −5..−3°F). The parser reads -?\d+ for each strike field.
  • Polymarket — the sign is decided from each value’s separator run: a run ending in -- (slug separator + minus) or whitespace-then-- (prose above -5c) is negative; a bare trailing hyphen after a word (between-70-...) is a slug separator and stays positive. Each between endpoint is signed independently (between -10 and -5c → both negative).

Get the sign wrong and every sub-freezing settlement inverts, so the delegators parse it explicitly rather than trust a greedy regex.

Polymarket’s city-to-station mapping is published in schemas/polymarket-city-stations.json (mirrored to both SDKs). Each city may use a different station per measure — paris-low settles on LFPB (Le Bourget), not the default LFPG (Charles de Gaulle):

from mostlyright.markets import POLYMARKET_CITY_STATIONS
print(POLYMARKET_CITY_STATIONS["paris"])
# {
# "default": "LFPG",
# "low": "LFPB", # different station for the low measure
# }

Measure-specific routing is preserved end-to-end — collapsing to default only would silently mis-route settlement.

import { POLYMARKET_CITY_STATIONS } from "@mostlyrightmd/markets";
console.log(POLYMARKET_CITY_STATIONS.paris);

Pull historical trade timeseries (price + size + side + timestamp) for a single contract:

from mostlyright.markets import kalshi_trades
df = kalshi_trades("&lt;contract-ticker&gt;")
print(df.head())
# Columns: trade_id, executed_at, price_cents, size, side

Three potential divergences between Python and TS trades surfaces are locked in via parity tests:

Shape detailPythonTypeScript
Candle bucket labelend_period_tsstart (Phase 21 docs note: both label by start; legacy doc-only divergence resolved)
Polymarket paginationlimit=100limit=100 (was 500, fixed)
Equal-timestamp orderingHTTP-order preservedHTTP-order preserved (no in-SDK sort)

Both SDKs preserve the issuer’s HTTP order — no implicit re-sorting on the trade-ID dimension. If you need a specific sort, sort downstream.

The catalog is registry-based. To add a new issuer:

from mostlyright.markets.catalog import register_adapter, MarketAdapter
class MyExchangeAdapter(MarketAdapter):
SUPPORTED_SOURCES = ["myexchange.trades"]
def fetch_trades(self, contract_id: str) -> list[dict]:
...
register_adapter("myexchange.trades", MyExchangeAdapter)

The same pattern lives in TS (registerAdapter from @mostlyrightmd/markets).

ErrorWhen
KalshiTickerErrorA Kalshi ticker doesn’t parse (bad series/date/strike) or its city isn’t in the settlement whitelist
PolymarketStrikeErrorpolymarket.dataset(outcome=True) can’t parse an above/below/between <n><C|F> strike
DataAvailabilityErrorUnknown adapter / contract / source
DeferredMarketErrorContract not yet open for settlement
PolymarketEventErrorPolymarket event not found at the resolver endpoint

All inherit from TradewindsErrorexcept TradewindsError catches them all.