経済指標カレンダーを提供するプロダクトチームには、ユーザーごとの国フィルター、閲覧者ごとのタイムゾーンでの発表時刻、日程が動いた発表の見せ方が必要です。Narwhal は UTC の時刻、発表元のタイムゾーン、対象期間、前回値、改定カウンターを付けて発表を返し、アプリがフィルター、時計、キャッシュを加えます。
アプリがカレンダー API に求めること
経済指標カレンダーを自社アプリに載せるプロダクトチームには、データ提供元に任せられない仕事が3つあります。ユーザーごとに国を選ばせること、すべての時刻を閲覧者のタイムゾーンで表示すること、発表が動いたときに画面を正しく保つことです。API は発表を提供し、アプリがフィルター、時計、キャッシュを担います。GET /v1/economics/calendar は countries(国ごとに繰り返し、USA や CAN のような3文字コードを使います)、start_date、end_date、limit、cursor を受け取ります。このリクエストは2か国の2日分を取得します。
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/economics/calendar?countries=USA&countries=CAN&start_date=2026-11-05&end_date=2026-11-06&limit=100"{
"releases": [
{
"release_id": "1b503ce9-69ee-4ec4-8dd9-deba7f1a6554",
"revision": 2,
"country": "USA",
"series_id": "usa-employment-report",
"name": "Employment report",
"topic": "employment",
"family": "labour",
"period": "2026-10",
"status": "scheduled",
"at": "2026-11-06T13:30:00Z",
"timezone": "America/New_York",
"values": [
{
"measure": "nonfarm_payrolls_change",
"concept": "payrolls",
"unit": "thousands",
"transformation": "change",
"previous": "29"
},
{
"measure": "unemployment_rate",
"concept": "unemployment_rate",
"unit": "percent",
"previous": "4.2"
}
]
},
{
"release_id": "20c30c26-9a9a-4218-a8fb-6233ac8d291a",
"revision": 1,
"country": "USA",
"series_id": "usa-productivity-and-costs",
"name": "Productivity and costs",
"topic": "other",
"family": "labour",
"period": "2026-Q3",
"stage": "preliminary",
"status": "scheduled",
"at": "2026-11-05T13:30:00Z",
"timezone": "America/New_York",
"values": []
}
],
"next_cursor": null
}完全なレスポンスには5件の発表が入っています。at は UTC の発表時刻、timezone は発表元自身のタイムゾーンで、アプリは at を閲覧者ごとに変換し、発表元のタイムゾーンを2つ目のラベルとして出すこともできます。period は対象期間で、values[].previous はその前の数値です。release_id はリクエストをまたいで発表を識別し、内容が変わると revision が上がります。values は常に存在し、空のリストになることもあります。
国ごとにキャッシュし、閲覧者のタイムゾーンで描画する
ユーザーが選ぶ国の組み合わせは異なるので、国ごとに別々にキャッシュし、ユーザーごとにリストをマージします。米国をフォローしている2人は、1つのキャッシュ済みリストを共有します。
- 国を1つずつ取得する
country_releasesは1つの国を要求し、next_cursorがnullになるまで続けます。1ページには最大100件の発表が入るためです。 - 回答を短い期間だけ保持する
TTLは、キャッシュされた国が新鮮とみなされる秒数です。変更された時刻をユーザーがどのくらい早く見る必要があるかで選びます。 - ユーザー向けにマージして変換する
user_calendarはユーザーの国を集め、ユーザーのタイムゾーンで各atを変換し、現地時刻で並べ替えます。ZoneInfoが閲覧者の時計の変更(夏時間など)を適用するので、アプリはオフセットを保存しません。import os import time from datetime import datetime from zoneinfo import ZoneInfo import requests BASE = "https://api.narwhalapi.com" HEADERS = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"} TTL = 300 # seconds a cached country stays fresh cache = {} # (country, start, end) -> (fetched_at, releases) def country_releases(country, start, end): key = (country, start, end) hit = cache.get(key) if hit and time.time() - hit[0] < TTL: return hit[1] params = {"countries": country, "start_date": start, "end_date": end, "limit": 100} releases = [] while True: r = requests.get(f"{BASE}/v1/economics/calendar", params=params, headers=HEADERS, timeout=10) r.raise_for_status() body = r.json() releases += body["releases"] if not body["next_cursor"]: break params["cursor"] = body["next_cursor"] cache[key] = (time.time(), releases) return releases def user_calendar(user, start, end): zone = ZoneInfo(user["tz"]) rows = [] for country in user["countries"]: for x in country_releases(country, start, end): if "at" in x: local = datetime.fromisoformat(x["at"].replace("Z", "+00:00")).astimezone(zone) rows.append((local, x)) return sorted(rows, key=lambda t: t[0]) user = {"countries": ["USA", "CAN"], "tz": "Asia/Jakarta"} day = None for local, x in user_calendar(user, "2026-11-05", "2026-11-06"): if local.date() != day: day = local.date() print(day.isoformat()) print(f" {local:%H:%M} {x['country']} {x['name']} {x.get('period', '-')}")
下の出力は、米国とカナダをフォローするジャカルタのユーザーの例です。Credit statistics の発表は11月6日の20:00 UTC で、ジャカルタでは11月7日の03:00なので、そのユーザーの画面では別の日付になります。UTC の日付ではなく、必ず閲覧者の現地の日付でグループ化してください。
2026-11-05
20:30 USA Productivity and costs 2026-Q3
20:30 USA Jobless claims 2026-10-31
2026-11-06
20:30 USA Employment report 2026-10
20:30 CAN Labour force survey 2026-10
2026-11-07
03:00 USA Credit statistics 2026-09発表が動いたとき画面を正しく保つ
表示する各行と一緒に release_id と revision を保存します。更新後、保存済みより revision が高い行は変更されているので、再描画して新しい時刻にフラグを付けます。ステータスが cancelled の発表には取り消し線を付けるか非表示にし、その期間に返ってこなくなった行は別の日付に移った可能性があるので、そのビューから外します。
発表によっては正確な時刻がなく、at の代わりに date、between、by が付いています。上のスクリプトはそれらをスキップします。実際のアプリでは、その日の先頭に終日の項目として並べてください。プッシュ通知には、webhook が release.scheduled、release.rescheduled、release.cancelled を通知します。webhook の受信 を参照してください。
プロダクトチームが一度だけ決めること
| 決定 | 選択 | フィールドまたはパラメータ |
|---|---|---|
| ユーザーごとの国フィルター | ユーザーの国を送り、国ごとに1つのキャッシュ済みリストを持つ | countries |
| 表示する時刻 | UTC の時刻を閲覧者のタイムゾーンに変換し、発表元のタイムゾーンをラベルとして表示する | at, timezone |
| 日付のグループ化 | 閲覧者の現地の日付でグループ化する | at |
| 変更の検出 | 保存済みの revision と新しい revision を比べる | release_id, revision |
| トピックまたはファミリーのチップ | リクエストにトピックのパラメータがないので、アプリ側でフィルターする | family, topic |
| レート制限とリトライ | キャッシュを使い、429 レスポンスではバックオフする。レスポンスのキャッシュと 429 エラーの処理 を参照 | レスポンスヘッダー |
ユーザーはトピックやファミリーで絞り込めますか。
リクエストは国、日付、limit、cursor を受け取り、トピックのフィルターはありません。すべての発表に family があり、ほとんどに topic があるので、自分のコードで絞り込みます。トピックのない発表があり、通常のルックアップではキーエラーになるため、x.get("topic") を使ってください。
日付の範囲は UTC と発表元の現地日付のどちらに従いますか。
発表元の現地日付に従います。UTC の23:30や23:50に出る日本の発表は、東京では翌朝になるので、翌日の範囲に入ります。アプリが閲覧者のタイムゾーンで1日を組み立てるときは、前後に1日ずつ余分にリクエストし、at で絞り込んでください。
1画面に何回のリクエストが必要ですか。
キャッシュ内の国ごとに1回、next_cursor に値があるページごとにもう1回です。キャッシュ済みの国は、有効期間が切れるまでコストがかかりません。
公開。