Where macro data meets prediction markets
Prediction markets have moved from curiosity to infrastructure. Kalshi — the first CFTC-regulated prediction exchange in the United States — lets you trade contracts on outcomes like "Will US CPI exceed 3.5% in April?" or "Will the Federal Reserve cut rates at the June meeting?" Polymarket, running on the Polygon blockchain, offers similar binary-outcome markets on macro events with global open access. Both platforms price probability in real time, and both are sensitive to exactly the same macro data releases that move USD/JPY, EUR/USD, and the rest of the FX complex.
If you already use FXMacroData to track central bank calendars, monitor CPI surprises, and pull policy rate history, you have the raw ingredients to build a systematic edge on these markets. This guide shows you how to connect the dots: pull structured macro data from FXMacroData, map it to open prediction market contracts, and build a reproducible decision workflow in Python.
By the end of this guide you will have a Python script that fetches upcoming macro announcements from FXMacroData, matches them to relevant prediction market contracts on Kalshi and Polymarket, and computes a directional signal from historical surprise data to help inform your position.
Prerequisites
- A FXMacroData API key — subscribe here for a free trial.
- Python 3.10 or later with
requestsinstalled (pip install requests). - A Kalshi account with API access enabled, or a Polymarket wallet for manual cross-reference.
- Basic familiarity with what CPI, Non-Farm Payrolls, and policy rate decisions are.
Step 1 — Pull the upcoming release calendar
The first thing you need is a clear picture of what announcements are coming and when. FXMacroData's release calendar endpoint returns all scheduled macro events for a currency, with their expected dates and announcement times.
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://fxmacrodata.com/api/v1"
def get_upcoming_releases(currency: str) -> list[dict]:
url = f"{BASE}/calendar/{currency}?api_key={API_KEY}"
resp = requests.get(url, 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["indicator"], event.get("announcement_datetime"), event.get("next_release_date"))
Each entry carries an indicator slug (e.g. inflation, non_farm_payrolls, policy_rate), the scheduled announcement time as a Unix timestamp, and the next expected release date. This is your primary input for filtering open prediction market contracts — if a contract resolves on "CPI for March 2026" you need the exact announcement date to size your time horizon correctly.
Step 2 — Fetch historical announcement data and compute the surprise series
Prediction markets price probability based on available information. If you can compute a recent-history surprise index for a given indicator — how often actual prints beat consensus, and by how much — you have a baseline for calibrating your position. Pull the last twelve months of actual vs. forecast data from the announcements endpoint:
def get_announcement_history(currency: str, indicator: str, limit: int = 24) -> list[dict]:
url = f"{BASE}/announcements/{currency}/{indicator}?api_key={API_KEY}&limit={limit}"
resp = requests.get(url, timeout=10)
resp.raise_for_status()
return resp.json().get("data", [])
def compute_surprise_series(records: list[dict]) -> list[dict]:
surprises = []
for r in records:
actual = r.get("actual_value")
consensus = r.get("predicted_value")
if actual is not None and consensus is not None:
surprises.append({
"date": r["date"],
"actual": actual,
"consensus": consensus,
"surprise": actual - consensus,
"direction": "beat" if actual > consensus else "miss",
})
return surprises
cpi_history = get_announcement_history("usd", "inflation", limit=24)
cpi_surprises = compute_surprise_series(cpi_history)
beat_count = sum(1 for s in cpi_surprises if s["direction"] == "beat")
miss_count = len(cpi_surprises) - beat_count
print(f"CPI beat rate (last 24 releases): {beat_count}/{len(cpi_surprises)} = {beat_count/len(cpi_surprises):.0%}")
The predicted_value field on each announcement record stores the market consensus from authoritative sources (Philadelphia Fed Survey of Professional Forecasters for USD indicators). This is the same consensus signal that anchors prediction market pricing, so your surprise series will be directly comparable to the implicit probability embedded in a Kalshi CPI contract.
Step 3 — Map indicators to prediction market contracts
Both Kalshi and Polymarket structure their macro contracts around specific indicator thresholds. The mapping is straightforward once you know the FXMacroData indicator slugs. Below is a reference table for the most liquid contracts:
| 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 NFP exceed X,000? | non_farm_payrolls |
USD | /api-data-docs/usd/non_farm_payrolls |
| Will Fed cut/hold/hike at FOMC? | policy_rate |
USD | /api-data-docs/usd/policy_rate |
| Will GDP growth exceed X%? | gdp_quarterly |
USD | /api-data-docs/usd/gdp_quarterly |
| Will unemployment fall below X%? | unemployment |
USD | /api-data-docs/usd/unemployment |
| Will ECB cut rates at next meeting? | policy_rate |
EUR | /api-data-docs/eur/policy_rate |
The release calendar surfaces all of these indicator schedules in a single view. When a Kalshi contract lists a resolution date, cross-reference it with the next_release_date from the FXMacroData calendar to confirm they resolve on the same underlying announcement. Mismatches — where the contract resolves on an advance estimate versus a final revision — are a common source of mispricing.
Step 4 — Query the predictions endpoint for consensus anchoring
FXMacroData's predictions endpoint provides forward-looking consensus values sourced from official survey data — the same surveys that prediction market participants use to anchor their priors. Pull the current consensus for the next CPI release and compare it to the threshold in the open Kalshi contract:
def get_predictions(currency: str, indicator: str) -> list[dict]:
url = f"{BASE}/predictions/{currency}/{indicator}?api_key={API_KEY}"
resp = requests.get(url, timeout=10)
resp.raise_for_status()
return resp.json().get("data", [])
cpi_predictions = get_predictions("usd", "inflation")
# Most recent upcoming prediction
if cpi_predictions:
next_pred = cpi_predictions[0]
for p in next_pred.get("predictions", []):
print(f"Source: {p['prediction_source_label']}")
print(f"Consensus: {p['predicted_value']}%")
print(f"For release date: {next_pred['date']}")
Sample response shape:
{
"currency": "USD",
"indicator": "inflation",
"count": 1,
"prediction_count": 1,
"data": [
{
"announcement_id": "usd_inflation_2026-05-14",
"currency": "usd",
"indicator": "inflation",
"date": "2026-05-14",
"announcement_datetime": 1747216200,
"predictions": [
{
"predicted_value": 2.8,
"prediction_type": "market_consensus",
"prediction_source": "philly_fed_spf",
"prediction_source_label": "Philadelphia Fed Survey of Professional Forecasters"
}
]
}
]
}
If a Kalshi contract asks "Will April CPI print above 2.9%?" and the SPF consensus is 2.8%, you now have a quantified starting point: the consensus says no with a 0.1 percentage point buffer. Your historical surprise series from Step 2 then tells you how often CPI has beaten consensus by more than 0.1 percentage point, giving you an empirical base rate to compare against the contract's implied probability.
Step 5 — Build the full decision signal
Assemble the three inputs into a single decision function. The logic is intentionally simple — the goal is a reproducible, data-grounded signal, not a black box:
def prediction_market_signal(
currency: str,
indicator: str,
contract_threshold: float,
contract_direction: str, # "above" or "below"
lookback: int = 24,
) -> dict:
"""
Returns a signal dict for a prediction market contract.
contract_threshold: the numeric threshold in the contract question
(e.g. 2.9 for "Will CPI exceed 2.9%?")
contract_direction: "above" means YES if actual > threshold
"""
history = get_announcement_history(currency, indicator, limit=lookback)
surprises = compute_surprise_series(history)
predictions = get_predictions(currency, indicator)
consensus = None
release_date = None
if predictions:
latest_preds = predictions[0].get("predictions", [])
if latest_preds:
consensus = latest_preds[0]["predicted_value"]
release_date = predictions[0]["date"]
# Base rate: how often did actual exceed the threshold historically?
actuals_above = sum(1 for r in history if r.get("actual_value") is not None
and r["actual_value"] > contract_threshold)
base_rate = actuals_above / len(history) if history else None
# Surprise bias: mean surprise (positive = beat)
mean_surprise = (sum(s["surprise"] for s in surprises) / len(surprises)
if surprises else None)
# Directional signal
if consensus is not None:
buffer = consensus - contract_threshold # positive = consensus above threshold
signal = "NO" if (contract_direction == "above" and buffer > 0) else "YES"
else:
signal = "NEUTRAL"
return {
"currency": currency.upper(),
"indicator": indicator,
"contract_threshold": contract_threshold,
"contract_direction": contract_direction,
"consensus": consensus,
"release_date": release_date,
"historical_base_rate_above_threshold": base_rate,
"mean_surprise_last_n": mean_surprise,
"lookback": lookback,
"signal": signal,
}
result = prediction_market_signal(
currency="usd",
indicator="inflation",
contract_threshold=2.9,
contract_direction="above",
lookback=24,
)
import json
print(json.dumps(result, indent=2))
Output example:
{
"currency": "USD",
"indicator": "inflation",
"contract_threshold": 2.9,
"contract_direction": "above",
"consensus": 2.8,
"release_date": "2026-05-14",
"historical_base_rate_above_threshold": 0.33,
"mean_surprise_last_n": 0.04,
"lookback": 24,
"signal": "NO"
}
Read this as: consensus is 2.8% (below the 2.9% threshold), the 24-release historical base rate for CPI printing above 2.9% is 33%, and the mean surprise has been a modest +0.04 ppt upward bias. The raw signal is NO, but the combination of base rate and surprise bias tells you this is not a high-conviction lean — adjust position size accordingly.
Step 6 — Extend to non-USD and cross-market contracts
Prediction markets increasingly list contracts on non-USD macro events: ECB decisions, UK CPI, Bank of Japan policy, and Australian employment. FXMacroData covers all of these through the same endpoint structure — swap the currency code and indicator slug and the workflow is identical.
# ECB rate decision signal
ecb_signal = prediction_market_signal(
currency="eur",
indicator="policy_rate",
contract_threshold=3.15, # "Will ECB rate fall below 3.15%?"
contract_direction="below",
lookback=12,
)
# UK inflation signal
uk_cpi_signal = prediction_market_signal(
currency="gbp",
indicator="inflation",
contract_threshold=2.5,
contract_direction="above",
lookback=18,
)
# Australian employment signal
aus_employment_signal = prediction_market_signal(
currency="aud",
indicator="employment",
contract_threshold=30.0, # thousands, "Will employment add 30K+?"
contract_direction="above",
lookback=18,
)
The EUR/USD and GBP/USD dashboards surface the policy rate history and CPI trend for each pair, giving you a visual context check before committing to a position. If the dashboard shows the ECB has been cutting consistently over the last four meetings, a contract asking "Will the ECB cut in June?" has very different base-rate dynamics than one asking "Will the ECB hike?"
Step 7 — Monitor COT positioning as a confirmation layer
Kalshi and Polymarket contracts on macro outcomes are not traded in isolation — speculative positioning in FX futures often reflects the same directional views. The COT dashboard for USD shows whether large speculators are net long or short dollars heading into a CPI print. If specs are heavily net long USD and the CPI signal is NO (below threshold), there is a potential double edge: the prediction market contract may be worth fading, and the FX position may also be vulnerable to a squeeze.
Pull COT data programmatically to include it in your signal framework:
def get_cot(currency: str) -> list[dict]:
url = f"{BASE}/cot/{currency}?api_key={API_KEY}&limit=4"
resp = requests.get(url, timeout=10)
resp.raise_for_status()
return resp.json().get("data", [])
usd_cot = get_cot("usd")
if usd_cot:
latest = usd_cot[0]
net_position = latest.get("net_position")
print(f"USD spec net position (latest week): {net_position:+,.0f} contracts")
Putting it all together
The complete workflow looks like this:
- Pull the release calendar to identify upcoming announcements and their scheduled times.
- Match announcement dates to open prediction market contracts on Kalshi or Polymarket.
- Fetch the predictions endpoint for the current consensus value.
- Compute the historical surprise series to measure directional bias and base rate.
- Generate a directional signal by comparing consensus to the contract threshold.
- Optionally layer in COT positioning from FX futures as a confirmation signal.
- Size your position based on the gap between the implied probability in the contract and your empirical base rate estimate.
FXMacroData provides second-level
announcement_datetime timestamps for every release — critically important for prediction markets that resolve within seconds of the official print. Always verify that the contract's stated resolution source (e.g. "BLS CPI for March 2026, first release") matches exactly the FXMacroData indicator and revision flag you are querying. Advance estimates and final revisions are stored as separate data points.
Get started
All endpoints used in this guide are available on a free trial — no credit card required. USD indicators including CPI, NFP, GDP, unemployment, Core PCE, and the policy rate are all accessible on the free tier. Non-USD indicators and COT data require a Professional plan.
- Subscribe and get your API key: /subscribe
- Release calendar: /dashboard/release-calendar
- USD indicator catalogue: /api-data-docs/usd
- COT dashboard: /dashboard/cot