# Using FXMacroData with Prediction Markets: Kalshi and Polymarket

Canonical: https://fxmacrodata.com/articles/fxmacrodata-prediction-markets-kalshi-polymarket
Author: FXMacroData
Published: 2026-05-05T14:30:00Z

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.

## Where macro data meets prediction markets

Prediction markets now list contracts on scheduled macro events.[Kalshi](https://kalshi.com) , an exchange regulated by the US Commodity Futures Trading Commission (CFTC), lists event contracts on outcomes such as the next[US CPI](/api-data-docs/usd/inflation) print or the result of a[Federal Reserve](/dashboard/press-releases/usd) meeting.[Polymarket](https://polymarket.com) , 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](/dashboard/USD_JPY) ,[EUR/USD](/dashboard/EUR_USD) , and the rest of the FX complex is the underlying release.

If you already use FXMacroData to track release calendars,[CPI](/articles/inflation-indicators-fx-guide) 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](/subscribe) 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](/articles/labor-statistics-fx-trading) , and[policy rate](/api-data-docs/usd/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](/indicators/trade-balance) at 12:30 UTC that day, then[consumer confidence](/indicators/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](/indicators/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](/united-states)[/api-data-docs/usd/inflation](/api-data-docs/usd/inflation) Will Core CPI exceed X%?`core_inflation`USD[/api-data-docs/usd/core_inflation](/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](/articles/federal-reserve) ?`policy_rate`USD[/api-data-docs/usd/policy_rate](/api-data-docs/usd/policy_rate) Will[GDP](/indicators/gdp) growth exceed X%?`gdp`USD[/api-data-docs/usd/gdp](/api-data-docs/usd/gdp) Will unemployment fall below X%?`unemployment`USD[/api-data-docs/usd/unemployment](/api-data-docs/usd/unemployment) Will the ECB change rates at the next meeting?`policy_rate`[EUR](/eurozone)[/api-data-docs/eur/policy_rate](/api-data-docs/eur/policy_rate)

The[release calendar](/dashboard/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](/dashboard/EUR_USD) and[GBP/USD](/dashboard/GBP_USD) dashboards show the policy-rate history and inflation trend for each pair; check them, along with recent[ECB](/dashboard/press-releases/eur) 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](/dashboard/cot/usd) 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:

- Pull the**release calendar**to identify upcoming releases and their scheduled times.

- Match each release to the**contract you are studying**on Kalshi or Polymarket, checking the measure and resolution source in its rules.

- Fetch the**predictions endpoint**for the latest pre-release forecast from a named source.

- Compare**past prints with that source**to measure its bias and the historical base rate.

- Generate a**directional read**by comparing the forecast with the contract threshold.

- Optionally add**[COT positioning](/articles/cot-report-guide-fx-traders)**from FX futures as context.

- 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](/release-speed) ). 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.

- **Subscribe and get your API key**:[/subscribe](/subscribe)

- **Release calendar**:[/dashboard/release-calendar](/dashboard/release-calendar)

- **USD indicator catalogue**: /api-data-docs/usd

- **COT dashboard**:[/dashboard/cot](/dashboard/cot)

Source: FXMacroData
