Skip to content

Reference

Macro Education

Using FXMacroData with Prediction Markets: Kalshi and Polymarket

Step-by-step Python guide to lining up FXMacroData release calendars, announcement rows, source-labelled forecasts, and COT positioning against Kalshi and Polymarket macro contracts.

Editorial standards and corrections

Share article X LinkedIn Email
Using FXMacroData with Prediction Markets: Kalshi and Polymarket article banner

Where macro data meets prediction markets

Prediction markets now list contracts on scheduled macro events. Kalshi, an exchange regulated by the US Commodity Futures Trading Commission (CFTC), lists event contracts on outcomes such as the next US CPI print or the result of a Federal Reserve meeting. Polymarket, a crypto-based market whose international platform settles on the Polygon network, lists similar macro questions; who can trade on it depends on where you live. Contract terms, thresholds, and resolution sources differ from market to market, so always read the rules of the specific contract. What the two platforms share with USD/JPY, EUR/USD, and the rest of the FX complex is the underlying release.

If you already use FXMacroData to track release calendars, CPI prints, and policy-rate history, you have the raw ingredients for a systematic view on these contracts. This guide shows how to pull release-timed data and source-labelled forecasts from FXMacroData, line them up against a contract's threshold, and build a reproducible decision workflow in Python. It is a research workflow, not trading advice, and nothing here connects to either platform's API.

Goal
By the end of this guide you will have a Python script that fetches upcoming macro releases from FXMacroData, pulls the latest pre-release forecast for the next print, measures how past prints compared with that forecast source, and turns the result into a simple directional read against a contract threshold.

Prerequisites

  • An FXMacroData API key. Subscribe here to start the 14-day trial.
  • Python 3.10 or later with requests installed (pip install requests).
  • A Kalshi or Polymarket account if you want to compare the output with live contract prices. The code below does not call either platform.
  • Basic familiarity with CPI, Non-Farm Payrolls, and policy rate decisions.

Step 1 — Pull the upcoming release calendar

Start with what is coming and when. The release calendar endpoint returns scheduled releases for a currency, each with a release slug, an epoch-seconds announcement_datetime, the same moment in UTC and local time, and a flag showing whether the date is confirmed by the publisher.

import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.fxmacrodata.com/v1"
HEADERS = {"X-API-Key": API_KEY}

def get_upcoming_releases(currency: str) -> list[dict]:
    resp = requests.get(f"{BASE}/calendar/{currency}", headers=HEADERS, timeout=10)
    resp.raise_for_status()
    return resp.json().get("data", [])

usd_calendar = get_upcoming_releases("usd")

for event in usd_calendar[:5]:
    print(event["release"], event["announcement_datetime_utc"], event["release_date_confirmed"])

A run on 6 October 2026 listed the trade balance at 12:30 UTC that day, then consumer confidence, jobless claims, business confidence, and core CPI at 12:30 UTC on 14 October. When a contract resolves on "CPI for September 2026", this is the release time to anchor your horizon to.


Step 2 — Measure past prints against a forecast source

Announcement rows carry the released value (val), the previous_value, the change_from_previous, a month-end reference date, and the release time as epoch seconds in announcement_datetime. They do not carry a consensus column. Forecasts live in the predictions endpoint, labelled by source, and both endpoints share an announcement_id join key. For US CPI the external per-release source is the Cleveland Fed inflation nowcast:

def get_announcement_history(currency: str, indicator: str, limit: int = 24) -> list[dict]:
    resp = requests.get(f"{BASE}/announcements/{currency}/{indicator}",
                        params={"limit": limit}, headers=HEADERS, timeout=10)
    resp.raise_for_status()
    return resp.json().get("data", [])

def get_pre_release_forecasts(currency: str, indicator: str, source: str,
                              start_date: str) -> dict[str, dict]:
    resp = requests.get(f"{BASE}/predictions/{currency}/{indicator}",
                        params={"prediction_source": source, "pre_release_only": "true",
                                "start_date": start_date, "limit": 100},
                        headers=HEADERS, timeout=10)
    resp.raise_for_status()
    return {g["announcement_id"]: g for g in resp.json().get("data", [])}

def compute_surprise_series(records: list[dict], forecasts: dict[str, dict]) -> list[dict]:
    surprises = []
    for r in records:
        actual = r.get("val")
        group = forecasts.get(r.get("announcement_id"))
        if actual is None or not group or not group.get("predictions"):
            continue
        # Use the last forecast published before the release.
        latest = max(group["predictions"], key=lambda p: p["generated_at"])
        forecast = latest["predicted_value"]
        surprises.append({
            "date": r["date"],
            "actual": actual,
            "previous": r.get("previous_value"),
            "change": r.get("change_from_previous"),
            "forecast": forecast,
            "surprise": round(actual - forecast, 2),
            "released_at": r.get("announcement_datetime"),
        })
    return surprises

cpi_history = get_announcement_history("usd", "inflation", limit=24)
start = min(r["date"] for r in cpi_history)
cpi_nowcasts = get_pre_release_forecasts("usd", "inflation", "cleveland_fed_nowcast", start)
cpi_surprises = compute_surprise_series(cpi_history, cpi_nowcasts)

above = sum(1 for s in cpi_surprises if s["surprise"] > 0)
print(f"CPI y/y above the Cleveland Fed nowcast: {above}/{len(cpi_surprises)} releases")

Run in early October 2026, this printed 13 of 24 releases above the last pre-release Cleveland Fed nowcast. Keep in mind what the comparison is: a model nowcast, not a survey of traders and not the price of any contract. It gives you a measured bias for one forecast source, which you can then hold up against the probability a contract implies.


Step 3 — Map indicators to prediction market contracts

Macro contracts are usually written around a threshold on a specific release. The mapping to FXMacroData indicator slugs looks like this. Check each contract's rules for the exact measure: a contract on the monthly CPI change needs frequency=mom, and payroll contracts are written on the monthly change in jobs, not the level.

Prediction market contract type FXMacroData indicator slug Currency API docs
Will CPI exceed X%? inflation USD /api-data-docs/usd/inflation
Will Core CPI exceed X%? core_inflation USD /api-data-docs/usd/core_inflation
Will payrolls grow by more than X thousand? non_farm_payrolls_change USD /api-data-docs/usd/non_farm_payrolls_change
Will Fed cut/hold/hike at FOMC? policy_rate USD /api-data-docs/usd/policy_rate
Will GDP growth exceed X%? gdp USD /api-data-docs/usd/gdp
Will unemployment fall below X%? unemployment USD /api-data-docs/usd/unemployment
Will the ECB change rates at the next meeting? policy_rate EUR /api-data-docs/eur/policy_rate

The release calendar shows these schedules in one view. When a contract lists a resolution date, check it against the FXMacroData calendar to confirm both refer to the same release. Watch the measure too: an advance GDP estimate and a later revision are different numbers, and a contract settles on whichever one its rules name.


Step 4 — Pull the forecast for the next release

The predictions endpoint returns one group per release, keyed by announcement_id, with every forecast row labelled by source and stamped with generated_at. Filter by prediction_source and pick the next future release:

def get_next_forecasts(currency: str, indicator: str, source: str) -> dict | None:
    resp = requests.get(f"{BASE}/predictions/{currency}/{indicator}",
                        params={"prediction_source": source}, headers=HEADERS, timeout=10)
    resp.raise_for_status()
    upcoming = [g for g in resp.json().get("data", [])
                if g.get("announcement_timing") == "future" and g.get("predictions")]
    return min(upcoming, key=lambda g: g["announcement_datetime"]) if upcoming else None

next_cpi = get_next_forecasts("usd", "inflation", "cleveland_fed_nowcast")
if next_cpi:
    latest = max(next_cpi["predictions"], key=lambda p: p["generated_at"])
    print(f"Source: {latest['prediction_source_label']}")
    print(f"Nowcast: {latest['predicted_value']}% y/y for reference month {next_cpi['date']}")
    print(f"Release: {next_cpi['announcement_datetime_local']}")

Response shape, abridged to the latest Cleveland Fed nowcast for September 2026 CPI:

{
  "currency": "USD",
  "indicator": "inflation",
  "count": 7,
  "prediction_count": 36,
  "data": [
    {
      "announcement_id": "usd_inflation_2026-09-30",
      "currency": "usd",
      "indicator": "inflation",
      "date": "2026-09-30",
      "announcement_datetime": 1791981000,
      "announcement_datetime_local": "2026-10-14T08:30:00-04:00",
      "announcement_timing": "future",
      "predictions": [
        {
          "predicted_value": 3.6,
          "prediction_type": "model_nowcast",
          "prediction_class": "model_nowcast",
          "prediction_source": "cleveland_fed_nowcast",
          "prediction_source_label": "Cleveland Fed Inflation Nowcasting",
          "generated_at": 1791208800
        }
      ]
    }
  ]
}

The date is the reference month (month-end), and announcement_datetime is the scheduled release. Without a prediction_source filter the endpoint also returns other sources, including FXMacroData's own blended forecast, so filter explicitly when you want one source. In this run the nowcast for September 2026 CPI was 3.6% year on year, against an August print of 3.4%.


Step 5 — Build the decision signal

Combine the three inputs in one function. The logic is deliberately simple: the aim is a reproducible, data-grounded read, not a black box.

def prediction_market_signal(currency: str, indicator: str, source: str,
                             contract_threshold: float, contract_direction: str,
                             lookback: int = 24) -> dict:
    """contract_direction "above" means the contract pays YES if the print > threshold."""
    history = get_announcement_history(currency, indicator, limit=lookback)
    start = min(r["date"] for r in history)
    forecasts = get_pre_release_forecasts(currency, indicator, source, start)
    surprises = compute_surprise_series(history, forecasts)
    upcoming = get_next_forecasts(currency, indicator, source)

    forecast = release = None
    if upcoming:
        latest = max(upcoming["predictions"], key=lambda p: p["generated_at"])
        forecast = latest["predicted_value"]
        release = upcoming["announcement_datetime_local"]

    def hit(value: float) -> bool:
        return value > contract_threshold if contract_direction == "above" else value < contract_threshold

    prints = [r["val"] for r in history if r.get("val") is not None]
    base_rate = sum(hit(v) for v in prints) / len(prints) if prints else None
    mean_surprise = (sum(s["surprise"] for s in surprises) / len(surprises)
                     if surprises else None)
    signal = "NEUTRAL" if forecast is None else ("YES" if hit(forecast) else "NO")

    return {
        "currency": currency.upper(),
        "indicator": indicator,
        "forecast_source": source,
        "contract_threshold": contract_threshold,
        "contract_direction": contract_direction,
        "next_forecast": forecast,
        "next_release": release,
        "base_rate_last_n": round(base_rate, 2) if base_rate is not None else None,
        "mean_surprise_vs_forecast": round(mean_surprise, 2) if mean_surprise is not None else None,
        "surprise_sample": len(surprises),
        "lookback": lookback,
        "signal": signal,
    }


result = prediction_market_signal("usd", "inflation", "cleveland_fed_nowcast",
                                  contract_threshold=3.4, contract_direction="above")

import json
print(json.dumps(result, indent=2))

Output from a run in early October 2026:

{
  "currency": "USD",
  "indicator": "inflation",
  "forecast_source": "cleveland_fed_nowcast",
  "contract_threshold": 3.4,
  "contract_direction": "above",
  "next_forecast": 3.6,
  "next_release": "2026-10-14T08:30:00-04:00",
  "base_rate_last_n": 0.12,
  "mean_surprise_vs_forecast": 0.0,
  "surprise_sample": 24,
  "lookback": 24,
  "signal": "YES"
}

Read this as follows. The latest nowcast for September CPI is 3.6%, above the 3.4% threshold, so the raw signal is YES. Only 12% of the last 24 prints were above 3.4%, though, and the average gap between the print and the nowcast was zero. The forecast leans one way and recent history leans the other, so this is a low-conviction read. Compare it with the contract's implied probability before acting.


Step 6 — Extend to non-USD contracts

The same endpoints cover all 22 FXMacroData currencies, so non-USD contracts use the same code with a different currency, indicator, and forecast source. Forecast sources differ by currency, so check GET /v1/predictions/coverage/{currency} first. Where no external source publishes a per-release forecast, the FXMacroData blended forecast (fxmacrodata_blended) is available, labelled as FXMacroData's own.

# Euro area HICP against the ECB Survey of Professional Forecasters
eur_cpi_signal = prediction_market_signal("eur", "inflation", "ecb_survey_of_professional_forec",
                                          contract_threshold=2.0, contract_direction="above",
                                          lookback=18)

# UK CPI against the FXMacroData blended forecast (no external per-release source)
uk_cpi_signal = prediction_market_signal("gbp", "inflation", "fxmacrodata_blended",
                                         contract_threshold=3.0, contract_direction="above",
                                         lookback=18)

In the same run, the euro area example returned an ECB SPF forecast of 1.87% for the HICP release due on 1 December 2026 (the next release that source covers), below the 2.0% threshold, so the signal was NO. The UK example returned a blended forecast of 3.14%, above 3.0%, but with no surprise history, because the blended source had no past pre-release rows in that window. If a source has no upcoming forecast at all, the function returns NEUTRAL instead of guessing. The EUR/USD and GBP/USD dashboards show the policy-rate history and inflation trend for each pair; check them, along with recent ECB decisions, before you commit.


Step 7 — Add COT positioning as context

Speculative positioning in FX futures often reflects the same macro views that prediction markets price. The USD COT dashboard tracks CFTC positioning in the US Dollar Index futures contract on ICE Futures U.S. Pull it programmatically like this:

def get_cot(currency: str, limit: int = 4) -> list[dict]:
    resp = requests.get(f"{BASE}/cot/{currency}", params={"limit": limit},
                        headers=HEADERS, timeout=10)
    resp.raise_for_status()
    return resp.json().get("data", [])

usd_cot = get_cot("usd")
if usd_cot:
    latest = usd_cot[0]
    print(f"Week of {latest['date']}: non-commercial net {latest['noncommercial_net']:+,} contracts "
          f"(z-score {latest['noncommercial_net_zscore']:+.2f})")

For the week of 29 September 2026 this printed a non-commercial net long of +11,881 contracts, a z-score of +0.89. Treat positioning as context for how crowded a view is, not as a forecast of the print.


Putting it all together

The complete workflow looks like this:

  1. Pull the release calendar to identify upcoming releases and their scheduled times.
  2. Match each release to the contract you are studying on Kalshi or Polymarket, checking the measure and resolution source in its rules.
  3. Fetch the predictions endpoint for the latest pre-release forecast from a named source.
  4. Compare past prints with that source to measure its bias and the historical base rate.
  5. Generate a directional read by comparing the forecast with the contract threshold.
  6. Optionally add COT positioning from FX futures as context.
  7. Compare your read with the contract's implied probability, and size any position on the gap.
Important note on resolution timing
FXMacroData records each release time as announcement_datetime in epoch seconds, and releases typically arrive within seconds of official publication (see the release-speed record). Each contract settles under its own rules, however, so check that its stated resolution source (for example "BLS CPI, first release") matches the FXMacroData indicator and measure you are querying. Pass revisions=all on the announcements endpoint to see each row's vintages, so a backtest uses the first print rather than a later revision.

Get started

All endpoints used in this guide are available during the 14-day trial. USD announcement history, including CPI, payrolls, GDP, unemployment, and the policy rate, can also be read on the free tier without a key, delayed 15 minutes after each release. Non-USD indicators, predictions, and COT data need a paid plan.

FXMacroData API data

Data endpoints used in this article

No FXMacroData API data endpoint is attributed to this article. Its evidence base is identified in the article and source links.

Explore the FXMacroData API reference

Frequently asked

Questions about this topic

Which fields hold the released value and release time?

Announcement rows from /v1/announcements/{currency}/{indicator} carry the released value in val, plus previous_value, change_from_previous, a month-end reference date, and the release time as epoch seconds in announcement_datetime. They do not carry a consensus field.

Where do the forecasts come from?

The /v1/predictions/{currency}/{indicator} endpoint returns one group per release, keyed by announcement_id, with each forecast labelled by prediction_source and stamped with generated_at. For US CPI the external per-release source is the Cleveland Fed inflation nowcast; FXMacroData's own blended forecast is labelled fxmacrodata_blended. Check /v1/predictions/coverage/{currency} for the sources available in each currency.

Does FXMacroData connect to Kalshi or Polymarket?

No. The guide's code calls only the FXMacroData API. You compare its output with a contract's price and rules on the platform yourself, and each contract settles under its own stated resolution source.

Keep reading

Blogroll

AI Answer-Ready

Key Facts

Page
Using FXMacroData with Prediction Markets: Kalshi and Polymarket
Section
Articles
Canonical URL
https://fxmacrodata.com/articles/fxmacrodata-prediction-markets-kalshi-polymarket
Source
FXMacroData editorial and official publisher references
Last Updated
2026-10-05 22:59 UTC

Provenance And Trust

Cite the canonical URL and source field above. Where available, this page maps to official publisher releases and timestamped updates.

Quick Q&A

Which fields hold the released value and release time? Announcement rows from /v1/announcements/{currency}/{indicator} carry the released value in val, plus previous_value, change_from_previous, a month-end reference date, and the release time as epoch seconds in announcement_datetime. They do not carry a consensus field.

Where do the forecasts come from? The /v1/predictions/{currency}/{indicator} endpoint returns one group per release, keyed by announcement_id, with each forecast labelled by prediction_source and stamped with generated_at. For US CPI the external per-release source is the Cleveland Fed inflation nowcast; FXMacroData's own blended forecast is labelled fxmacrodata_blended. Check /v1/predictions/coverage/{currency} for the sources available in each currency.

Does FXMacroData connect to Kalshi or Polymarket? No. The guide's code calls only the FXMacroData API. You compare its output with a contract's price and rules on the platform yourself, and each contract settles under its own stated resolution source.

Prompt Packs

Use these in ChatGPT, Claude, Gemini, Mistral, Perplexity, or Grok for consistent source-aware outputs.