DuckDB API로 Narwhal 데이터 조회하기

DuckDB와 Narwhal API를 쓰면 공휴일, 발표일, 경제 데이터, 원자재, 환율, 예측 시장 확률을 SQL로 조회할 수 있습니다. 작은 Python 함수가 API 키로 JSON을 가져와 응답을 파일로 저장하고, DuckDB가 그 파일을 읽어 테이블로 만듭니다.

설정할 내용

DuckDB는 read_json으로 로컬 JSON을 읽으며, 이 구성에서는 데이터 영역마다 테이블이 있는 narwhal.duckdb 데이터베이스가 만들어집니다. API 요청과 응답 파일 저장은 Python이 맡고, SQL은 그 파일을 읽습니다. 전체 예제에는 공휴일과 발표일 건수, CPI 관측값, Brent 가격, 환율, 연방준비제도 정책 확률을 조회하는 쿼리가 들어 있습니다. Narwhal은 하나의 계정과 하나의 API 키로 이 API들을 제공하며, REST와 MCP가 같은 키를 공유합니다.

API 키 설정

  1. 무료 키 만들기 narwhalapi.com/signup에서 무료 키를 만드세요. 예제의 모든 데이터 영역에 같은 Narwhal API 키를 씁니다. 요청 헬퍼는 이 키를 Authorization 헤더에 Bearer YOUR_KEY 형태로 담아 보냅니다.
  2. 환경 변수에 키 저장하기 Python 파일용으로 NARWHAL_API_KEY라는 환경 변수를 설정하세요. Python 설정은 os.environ으로 이를 읽어 fetch가 쓰는 헤더에 넣으므로, 키가 SQL 문장에 나타나지 않습니다.
  3. Python 패키지 설치하기 Python 파일용으로 duckdb와 requests를 설치하세요. 예제는 두 패키지를 모두 가져와서, requests로 각 응답을 받고 duckdb.connect로 데이터베이스를 엽니다.
    DuckDB와 requests 설치하기
    pip install duckdb requests

코드로 데이터 가져오기

Python 예제를 파일로 저장하고, 환경에서 NARWHAL_API_KEY를 사용할 수 있는 상태로 실행하세요. fetch를 호출할 때마다 이름이 붙은 JSON 파일이 쓰이고, 이어지는 SQL이 그 파일을 대응하는 테이블로 읽어 들입니다. 헬퍼는 https://api.narwhalapi.com으로 GET 요청을 보내고, 응답을 쓰기 전에 raise_for_status를 호출합니다. 마지막 쿼리들은 같은 DuckDB 연결로 만들어진 테이블을 읽습니다. 특정 영역만 가져오려면 이 헬퍼와 연결을 쓰는 더 짧은 영역별 예시를 사용하세요.

DuckDB, 여섯 영역을 테이블로
import os
import duckdb
import requests

API = "https://api.narwhalapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"}


def fetch(name, path, **params):
    r = requests.get(API + path, params=params, headers=HEADERS, timeout=10)
    r.raise_for_status()
    with open(f"{name}.json", "w") as file:
        file.write(r.text)


con = duckdb.connect("narwhal.duckdb")

fetch("holidays", "/v1/calendars/holidays/USA/2026")
con.sql(
    "CREATE OR REPLACE TABLE holidays AS "
    "SELECT unnest(holidays, recursive := true) "
    "FROM read_json('holidays.json')"
)

fetch(
    "releases",
    "/v1/economics/calendar",
    countries="USA",
    start_date="2026-11-01",
    end_date="2026-11-30",
    limit=100,
)
con.sql(
    "CREATE OR REPLACE TABLE releases AS "
    "SELECT unnest(releases, recursive := true) "
    "FROM read_json('releases.json')"
)

fetch("cpi", "/v1/economics/USA/cpi/observations", start="2025-09")
con.sql(
    "CREATE OR REPLACE TABLE cpi AS "
    "SELECT unnest(observations, recursive := true) "
    "FROM read_json('cpi.json')"
)

fetch("brent", "/v1/commodities/spot/BRENT/USD")
con.sql(
    "CREATE OR REPLACE TABLE brent AS SELECT price, unit FROM read_json('brent.json')"
)

fetch("fx", "/v1/fx/rates/USD", currencies="EUR,IDR")
con.sql(
    "CREATE OR REPLACE TABLE fx AS "
    "SELECT unnest(rates, recursive := true) "
    "FROM read_json('fx.json')"
)

fetch("fed", "/v1/predictions/policy-paths/federal-reserve")
con.sql(
    "CREATE OR REPLACE TABLE fed AS "
    "SELECT unnest(meetings, recursive := true) "
    "FROM read_json('fed.json')"
)

print(
    con.sql("SELECT count(*) FROM holidays").fetchall(),
    con.sql("SELECT count(*) FROM releases").fetchall(),
)
print(con.sql("SELECT period, value FROM cpi ORDER BY period DESC LIMIT 2").fetchall())
print(
    con.sql("SELECT * FROM brent").fetchall(),
    con.sql("SELECT currency, rate FROM fx").fetchall(),
)
print(
    con.sql(
        "SELECT p_hold, expected_change_bps FROM fed ORDER BY resolves_at LIMIT 1"
    ).fetchall()
)

일정에 맞춰 실행하기

가져오기를 반복하려면 cron에서 Python 파일을 실행하세요. 요청 헤더가 환경 변수에서 키를 읽으므로, 예약 실행되는 그 Python 파일에서 NARWHAL_API_KEY를 사용할 수 있게 해야 합니다. 호출할 때마다 응답을 다시 가져와 DuckDB가 읽기 전에 JSON 파일을 씁니다. CREATE OR REPLACE TABLE 덕분에 실행을 반복해도 결과가 같습니다. 같은 이름의 테이블이 그 파일에서 읽은 데이터로 교체되기 때문입니다. 예약 실행에서는 fetch 호출과 테이블 생성문을 같은 Python 파일에 함께 두세요.

DuckDB에서 공휴일 조회하기

2026년 미국 공휴일 캘린더를 가져온 뒤, 그 holidays 필드를 holidays 테이블로 읽어 들입니다. 예시는 date와 name을 선택하므로 SQL 결과에 공휴일 날짜와 이름이 함께 나옵니다. 2026년 10월 24일 기준으로 이 응답에는 공휴일이 11건 들어 있었습니다. 공휴일 행이 아니라 건수가 필요하면 전체 Python 예제에 이 테이블의 건수를 세는 쿼리도 있습니다.

공휴일 날짜와 이름 읽기
fetch("holidays", "/v1/calendars/holidays/USA/2026")
con.sql(
    "CREATE OR REPLACE TABLE holidays AS "
    "SELECT unnest(holidays, recursive := true) "
    "FROM read_json('holidays.json')"
)
print(con.sql("SELECT date, name FROM holidays").fetchall())

DuckDB에서 발표일 조회하기

시작일, 종료일, 한도를 지정해 2026년 11월의 미국 경제 캘린더를 가져옵니다. 응답의 releases 필드에는 at, period, topic, status가 들어 있으며, 이 예시는 테이블에서 기간, 주제, 상태 열을 읽습니다. 2026년 10월 24일 기준으로 응답에는 발표가 31건 들어 있었습니다. 이것은 경제 지표 발표일이고, 기간별 CPI 값은 다음 예시의 관측값 요청에서 읽습니다.

발표 기간과 주제 읽기
fetch(
    "releases",
    "/v1/economics/calendar",
    countries="USA",
    start_date="2026-11-01",
    end_date="2026-11-30",
    limit=100,
)
con.sql(
    "CREATE OR REPLACE TABLE releases AS "
    "SELECT unnest(releases, recursive := true) "
    "FROM read_json('releases.json')"
)
print(con.sql("SELECT period, topic, status FROM releases").fetchall())

DuckDB에서 경제 데이터 조회하기

start를 2025-09로 지정해 미국 CPI 관측값을 읽습니다. JSON 필드 이름은 observations이고, 만들어지는 cpi 테이블에는 쿼리가 선택할 period와 value 열이 있습니다. 2026년 10월 24일 기준으로 2026-09의 CPI 값은 336.940이었습니다. 예시는 관측값을 기간의 내림차순으로 정렬해 결과 수를 제한하며, 전체 Python 예제와 같은 쿼리를 씁니다.

CPI 관측값 읽기
fetch("cpi", "/v1/economics/USA/cpi/observations", start="2025-09")
con.sql(
    "CREATE OR REPLACE TABLE cpi AS "
    "SELECT unnest(observations, recursive := true) "
    "FROM read_json('cpi.json')"
)
print(con.sql("SELECT period, value FROM cpi ORDER BY period DESC LIMIT 2").fetchall())

DuckDB에서 원자재 조회하기

USD 기준 Brent 현물 가격을 가져와, 저장된 JSON에서 price와 unit을 바로 읽습니다. 이 테이블 생성문은 목록을 펼치지 않고 해당 필드를 선택합니다. 2026년 10월 24일 기준으로 Brent는 배럴당 116.8809달러였습니다. 예시는 SELECT *로 brent 테이블을 읽기 때문에, Python으로 돌아오는 결과에 가격과 단위가 모두 들어 있습니다.

Brent 가격과 단위 읽기
fetch("brent", "/v1/commodities/spot/BRENT/USD")
con.sql(
    "CREATE OR REPLACE TABLE brent AS SELECT price, unit FROM read_json('brent.json')"
)
print(con.sql("SELECT * FROM brent").fetchall())

DuckDB에서 환율 조회하기

기준 통화로 USD를 쓰고 currencies 매개변수에 EUR과 IDR을 지정합니다. 응답의 rates 필드를 fx 테이블로 읽어 들인 뒤 currency와 rate를 함께 선택합니다. 2026년 10월 24일 기준으로 USD/EUR은 0.89271, USD/IDR은 18107.8이었습니다. 이 환율은 텍스트 문자열로 저장됩니다. 쿼리는 행을 Python으로 다시 읽을 때 각 통화와 환율을 나란히 유지합니다.

환율 읽기
fetch("fx", "/v1/fx/rates/USD", currencies="EUR,IDR")
con.sql(
    "CREATE OR REPLACE TABLE fx AS "
    "SELECT unnest(rates, recursive := true) "
    "FROM read_json('fx.json')"
)
print(con.sql("SELECT currency, rate FROM fx").fetchall())

DuckDB에서 예측 시장 조회하기

응답의 meetings 필드에서 연방준비제도 정책 경로를 읽어 fed 테이블에 넣습니다. 2026년 10월 24일 기준으로 2026년 10월 28일 회의의 동결 확률은 0.9188, 예상 변동폭은 1.7 베이시스포인트였습니다. 쿼리는 resolves_at 순으로 정렬하고 그 회의의 p_hold와 expected_change_bps를 읽습니다. 숫자 열은 Python으로 반환됩니다.

동결 확률과 예상 변동폭 읽기
fetch("fed", "/v1/predictions/policy-paths/federal-reserve")
con.sql(
    "CREATE OR REPLACE TABLE fed AS "
    "SELECT unnest(meetings, recursive := true) "
    "FROM read_json('fed.json')"
)
print(
    con.sql(
        "SELECT p_hold, expected_change_bps FROM fed ORDER BY resolves_at LIMIT 1"
    ).fetchall()
)

목록 필드에 unnest를 쓰는 이유는 무엇인가요?

목록은 하나의 JSON 필드 안에 들어 있습니다. unnest(list_column, recursive := true)는 이를 항목마다 한 행으로 펼치므로, 공휴일, 발표, 관측값, 환율, 회의 목록이 각각의 테이블에서 행이 됩니다. unnest 안에는 응답의 필드 이름을 쓰세요. 예를 들어 CPI 응답은 저장한 파일과 테이블 이름이 cpi이더라도 observations를 씁니다.

Python에서 타임스탬프를 선택하려면 어떻게 해야 하나요?

Python에서 at이나 resolves_at 타임스탬프를 선택하기 전에 pytz를 설치하세요. 필요하면 SQL에서 타임스탬프를 캐스팅해도 됩니다. 예측 시장 예시는 SQL 안에서 resolves_at 순으로 정렬하고 숫자 열을 Python으로 반환하며, 발표일 예시는 period, topic, status를 선택합니다.

에 게시되었습니다.

다음 단계