Repository navigation
feat: prediction-market resolution outcomes, price and trade history (Kalshi, ForecastEx, Polymarket, Polymarket US) - #76
Merged
Conversation
…rchive tiers KalshiProvider gains iter_markets/fetch_markets and iter_trades/fetch_trades. Both page through the live endpoints (/markets, /markets/trades) and the archive (/historical/markets, /historical/trades), using /historical/cutoff to skip a tier that cannot match and dropping duplicates. Filters the archive does not accept (status, close-time window, multivariate exclusion combined with a ticker filter) are applied on the client. fetch_markets returns a typed frame with a normalized resolution outcome (result in yes/no/void/other or null, settlement_value, settlement_ts), and fetch_trades returns trade_id, ticker, timestamp, price, count, taker_side, is_block_trade. Both the *_dollars/*_fp fields and legacy unsuffixed fields are read. list_markets and the other existing methods are unchanged. BaseProvider gets a shared rate-limited _request/_request_json helper that maps 429/404/5xx/transport failures to the shared exceptions and retries transient ones under the existing provider retry policy, now exposed as provider_retry. prediction_markets.py holds the outcome vocabulary and trade schema shared by the prediction-market adapters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iders ForecastExProvider reads the free daily CSVs in the public forecastex-public-data bucket: fetch_prices (daily OHLC, marks, settlement price), fetch_trades (every matched YES/NO pair) and fetch_products. Its fetch_markets derives the resolution outcome: a contract absent from the next day's prices file has expired, and a final YES settlement price of 1.00/0.00 is the payout. Intraday contracts that drop out with a daily mark instead of the payout are reported as expired with a null result rather than guessed. PolymarketUSProvider reads the public gateway of the CFTC-regulated Polymarket US exchange (separate from the global PolymarketProvider, which is unchanged): offset-paginated markets with a normalized outcome taken from the long market side, /settlement, and price history in fixed or 24-hour windows. The gateway's outcomes labels are not always in outcomePrices order, so labels are never zipped with prices. Both are registered as "events" providers outside DataManager routing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add provider pages for ForecastEx and Polymarket US, extend the Kalshi page with resolved-market enumeration, the normalized outcome columns and trade history, and list both new venues in the provider index, the prediction-market source reference, provider selection and the navigation. Rename the new offline test modules so their basenames do not collide with the integration modules of the same venue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dependency audit now flags multidict 6.7.1 (CVE-2026-104874), urllib3 2.7.0 (PYSEC-2026-4175..4177) and virtualenv 21.7.1 (PYSEC-2026-4011..4014). Upgrade the three transitive pins to 6.9.1, 2.8.0 and 21.14.6; python-discovery moves with virtualenv. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aker sides Replace the exclude_multivariate flag with Kalshi's mve_filter parameter (None, "exclude", "only") on iter_markets/fetch_markets, so combo (KXMVE*) markets can be included, excluded or selected. /markets receives the value directly; /historical/markets rejects "only" and takes "exclude" only without a ticker filter, so the selection is completed on the client. The normalized market frame gains series (series filter or event-ticker prefix) and previous_price; the trade frame keeps taker_outcome_side and taker_book_side next to taker_side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…contracts
Price history now follows the market's tier. fetch_candles (and
fetch_ohlcv, which shares the routing) reads archived markets from
/historical/markets/{ticker}/candlesticks and live ones from
/series/{series}/markets/{ticker}/candlesticks. An explicit tier or
series_ticker skips detection; otherwise a live 404 falls back to the
archive, and a second 404 looks the series up from the market's event,
which fixes old tickers whose prefix is not their series (FED-23DEC is in
KXFED). Ranges beyond Kalshi's 5000-candle limit are split.
Prices are converted per field by encoding: *_dollars values and
unsuffixed strings are dollars, unsuffixed integers are legacy cents. The
previous blanket /100 on unsuffixed fields would have scaled archive
candles down 100x. fetch_candles returns trade OHLC, mean and previous,
YES bid/ask close, volume, open interest and the serving tier.
iter_market_frames streams fetch_markets' normalized rows in frames of
chunk_size rows for listings too large to materialize; fetch_markets now
delegates to it.
The provider-contracts public job runs the new Kalshi, ForecastEx and
Polymarket US live smoke tests, and the workflow contract test checks it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y and trades iter_markets / iter_market_frames / fetch_markets page through Gamma's GET /markets/keyset (GET /markets refuses offsets beyond 2000), passing next_cursor back as after_cursor; any other parameter name silently returns page 1, so a repeated cursor raises instead of looping. The server honours closed, volume_num_min, end_date_min and end_date_max (verified 2026-10-08) and caps pages at 100; the filters are re-applied on the client. include_tag=true brings the market tags, the reliable topic field now that category is mostly null. Rows are normalized to the shared resolution vocabulary. Gamma markets are binary and ordered: the first outcome is the YES side whatever its label, final prices [1,0]/[0,1] are yes/no, [0.5,0.5] is void, a UMA "resolved" market with other prices is "other", the rest is null. settlement_ts is closedTime (else umaEndDate), and the frame carries token ids, event slug/title, neg-risk and the raw fee fields. fetch_candles always sends startTs/endTs with fidelity, split into windows of at most 15 days: interval=max returns nothing for resolved markets below daily fidelity, and longer spans are rejected with 400. fetch_trades pages data-api /trades newest first. Past the offset cap of 10000 it moves the inclusive end bound to the oldest second seen and drops the boundary trades already yielded; a repeated page or a second too dense to page past raises. Prices are converted to the YES price and the taker side to yes/no from outcome index and BUY/SELL. Offline tests use responses recorded on 2026-10-08; each was checked to fail with its defect planted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CLOB treats interval as a range ending now that overrides startTs and endTs, so fetch_ohlcv's interval=1h chunks came back empty for any past window (reproduced live on 2026-10-08 for a market resolved in 2025). Chunks now send their bounds with the interval's step as fidelity. YYYY-MM-DD bounds were also converted in the machine's local zone; they are UTC days now, and "max" no longer builds a single multi-year window the CLOB rejects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Polymarket provider page covers fetch_markets / iter_market_frames (keyset paging, normalized columns, first-outcome-is-YES mapping), fetch_candles (15-day windows, the empty interval=max history of resolved markets) and fetch_trades (offset cap and end-bound paging), and replaces the outdated 7-day range advice. The prediction-market reference lists Polymarket among the venues with normalized outcomes. TestPolymarketResolvedHistory smoke-tests the three calls against a market resolved on 2026-01-01; the provider-contracts public job runs it and the workflow contract test checks that it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows has no time.tzset, so the ecosystem qualification job errored in the fixture that switches the local zone to America/New_York. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 14, 2026
stefan-jansen
added a commit
that referenced
this pull request
Oct 15, 2026
Include the real month end in Fama-French filters, include the stored end-date bar in gap detection, request the inclusive macro end date from Yahoo, interpret Finnhub date bounds in UTC, and follow Massive aggregate pagination. Closes #73 Preserves the five applicable fixes contributed by quantum-like in #74. The Kalshi and Polymarket UTC fixes from that contribution are already supplied by #76. The regressions failed against the original base and passed with the fixes. The replacement retains the original contribution commits for review and passed the supported CI checks in #85. Co-authored-by: quantum-like <quantum-like@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds what research on resolved prediction markets needs: every resolved market with a normalized outcome, price history that reaches resolved markets, and trade history where the venue publishes it for free.
Kalshi (
KalshiProvider, existing methods unchanged)iter_markets/fetch_markets: cursor-paginated listing over the live/marketsendpoint and the/historical/marketsarchive./historical/cutoffdecides when the archive can be skipped. Filters: status (incl.settled), series, event, min/max close time, and Kalshi'smve_filter(None includes combo/parlayKXMVE*markets,"exclude"drops them,"only"selects them; the archive rejectsonly, so it is applied on the client there). The archive accepts only one of event/series/mve filter and no status or time filter, so the remaining filters run on the client. Live/archive duplicates are dropped.Market frame: ticker, event_ticker, series, title, status, market_type, open/close time, result, settlement_value, settlement_ts, result_raw, expiration_value, last_price, previous_price, volume, open_interest, source.
iter_trades/fetch_trades: trades for a ticker over/markets/tradesand/historical/trades, with min/max time; columns include taker_side, taker_outcome_side and taker_book_side.Reads the
*_dollars/*_fpfields and the legacy unsuffixed fields (integers as cents, strings as dollars).fetch_candles(ticker, start_ts, end_ts, period, series_ticker=None, tier=None): price history from whichever tier holds the market (/historical/markets/{ticker}/candlesticksfor archived markets,/series/{series}/markets/{ticker}/candlesticksfor live ones). Giventierorseries_ticker, it goes straight to the right endpoint. Otherwise a live 404 falls back to the archive, and a second 404 looks the series up from the market's event (FED-23DECis in seriesKXFED). Ranges over Kalshi's 5000-candle limit are split. Columns: trade OHLC, mean, previous, YES bid/ask close, volume, open interest, source.fetch_ohlcvkeeps its signature and now uses the same routing.Units fix: unsuffixed integers are legacy cents;
*_dollarsvalues and unsuffixed strings (archive candles) are dollars. The old blanket /100 would have scaled archive candles down 100x.iter_market_frames(chunk_size=100_000, ...)streams the normalized market rows for listings too large to hold in memory;fetch_marketsdelegates to it.ForecastEx (new
ForecastExProvider)forecastex-public-dataS3 bucket:fetch_prices,fetch_trades(every matched pair),fetch_products.fetch_marketsderives the outcome. A contract that is missing from the next day's prices file has expired, and a final YES settlement price of 1.00 or 0.00 is the payout. Intraday contracts (hourly temperatures) drop out of the files with a daily mark, not their payout. They are reported asexpiredwith a null result instead of a guessed outcome.Polymarket US (new
PolymarketUSProvider, separate from the global venue below)/v1/markets(the server caps pages at 500),/v1/markets/{slug}/settlement, and/v1/price-history(full history, or 24-hour one-minute windows).outcomeslabels are sometimes listed in a different order fromoutcomePrices:["No","Yes"]comes with long-first["1","0"]for the FOMC 2026-04-29 "maintains" market, whose YES side won. Zipping the two lists gives the wrong winner.Polymarket, global venue (
PolymarketProvider, existing methods kept)iter_markets/iter_market_frames(chunk_size, closed=True, min_volume=None, end_date_min=None, end_date_max=None)/fetch_markets: every market through Gamma'sGET /markets/keyset(GET /marketsrefuses offsets above 2000). The cursor goes back asafter_cursor; any other parameter name silently returns page 1, so a repeated cursor raises. Pages hold at most 100 markets. The server honoursclosed,volume_num_min,end_date_minandend_date_max(verified 2026-10-08), and the client applies them again.include_tag=truebrings the market tags;categoryis mostly null on recent markets.["Trump","Boden"]: Trump is YES). Final prices[1,0]/[0,1]give yes/no,[0.5,0.5]gives void, a UMA-resolved market with other prices gives other, and everything else is null.settlement_tsisclosedTime, elseumaEndDate.fetch_candles(token_id, start, end, fidelity_minutes): timestamp and price.interval=maxreturns an empty history for resolved markets at fidelities finer than one day, andstartTs/endTsspans over 15 days get HTTP 400. Requests therefore send explicit bounds in windows of at most 15 days, which serves resolved markets down to one-minute fidelity.iter_trades/fetch_trades(condition_id, start=None, end=None): data-API/trades, which returns trades newest first with inclusivestart/endbounds, at most 10,000 per request, and rejects offsets above 10,000. Past the offset cap,endmoves to the oldest second seen and trades from that second that were already yielded are dropped. A repeated page, or a single second with more trades than one offset window reaches, raises.priceis the YES price (one minus the token price for the second outcome) andtaker_sideis yes/no, from outcome index and BUY/SELL.fetch_ohlcvreturned no rows for past ranges, because it sentinterval(a range ending now, which overrides the bounds) together withstartTs/endTs. It also readYYYY-MM-DDbounds in the machine's local time zone.Shared
prediction_markets.py: the outcome vocabulary (resultis one ofyes,no,void,other, or null while unresolved) plussettlement_valueandsettlement_ts, and the trade schema shared by the adapters.BaseProvider._request/_request_json: a rate-limited GET that maps 429 / 404 / 5xx / transport failures to the shared exceptions and retries transient failures under the existing retry policy, which is now exposed asprovider_retryand honorsRetry-After.eventsproviders. They are not routed through DataManager.Dependencies
uv.lock: multidict 6.9.1, urllib3 2.8.0, virtualenv 21.14.6. The CI dependency audit started flagging the previously locked versions after main's last run, and the audit job fails until they are bumped.Tests
tests/providers/test_kalshi_history.py,test_forecastex_provider.pyandtest_polymarket_us_provider.pyrun against in-processhttpx.MockTransportvenues. They cover success, pagination within and across the live/archive boundary, empty responses, malformed JSON/CSV/records, 429 retry honoringRetry-After, persistent 429, 5xx, transport errors, and repeated cursors.tests/providers/test_polymarket_history.pyreplays Gamma, CLOB and data-API responses recorded on 2026-10-08 (tests/providers/fixtures/polymarket/) through fake servers that implement the verified paging, window and offset rules. Each test names the defect it prevents. Every test was shown to fail with its defect planted (20 mutations, including a wrong cursor parameter, removed repeated-cursor and repeated-page guards, a 30-day price window,interval=max, a raw NO-token price reported as YES, and the boundary second dropped or duplicated when paging trades). Thefetch_ohlcvtests fail on the oldintervalrequest and, under a non-UTCTZ, on local-zone dates.tests/integration/test_kalshi.py::TestKalshiResolvedHistory(gated byRUN_PUBLIC_KALSHI_TESTS=1),tests/integration/test_forecastex.py,tests/integration/test_polymarket_us.pyandtests/integration/test_polymarket.py::TestPolymarketResolvedHistory. All passed on 2026-10-08, including combo-market selection, hourly candles for one archived and one live KXFEDDECISION market, and 20 days of hourly Polymarket prices plus trades for a market resolved on 2026-01-01. The public job inprovider-contracts.ymlruns these tests, andtests/test_release_workflows.pychecks that it does.Gates run locally
ruff check, ruff format --check, ty check, pytest
-W error::ResourceWarning(3759 passed), mkdocs build --strict, uv build, wheel import of the new exports, actionlint, pre-commit run --all-files, pip-audit of the locked environment: all pass.🤖 Generated with Claude Code