Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions quantmind/preprocess/fetch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
CrossrefMetadata,
resolve_doi,
)
from quantmind.preprocess.fetch.fxmacrodata import (
DEFAULT_FXMACRODATA_BASE_URL,
CalendarRelease,
RawCalendar,
fetch_fxmacrodata_calendar,
)
from quantmind.preprocess.fetch.http import (
DEFAULT_USER_AGENT,
FetchAttemptsExhausted,
Expand All @@ -30,16 +36,20 @@

__all__ = [
"ArxivIdParseError",
"CalendarRelease",
"CrossrefMetadata",
"DEFAULT_FXMACRODATA_BASE_URL",
"DEFAULT_USER_AGENT",
"FeedItem",
"FetchAttemptsExhausted",
"FetchPolicy",
"Fetched",
"HttpFetcher",
"RawCalendar",
"RawFeed",
"RawPaper",
"fetch_arxiv",
"fetch_fxmacrodata_calendar",
"fetch_rss_feed",
"fetch_url",
"parse_feed",
Expand Down
101 changes: 101 additions & 0 deletions quantmind/preprocess/fetch/fxmacrodata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""FXMacroData fetch helpers for macroeconomic context."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Optional

import httpx

DEFAULT_FXMACRODATA_BASE_URL = "https://api.fxmacrodata.com/v1"


@dataclass(frozen=True, slots=True)
class CalendarRelease:
"""One scheduled macroeconomic or central-bank release."""

release: str
name: str
announcement_datetime_utc: str | None
announcement_datetime_local: str | None
release_date_confirmed: bool
event_importance: str | None
market_tier: int | None
source: str | None
source_url: str | None
raw: dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class RawCalendar:
"""Release-calendar payload for a single currency.

``releases`` holds the parsed rows; ``metadata`` preserves the FXMacroData
envelope (currency, timezone, data quality) so the format layer can build
knowledge items without a second request.
"""

currency: str
timezone: str | None
url: str
releases: tuple[CalendarRelease, ...] = ()
metadata: dict[str, Any] = field(default_factory=dict)


async def fetch_fxmacrodata_calendar(
currency: str = "usd",
*,
limit: int = 50,
api_key: Optional[str] = None,
base_url: str = DEFAULT_FXMACRODATA_BASE_URL,
timeout: float = 30.0,
) -> RawCalendar:
"""Fetch official release-calendar rows from FXMacroData.

Returns a frozen :class:`RawCalendar` in line with the fetch layer
contract. No parsing beyond splitting rows from the response envelope --
interpreting the rows is the format layer's job.
"""

limit_count = max(1, min(int(limit), 100))
currency_code = currency.lower()
params: dict[str, str] = {"limit": str(limit_count)}

url = f"{base_url.rstrip('/')}/calendar/{currency_code}"
headers = {"User-Agent": "QuantMind/0.2 fxmacrodata-fetch"}
if api_key:
# Sent as a header so the key is never captured in request logs or
# proxy access logs the way a query parameter would be.
headers["X-API-Key"] = api_key

async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
response = await client.get(url, params=params, headers=headers)
response.raise_for_status()
payload: dict[str, Any] = response.json()

rows = payload.get("data")
rows = rows[:limit_count] if isinstance(rows, list) else []
releases = tuple(
CalendarRelease(
release=str(row.get("release", "")),
name=str(row.get("name", "")),
announcement_datetime_utc=row.get("announcement_datetime_utc"),
announcement_datetime_local=row.get("announcement_datetime_local"),
release_date_confirmed=bool(row.get("release_date_confirmed", False)),
event_importance=row.get("event_importance"),
market_tier=row.get("market_tier"),
source=row.get("source"),
source_url=row.get("source_url"),
raw=row,
)
for row in rows
if isinstance(row, dict)
)
metadata = {key: value for key, value in payload.items() if key != "data"}
return RawCalendar(
currency=str(payload.get("currency", currency_code)).upper(),
timezone=payload.get("timezone"),
url=url,
releases=releases,
metadata=metadata,
)