spb/hfmarketdata
Public
Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)
JavaScript 53.7%
Python 38.3%
CSS 4.6%
TypeScript 3.1%
1"""Tiny HF Market Data client shared by the hfmd-* skills (requests + pandas only).23Copied verbatim into every skill's `scripts/` folder by `skills/scripts/build_skills.py`4so each skill stays self-contained. Do not edit the copies — edit `skills/_shared/hfmd.py`.56Environment: HFMD_API_KEY (optional; free API key = 120 req/min, keyless mode has low hourly limits = 30 req/h;7 higher limits on request by e-mail to contact@spboucher.ai, also free),8 HFMD_BASE_URL (default https://www.hfmarketdata.io).9"""10from __future__ import annotations1112import os13import sys14import time15from typing import Any1617import pandas as pd18import requests1920BASE_URL = os.environ.get("HFMD_BASE_URL", "https://www.hfmarketdata.io").rstrip("/")21API_KEY = os.environ.get("HFMD_API_KEY", "").strip()22UA = "hfmd-skills/1.0 (+https://www.hfmarketdata.io/integrations/skills)"2324TIMEFRAMES = {"1m": "1min", "5m": "5min", "30m": "30min", "1h": "1hour", "1d": "1day",25 "1min": "1min", "5min": "5min", "30min": "30min", "1hour": "1hour", "1day": "1day"}26V2_INTERVAL = {"1min": "1m", "5min": "5m", "30min": "30m", "1hour": "1h", "1day": "1d"}272829class HfmdError(RuntimeError):30 def __init__(self, status: int, code: str, message: str, url: str):31 super().__init__(f"HF Market Data error {status} {code}: {message} ({url})")32 self.status, self.code, self.url = status, code, url333435_session = requests.Session()36_session.headers.update({"Accept": "application/json", "User-Agent": UA})37if API_KEY:38 _session.headers["Authorization"] = f"Bearer {API_KEY}"394041def log(msg: str) -> None:42 print(msg, file=sys.stderr)434445def get(path: str, params: dict[str, Any] | None = None, *, retries: int = 3) -> tuple[Any, dict[str, str]]:46 """GET a JSON endpoint. Returns (body, headers). Retries on 429 honouring Retry-After."""47 url = f"{BASE_URL}{path}"48 p = {k: (",".join(v) if isinstance(v, (list, tuple)) else v) for k, v in (params or {}).items() if v not in (None, "")}49 p.setdefault("format", "json")50 for attempt in range(retries + 1):51 r = _session.get(url, params=p, timeout=120)52 if r.status_code == 429 and attempt < retries:53 wait = float(r.headers.get("Retry-After", "5"))54 log(f"429 rate limited — sleeping {wait:.0f}s (set HFMD_API_KEY for 120 req/min)")55 time.sleep(min(wait, 120))56 continue57 if r.status_code >= 400:58 try:59 body = r.json()60 except ValueError:61 body = {}62 err = body.get("error") or {}63 raise HfmdError(r.status_code, err.get("code", "HTTP_ERROR"), err.get("message") or body.get("detail") or r.text[:200], r.url)64 rem = r.headers.get("X-RateLimit-Remaining-Requests")65 if rem is not None:66 log(f"rate limit: {rem}/{r.headers.get('X-RateLimit-Limit-Requests')} requests left")67 return r.json(), dict(r.headers)68 raise HfmdError(429, "RATE_LIMIT_EXCEEDED", "gave up after retries", url)697071def rows_of(body: Any) -> list[dict]:72 """Rows from either envelope: v1 {count,data} or v2 {data,meta}."""73 if isinstance(body, list):74 return body75 if isinstance(body, dict):76 data = body.get("data")77 if isinstance(data, list):78 return data79 return []808182def meta_of(body: Any) -> dict:83 if isinstance(body, dict):84 m = dict(body.get("meta") or {})85 if "count" in body and "count" not in m:86 m["count"] = body["count"]87 return m88 return {}899091def to_df(body: Any, time_col: str | None = "datetime") -> pd.DataFrame:92 df = pd.DataFrame(rows_of(body))93 if time_col and time_col in df.columns:94 df[time_col] = pd.to_datetime(df[time_col])95 df = df.sort_values(time_col).reset_index(drop=True)96 return df979899def bars(asset: str, ticker: str, timeframe: str = "1day", start: str | None = None, end: str | None = None,100 adjustment: str | None = None, limit: int = 50_000) -> pd.DataFrame:101 """v1 bars (stock/etf/crypto/index/fx, or futures = vendor continuous). Paginates by date when needed."""102 tf = TIMEFRAMES.get(timeframe, timeframe)103 frames: list[pd.DataFrame] = []104 cursor_start = start105 while True:106 body, _ = get(f"/v1/bars/{asset}/{ticker}", {"timeframe": tf, "start": cursor_start, "end": end, "adjustment": adjustment, "limit": limit, "order": "asc"})107 df = to_df(body)108 if df.empty:109 break110 frames.append(df)111 if len(df) < limit:112 break113 last = df["datetime"].iloc[-1]114 cursor_start = (last + pd.Timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M") if tf != "1day" else (last + pd.Timedelta(days=1)).strftime("%Y-%m-%d")115 out = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame(columns=["ticker", "datetime", "open", "high", "low", "close", "volume"])116 return out.drop_duplicates("datetime").reset_index(drop=True)117118119def contract_bars(symbol: str, timeframe: str = "1day", start: str | None = None, end: str | None = None, limit: int = 50_000) -> pd.DataFrame:120 """v2 individual futures contract bars (ESZ25…)."""121 tf = TIMEFRAMES.get(timeframe, timeframe)122 body, _ = get(f"/v1/futures/contract/{symbol}/bars", {"interval": V2_INTERVAL.get(tf, "1d"), "from": start, "to": end, "limit": limit})123 return to_df(body)124125126def continuous(root: str, roll: str = "volume", adjust: str = "back_adjusted", depth: int = 1, timeframe: str = "1day",127 start: str | None = None, end: str | None = None, limit: int = 50_000) -> tuple[pd.DataFrame, dict]:128 """v2 server-built continuous series. Returns (df, meta) — meta['roll_dates'] lists the rolls."""129 tf = TIMEFRAMES.get(timeframe, timeframe)130 body, _ = get(f"/v1/futures/{root}/continuous", {"roll": roll, "adjust": adjust, "depth": depth, "interval": V2_INTERVAL.get(tf, "1d"), "from": start, "to": end, "limit": limit})131 return to_df(body), meta_of(body)132133134def contracts(root: str, status: str | None = None) -> pd.DataFrame:135 body, _ = get(f"/v1/futures/{root}/contracts", {"status": status, "limit": 1000})136 return to_df(body, time_col=None)137138139def term_structure(root: str, as_of: str | None = None) -> tuple[pd.DataFrame, dict]:140 body, _ = get(f"/v1/futures/{root}/term-structure", {"as_of": as_of})141 return to_df(body, time_col=None), meta_of(body)142143144def screener(filters: str, sort: str | None = None, as_of: str | None = None, limit: int = 100) -> tuple[pd.DataFrame, dict]:145 body, _ = get("/v1/fundamentals/screener", {"filters": filters, "sort": sort, "as_of": as_of, "limit": limit})146 return to_df(body, time_col=None), meta_of(body)147148149def ratios(ticker: str) -> dict:150 body, _ = get(f"/v1/fundamentals/{ticker}/ratios")151 d = body.get("data", body) if isinstance(body, dict) else body152 return d if isinstance(d, dict) else (d[0] if d else {})153154155def statements(ticker: str, statement: str = "income", period: str = "quarterly", limit: int = 12) -> pd.DataFrame:156 body, _ = get(f"/v1/fundamentals/{ticker}/statements", {"statement": statement, "period": period, "limit": limit})157 return to_df(body, time_col=None)158