To use market odds as an input, read the policy path on a schedule and subtract the earlier expected move from the later one. Narwhal gives expected_change_bps for each meeting, live and as daily history.
Poll the policy path
The signal here is the market-implied move of a central bank policy rate. The policy path returns it for each upcoming meeting. This request reads the Federal Reserve and keeps the first meeting.
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/predictions/policy-paths/federal-reserve"{
"as_of": "2026-11-03T02:28:00Z",
"meetings": [
{
"event_id": "usa-fed-2026-12",
"p_cut": "0.0097",
"p_hold": "0.2534",
"p_hike": "0.7369",
"expected_change_bps": "18.6",
"cumulative_change_bps": "18.6",
"resolves_at": "2026-12-09T19:00:00Z"
}
]
}expected_change_bps is the probability weighted change in the policy rate at that meeting, in basis points. It is a change and never the current rate. cumulative_change_bps adds the meetings in the list in order. p_cut, p_hold and p_hike are the odds behind it, and as_of is the time of the quotes. Poll this endpoint and store each reading with its as_of, one row per meeting, with the four odds and move fields kept as Decimal values.
Read the same path on past days
The history endpoint returns one snapshot per day. start and end are both required and are date-time values, so 2026-10-26 alone is rejected and 2026-10-26T00:00:00Z is accepted. The window can be at most 366 days. This request asks for 26 October to 2 November 2026, and the response below keeps two days and the first meeting of each.
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/predictions/policy-paths/federal-reserve/history?start=2026-10-26T00:00:00Z&end=2026-11-03T00:00:00Z"{
"meeting_dates": [
{
"event_id": "usa-fed-2026-10",
"resolves_at": "2026-10-28T18:00:00Z"
},
{
"event_id": "usa-fed-2026-12",
"resolves_at": "2026-12-09T19:00:00Z"
}
],
"days": [
{
"date": "2026-11-01",
"meetings": [
{
"event_id": "usa-fed-2026-12",
"p_cut": "0.0093",
"p_hold": "0.2766",
"p_hike": "0.7141",
"expected_change_bps": "17.9",
"cumulative_change_bps": "17.9"
}
]
},
{
"date": "2026-11-02",
"meetings": [
{
"event_id": "usa-fed-2026-12",
"p_cut": "0.0098",
"p_hold": "0.2535",
"p_hike": "0.7367",
"expected_change_bps": "18.5",
"cumulative_change_bps": "18.5"
}
]
}
]
}days holds one entry per date, and each entry holds the meetings that were still open on that date, with the same odds fields as the live path. meeting_dates gives the resolves_at of each meeting once.
Compute the change between two readings
The change in the expected move is the later expected_change_bps minus the earlier one for the same meeting. Match the readings on event_id, not on position. On 26 October 2026 the first row is the October meeting, usa-fed-2026-10, and by 2 November 2026 that meeting has been decided, so the first row is the December meeting. The script does the subtraction with Decimal and then repeats it between two live polls.
import json
import os
import time
import urllib.request
from decimal import Decimal
def get(path):
req = urllib.request.Request(
"https://api.narwhalapi.com" + path,
headers={"Authorization": "Bearer " + os.environ["NARWHAL_API_KEY"]},
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
def reading(meetings):
return {m["event_id"]: (Decimal(m["expected_change_bps"]), Decimal(m["cumulative_change_bps"]))
for m in meetings}
def change(before, after):
# Match meetings by event_id: the first row is a different meeting once a decision has passed.
return {e: (after[e][0] - before[e][0], after[e][1] - before[e][1])
for e in before if e in after}
path = "/v1/predictions/policy-paths/federal-reserve/history"
days = get(path + "?start=2026-10-26T00:00:00Z&end=2026-11-03T00:00:00Z")["days"]
first, last = days[0], days[-1]
print(first["date"], "->", last["date"])
for event_id, (move, cumulative) in list(change(reading(first["meetings"]),
reading(last["meetings"])).items())[:4]:
print(event_id, f"{move:+} bps at the meeting, {cumulative:+} bps cumulative")
# Polling: take a reading, wait, take another and run the same comparison.
def poll():
return reading(get("/v1/predictions/policy-paths/federal-reserve")["meetings"])
previous = poll()
time.sleep(120)
for event_id, (move, _) in list(change(previous, poll()).items())[:2]:
print(event_id, f"{move:+} bps since the last poll")
| Meeting | 26 October 2026 | 2 November 2026 | Change in expected move |
|---|---|---|---|
usa-fed-2026-12 | 19.5 bps | 18.5 bps | -1.0 bps |
usa-fed-2027-01 | 7.3 bps | 6.1 bps | -1.2 bps |
The two live polls, taken two minutes apart, printed +0.0 bps for both meetings. A change of zero is a valid reading. It means the quotes did not move at the one decimal the field carries. A negative change means the expected move shifted toward a cut, and a positive change means it shifted toward a hike. A field value below zero means the market expects a cut at that meeting.
What to check before you use the number
- Compare per meeting values with each other. The cumulative change from 26 October to 2 November is -2.7 bps for the December meeting, but the per meeting change is -1.0 bps. The October meeting was decided in between and its 1.7 bps left the running total.
expected_change_bpscarries one decimal, so a change smaller than 0.1 bps cannot appear.- Check
as_ofon each reading. Two readings with the sameas_ofcome from the same venue check. - A central bank with no priced meeting returns an empty
meetingslist. - The numbers are market quotes. Narwhal does not say whether a change in them predicts anything, and this page makes no claim that it does.
Can I get an alert instead of polling?
Yes. The guide on getting an alert when prediction odds move describes the option. Polling suits a script that already runs on a schedule, such as the one above, and an alert suits a threshold you want to be told about.
How do I seed a store with past readings?
Call the history endpoint in windows of up to 366 days and load each days entry as a reading dated at its date. Then add live polls from the policy path. The two sources carry the same odds and move fields, so a comparison can span the join. The daily snapshots are one per date, so a poll between two snapshots has no history row to compare with.
Why did the first meeting change between two days?
A path lists the meetings that are still open. Once the Federal Reserve decided on 28 October 2026, the October meeting left the list and the December meeting moved to the first row. In the history response, the entry for 28 October still lists the October meeting first, and the entry for 29 October starts with the December meeting.
Which central banks does the path cover?
The path answers for federal-reserve, and the same endpoint also answers for european-central-bank, bank-of-japan, bank-of-canada and reserve-bank-of-australia. Use central bank rate odds for how to read one meeting in detail.
Published .