SPB Git

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)

Python 46.7% JavaScript 37.4% CSS 14.9% HTML 0.9%

HF Market Data — FirstRate full-history downloader, DuckDB/Parquet lake, open API and docs platform

- frd_downloader.py: 745-job resumable plan (options 2010→today, stock/etf A-Z,
  futures continuous+contracts, crypto/index/fx, all timeframes & adjustments),
  CSV→Parquet(zstd) via DuckDB, raw archives deleted on the fly
- hfmarketdata/api: open FastAPI+DuckDB API (bars, multi-ticker, point-in-time
  snapshot, options chains/history, live status), JSON+CSV
- hfmarketdata/web: React+Vite documentation platform (editorial light theme)

Author: Simon-Pierre Boucher <contact@spboucher.ai>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon-pierre boucher committed 21 h ago (Aug 10, 2026)

Showing 14 changed files with +4,352 and −0

added .gitignore +24 −0
@@ -0,0 +1,24 @@
1 +# --- heavy data: never in the repo ---
2 +*.zip
3 +*.parquet
4 +*.csv
5 +*.duckdb
6 +parquet/
7 +zips/
8 +data/
9 +state/
10 +logs/
11 +
12 +# credentials / vendor docs containing the customer id
13 +api.txt
14 +
15 +# environments & builds
16 +venv/
17 +__pycache__/
18 +*.pyc
19 +node_modules/
20 +hfmarketdata/web/dist/
21 +hfmarketdata/venv/
22 +
23 +# OS / editor
24 +.DS_Store
added README.md +114 −0
@@ -0,0 +1,114 @@
1 +# HF Market Data
2 +
3 +**Open high-frequency market data platform** — a full-history downloader, a DuckDB-over-Parquet data lake, an open REST API and a documentation platform, live at **[www.hfmarketdata.io](https://www.hfmarketdata.io)**.
4 +
5 +[![Live](https://img.shields.io/badge/live-www.hfmarketdata.io-2e5ce6)](https://www.hfmarketdata.io)
6 +[![API](https://img.shields.io/badge/API-open%20%C2%B7%20no%20key-2e5ce6)](https://www.hfmarketdata.io/docs)
7 +[![Python](https://img.shields.io/badge/Python-3.12%2B-3776AB?logo=python&logoColor=white)](#)
8 +[![FastAPI](https://img.shields.io/badge/FastAPI-0.115%2B-009688?logo=fastapi&logoColor=white)](#)
9 +[![DuckDB](https://img.shields.io/badge/DuckDB-1.x-FFF000?logo=duckdb&logoColor=black)](#)
10 +[![React](https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=black)](#)
11 +[![Vite](https://img.shields.io/badge/Vite-6-646CFF?logo=vite&logoColor=white)](#)
12 +[![Parquet](https://img.shields.io/badge/storage-Parquet%20%C2%B7%20zstd-50ABF1)](#)
13 +
14 +[![Author](https://img.shields.io/badge/author-Simon--Pierre%20Boucher-1c2430)](mailto:contact@spboucher.ai)
15 +[![Contact](https://img.shields.io/badge/contact-contact%40spboucher.ai-1c2430)](mailto:contact@spboucher.ai)
16 +
17 +---
18 +
19 +## Dataset metrics
20 +
21 +Live snapshot of the lake while ingestion is still running (2026-08-10):
22 +
23 +| Metric | Value |
24 +|---|---:|
25 +| Rows converted to Parquet | **1,785,837,666+** |
26 +| Parquet files (one per instrument/segment) | **211,140+** |
27 +| Lake size (zstd Parquet) | **34 GB+** (raw archives deleted on the fly) |
28 +| Options quarters ingested | **30+ / 66** (2010_q1 → today) |
29 +| Options underlyings | **5,800+** US equities & indices |
30 +| Bar timeframes | 1min · 5min · 30min · 1hour · 1day |
31 +| Asset classes | stocks · ETFs · futures (continuous + contracts) · crypto · indices · FX · options |
32 +| Download plan | 745 jobs, fully resumable, 0 conversion errors |
33 +
34 +Query the live inventory anytime: [`GET /v1/status`](https://www.hfmarketdata.io/v1/status).
35 +
36 +## Architecture
37 +
38 +```
39 +FirstRate Data API ──▶ frd_downloader.py ──▶ Parquet lake (zstd, one file per instrument)
40 + (zip archives) · 745-job plan /parquet/{type}/{timeframe}/{adjustment}/{TICKER}.parquet
41 + · resume manifest /parquet/options/{year}_{quarter}/…
42 + · CSV→Parquet via
43 + DuckDB, raw deleted
44 + │ queried in place (predicate pushdown)
45 +
46 + hfmarketdata/api/main.py (FastAPI + DuckDB)
47 + │ :8090
48 + hfmarketdata/web/ (React + Vite docs platform)
49 +
50 + https://www.hfmarketdata.io (ngrok)
51 +```
52 +
53 +## Repository layout
54 +
55 +```
56 +frd_downloader.py Full-history downloader & Parquet converter (single file)
57 +requirements.txt Downloader dependencies (requests, duckdb)
58 +hfmarketdata/
59 + api/main.py Open REST API — FastAPI + DuckDB over the Parquet lake
60 + requirements.txt API dependencies
61 + web/ Documentation platform (React 18 + Vite, English)
62 +```
63 +
64 +Heavy data (Parquet lake, zip archives, venvs, builds) is **not** in this repository — see `.gitignore`.
65 +
66 +## The downloader
67 +
68 +Downloads **everything** FirstRate Data publishes and stores it query-ready:
69 +
70 +- **Options** — every quarterly archive since 2010 + the current partial quarter (16 fields per row: quotes, bid/ask IV, open interest, volume, delta, gamma, vega, theta, rho)
71 +- **Stocks & ETFs** — full history, tickers A–Z, all 5 timeframes × `adj_split` / `adj_splitdiv` / `UNADJUSTED`
72 +- **Futures** — continuous series (`contin_UNadj` / `contin_adj_ratio` / `contin_adj_absolute`) + individual contracts
73 +- **Crypto, indices, FX** — full history, all timeframes
74 +- **Metadata** — ticker listings, splits, dividends, update logs, continuous-series audit
75 +
76 +```bash
77 +export FRD_USERID=<your FirstRate customer id>
78 +python3 -m venv venv && venv/bin/pip install -r requirements.txt
79 +venv/bin/python frd_downloader.py --data-root /Volumes/ssd/firstratedata
80 +```
81 +
82 +Fully resumable (`state/manifest.json`), disk-space guard, retries with backoff, and raw zips/CSVs are deleted as soon as each segment is converted. Notable quirks handled: FirstRate files mix LF/CRLF line endings (`strict_mode=false`), daily files use `yyyyMMdd` dates, the API 302-redirects to a CDN.
83 +
84 +## The API
85 +
86 +Open, keyless, JSON + CSV. Highlights:
87 +
88 +| Endpoint | What it does |
89 +|---|---|
90 +| `GET /v1/status` | Live inventory of the lake |
91 +| `GET /v1/{asset}/tickers` | Instrument discovery per type/timeframe/adjustment |
92 +| `GET /v1/bars/{asset}/{ticker}` | OHLCV bars, precise `start`/`end` windows, up to 2M rows in CSV |
93 +| `GET /v1/bars/{asset}?tickers=A,B,C` | Multi-ticker bars in one call (per-ticker limit) |
94 +| `GET /v1/snapshot/{asset}?tickers=…&at=…` | Whole watchlist at one precise moment |
95 +| `GET /v1/options/chain/{ticker}` | Full EOD chain — quotes, IV, Greeks |
96 +| `GET /v1/options/history/{ticker}` | One contract's daily life story |
97 +| `GET /docs` | OpenAPI / Swagger |
98 +
99 +```bash
100 +# 4 hours of 1-minute bars across a watchlist
101 +curl "https://www.hfmarketdata.io/v1/bars/stock?tickers=AAPL,MSFT,NVDA&timeframe=1min&start=2024-06-03%2009:30:00&end=2024-06-03%2013:30:00"
102 +```
103 +
104 +## The platform
105 +
106 +React 18 + Vite documentation site (English) served by the API process — refined editorial light theme (Fraunces / Inter / JetBrains Mono), full endpoint reference with parameter tables, request snippets (curl / Python / JavaScript), example + **live** responses, response-field dictionaries and a live dataset status page.
107 +
108 +```bash
109 +cd hfmarketdata/web && npm install && npm run build # → dist/, served at /
110 +```
111 +
112 +## Author
113 +
114 +**Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai)
added frd_downloader.py +627 −0
@@ -0,0 +1,627 @@
1 +#!/usr/bin/env python3
2 +# -*- coding: utf-8 -*-
3 +"""
4 +================================================================================
5 + FirstRate Data — Full Historical Downloader & Parquet Converter
6 +================================================================================
7 +
8 +Downloads the COMPLETE historical datasets from the FirstRate Data API and
9 +stores them as query-optimized Parquet files (zstd compressed), ready to be
10 +served by an ultra-fast query layer (DuckDB / pyarrow).
11 +
12 +Coverage
13 +--------
14 + * options : full archives per year/quarter (2010 -> today)
15 + + 'month' update for the current partial quarter
16 + * stock, etf : period=full, ticker_range A-Z,
17 + timeframes 1min/5min/30min/1hour/1day,
18 + adjustments adj_split / adj_splitdiv / UNADJUSTED
19 + (UNADJUSTED only exists for 1min & 1day)
20 + * futures (continuous) : timeframes x contin_UNadj / contin_adj_ratio /
21 + contin_adj_absolute
22 + * futures (individual) : contract_files=archive + update, all timeframes
23 + * crypto, index, fx : period=full, all timeframes
24 + * metadata : ticker listings, splits, dividends, update logs,
25 + futures continuous-series audit file
26 + * delisted stocks : optional (--include-delisted), archives 1-4 +
27 + 2026 update, all timeframe/adjustment combos
28 +
29 +Storage layout (under --data-root)
30 +----------------------------------
31 + parquet/{type}/{timeframe}/{adjustment}/{TICKER}.parquet (bar data)
32 + parquet/options/{year}_{quarter}/{file}.parquet (options)
33 + meta/… (csv metadata)
34 + zips/… (only w/ --keep-zips)
35 + state/manifest.json (resume state)
36 + logs/frd_downloader.log
37 +
38 +Every Parquet file carries a `ticker` column, so the whole tree can be
39 +queried directly, e.g.:
40 +
41 + SELECT * FROM read_parquet('parquet/stock/1day/adj_splitdiv/*.parquet')
42 + WHERE ticker = 'AAPL' AND datetime >= '2020-01-01';
43 +
44 +Usage
45 +-----
46 + python3 frd_downloader.py --data-root /Volumes/ssd/firstratedata
47 + python3 frd_downloader.py --list-jobs # preview the job plan
48 + python3 frd_downloader.py --types options stock # restrict asset types
49 + python3 frd_downloader.py --include-delisted # add delisted stocks
50 +
51 +The run is fully resumable: completed jobs are recorded in the manifest and
52 +skipped on re-run.
53 +
54 +Author : Simon-Pierre Boucher
55 +Contact : contact@spboucher.ai
56 +Created : 2026-08-09
57 +================================================================================
58 +"""
59 +
60 +from __future__ import annotations
61 +
62 +import argparse
63 +import json
64 +import logging
65 +import os
66 +import shutil
67 +import sys
68 +import tempfile
69 +import threading
70 +import time
71 +import zipfile
72 +from concurrent.futures import ThreadPoolExecutor, as_completed
73 +from dataclasses import dataclass, field
74 +from datetime import date
75 +from pathlib import Path
76 +from typing import Iterator
77 +from urllib.parse import urlencode
78 +
79 +import duckdb
80 +import requests
81 +
82 +__author__ = "Simon-Pierre Boucher"
83 +__contact__ = "contact@spboucher.ai"
84 +__version__ = "1.0.0"
85 +
86 +# ------------------------------------------------------------------------------
87 +# Configuration
88 +# ------------------------------------------------------------------------------
89 +
90 +BASE_URL = "https://firstratedata.com/api"
91 +# FirstRate Data customer id — never hardcoded: export FRD_USERID=<your id>
92 +USERID = os.environ.get("FRD_USERID", "")
93 +
94 +TIMEFRAMES = ["1day", "1hour", "30min", "5min", "1min"] # small -> large
95 +STOCK_ETF_ADJUSTMENTS = ["adj_split", "adj_splitdiv", "UNADJUSTED"]
96 +UNADJUSTED_TIMEFRAMES = {"1min", "1day"} # per API doc
97 +FUTURES_ADJUSTMENTS = ["contin_UNadj", "contin_adj_ratio", "contin_adj_absolute"]
98 +TICKER_RANGES = [chr(c) for c in range(ord("A"), ord("Z") + 1)]
99 +OPTIONS_FIRST_YEAR = 2010
100 +
101 +DOWNLOAD_TIMEOUT = 7200 # seconds per request (archives can be huge)
102 +CHUNK_SIZE = 4 * 1024 * 1024 # 4 MiB streaming chunks
103 +MAX_RETRIES = 5
104 +RETRY_BACKOFF = 60 # seconds, multiplied by attempt number
105 +MIN_FREE_GB = 30 # abort downloads below this free-space floor
106 +
107 +log = logging.getLogger("frd")
108 +
109 +# ------------------------------------------------------------------------------
110 +# Job model
111 +# ------------------------------------------------------------------------------
112 +
113 +
114 +@dataclass
115 +class Job:
116 + """One downloadable unit: an API request plus its destination."""
117 +
118 + key: str # unique id, used in the manifest
119 + endpoint: str # API endpoint path (e.g. 'data_file')
120 + params: dict # query parameters (userid added at request time)
121 + dest_subdir: str # where converted output lands, under data root
122 + kind: str = "bars" # 'bars' (zip of csv -> parquet) or 'meta' (raw text)
123 + priority: int = 50 # lower runs first
124 +
125 + @property
126 + def url(self) -> str:
127 + q = dict(self.params)
128 + q["userid"] = USERID
129 + return f"{BASE_URL}/{self.endpoint}?{urlencode(q)}"
130 +
131 + @property
132 + def display_url(self) -> str:
133 + return f"{BASE_URL}/{self.endpoint}?{urlencode(self.params)}&userid=***"
134 +
135 +
136 +def _current_quarter(today: date) -> tuple[int, int]:
137 + return today.year, (today.month - 1) // 3 + 1
138 +
139 +
140 +def build_jobs(types: list[str], include_delisted: bool) -> list[Job]:
141 + """Build the full download plan, ordered so that the small/most-useful
142 + datasets (daily bars, metadata) land first and the huge ones last."""
143 + jobs: list[Job] = []
144 + today = date.today()
145 +
146 + # -- metadata (tiny, always useful, run first) ------------------------------
147 + for t in ["stock", "etf", "futures", "crypto", "index", "fx"]:
148 + if t in types:
149 + jobs.append(Job(
150 + key=f"ticker_listing|{t}", endpoint="ticker_listing",
151 + params={"type": t}, dest_subdir=f"meta/{t}",
152 + kind="meta", priority=0))
153 + for t in ["stock", "etf"]:
154 + if t in types:
155 + for mft in ["splits", "dividends"]:
156 + jobs.append(Job(
157 + key=f"meta|{t}|{mft}", endpoint="meta_file",
158 + params={"type": t, "metafile_type": mft},
159 + dest_subdir=f"meta/{t}", kind="meta", priority=1))
160 + if "stock" in types:
161 + for lt in ["delisted", "added", "changed"]:
162 + jobs.append(Job(
163 + key=f"update_log|stock|{lt}", endpoint="update_log",
164 + params={"type": "stock", "log_type": lt},
165 + dest_subdir="meta/stock", kind="meta", priority=1))
166 + if "futures" in types:
167 + jobs.append(Job(
168 + key="meta|futures|contin_audit", endpoint="meta_file",
169 + params={"type": "futures", "metafile_type": "contin_audit"},
170 + dest_subdir="meta/futures", kind="meta", priority=1))
171 +
172 + # -- stock / etf : full archives, per letter --------------------------------
173 + for t in ["stock", "etf"]:
174 + if t not in types:
175 + continue
176 + for tf_i, tf in enumerate(TIMEFRAMES):
177 + for adj in STOCK_ETF_ADJUSTMENTS:
178 + if adj == "UNADJUSTED" and tf not in UNADJUSTED_TIMEFRAMES:
179 + continue
180 + for letter in TICKER_RANGES:
181 + jobs.append(Job(
182 + key=f"{t}|full|{tf}|{adj}|{letter}",
183 + endpoint="data_file",
184 + params={"type": t, "period": "full",
185 + "ticker_range": letter,
186 + "timeframe": tf, "adjustment": adj},
187 + dest_subdir=f"parquet/{t}/{tf}/{adj}",
188 + priority=10 + tf_i * 4))
189 +
190 + # -- futures : continuous series --------------------------------------------
191 + if "futures" in types:
192 + for tf_i, tf in enumerate(TIMEFRAMES):
193 + for adj in FUTURES_ADJUSTMENTS:
194 + jobs.append(Job(
195 + key=f"futures|full|{tf}|{adj}",
196 + endpoint="data_file",
197 + params={"type": "futures", "period": "full",
198 + "timeframe": tf, "adjustment": adj},
199 + dest_subdir=f"parquet/futures/{tf}/{adj}",
200 + priority=10 + tf_i * 4))
201 + # individual contracts (pre-2026 archive + current-year update)
202 + for tf_i, tf in enumerate(TIMEFRAMES):
203 + for cf in ["archive", "update"]:
204 + jobs.append(Job(
205 + key=f"futures_contract|{cf}|{tf}",
206 + endpoint="futures_contract",
207 + params={"contract_files": cf, "timeframe": tf},
208 + dest_subdir=f"parquet/futures_contracts/{tf}/{cf}",
209 + priority=12 + tf_i * 4))
210 +
211 + # -- crypto / index / fx : full archives -------------------------------------
212 + for t in ["crypto", "index", "fx"]:
213 + if t not in types:
214 + continue
215 + for tf_i, tf in enumerate(TIMEFRAMES):
216 + jobs.append(Job(
217 + key=f"{t}|full|{tf}",
218 + endpoint="data_file",
219 + params={"type": t, "period": "full", "timeframe": tf},
220 + dest_subdir=f"parquet/{t}/{tf}/none",
221 + priority=10 + tf_i * 4))
222 +
223 + # -- options : every quarter since 2010 + current partial quarter ------------
224 + # Priority 5-6: options are the primary dataset, they download right after
225 + # the metadata and before all the bar archives.
226 + if "options" in types:
227 + cur_year, cur_q = _current_quarter(today)
228 + for year in range(OPTIONS_FIRST_YEAR, cur_year + 1):
229 + for q in range(1, 5):
230 + if year == cur_year and q >= cur_q:
231 + continue # current/future quarters have no full archive yet
232 + jobs.append(Job(
233 + key=f"options|{year}|q{q}",
234 + endpoint="data_file",
235 + params={"type": "options", "year": str(year),
236 + "quarter": f"q{q}"},
237 + dest_subdir=f"parquet/options/{year}_q{q}",
238 + priority=5))
239 + jobs.append(Job(
240 + key="options|update|month",
241 + endpoint="data_file",
242 + params={"type": "options", "period": "month"},
243 + dest_subdir=f"parquet/options/{cur_year}_q{cur_q}_partial",
244 + priority=6))
245 +
246 + # -- delisted stocks (optional, survivorship-bias-free research) -------------
247 + if include_delisted and "stock" in types:
248 + for tf_i, tf in enumerate(TIMEFRAMES):
249 + for adj in STOCK_ETF_ADJUSTMENTS:
250 + if adj == "UNADJUSTED" and tf != "1min":
251 + continue # per API doc: UNADJUSTED delisted = 1min only
252 + for arc in ["1", "2", "3", "4"]:
253 + jobs.append(Job(
254 + key=f"delisted|archive{arc}|{tf}|{adj}",
255 + endpoint="delisted_data_file",
256 + params={"archive_number": arc,
257 + "timeframe": tf, "adjustment": adj},
258 + dest_subdir=f"parquet/stock_delisted/{tf}/{adj}",
259 + priority=30 + tf_i * 4))
260 + jobs.append(Job(
261 + key=f"delisted|update_year|{tf}|{adj}",
262 + endpoint="delisted_data_file",
263 + params={"update": "year", "timeframe": tf,
264 + "adjustment": adj},
265 + dest_subdir=f"parquet/stock_delisted/{tf}/{adj}",
266 + priority=30 + tf_i * 4))
267 +
268 + jobs.sort(key=lambda j: (j.priority, j.key))
269 + return jobs
270 +
271 +
272 +# ------------------------------------------------------------------------------
273 +# Manifest (resume state)
274 +# ------------------------------------------------------------------------------
275 +
276 +
277 +class Manifest:
278 + """Thread-safe JSON manifest tracking completed/failed jobs."""
279 +
280 + def __init__(self, path: Path):
281 + self.path = path
282 + self._lock = threading.Lock()
283 + self._data: dict = {"jobs": {}, "version": __version__}
284 + if path.exists():
285 + try:
286 + self._data = json.loads(path.read_text())
287 + except (json.JSONDecodeError, OSError):
288 + log.warning("Manifest unreadable, starting fresh: %s", path)
289 +
290 + def is_done(self, key: str) -> bool:
291 + return self._data["jobs"].get(key, {}).get("status") == "done"
292 +
293 + def mark(self, key: str, status: str, **info) -> None:
294 + with self._lock:
295 + entry = self._data["jobs"].setdefault(key, {})
296 + entry.update(status=status, updated=time.strftime("%Y-%m-%d %H:%M:%S"), **info)
297 + tmp = self.path.with_suffix(".tmp")
298 + tmp.write_text(json.dumps(self._data, indent=1))
299 + tmp.replace(self.path)
300 +
301 + def summary(self) -> dict:
302 + counts: dict[str, int] = {}
303 + for e in self._data["jobs"].values():
304 + counts[e.get("status", "?")] = counts.get(e.get("status", "?"), 0) + 1
305 + return counts
306 +
307 +
308 +# ------------------------------------------------------------------------------
309 +# CSV -> Parquet conversion
310 +# ------------------------------------------------------------------------------
311 +
312 +# Column layouts by column count (FirstRate bar files have no header row).
313 +# The first field is read as VARCHAR because FirstRate mixes date formats
314 +# ('2005-01-03 17:00:00' intraday vs '20100104' daily) — it is normalized to a
315 +# proper TIMESTAMP at conversion time (see _DT_PARSE).
316 +BAR_SCHEMAS = {
317 + 5: {"dt_raw": "VARCHAR", "open": "DOUBLE", "high": "DOUBLE",
318 + "low": "DOUBLE", "close": "DOUBLE"},
319 + 6: {"dt_raw": "VARCHAR", "open": "DOUBLE", "high": "DOUBLE",
320 + "low": "DOUBLE", "close": "DOUBLE", "volume": "DOUBLE"},
321 + 7: {"dt_raw": "VARCHAR", "open": "DOUBLE", "high": "DOUBLE",
322 + "low": "DOUBLE", "close": "DOUBLE", "volume": "DOUBLE",
323 + "open_interest": "DOUBLE"},
324 +}
325 +
326 +_DT_PARSE = ("COALESCE("
327 + "try_strptime(dt_raw, '%Y-%m-%d %H:%M:%S'), "
328 + "try_strptime(dt_raw, '%Y%m%d'), "
329 + "try_strptime(dt_raw, '%Y-%m-%d'), "
330 + "try_strptime(dt_raw, '%m/%d/%Y %H:%M:%S'), "
331 + "try_strptime(dt_raw, '%m/%d/%Y'))")
332 +
333 +# Options chain layout, per https://firstratedata.com/_readme/options.txt :
334 +# Trade Date, Strike, Expiry Date, Call/Put, Last Trade Price, Bid Price,
335 +# Ask Price, Bid IV, Ask IV, Open Interest, Volume, Delta, Gamma, Vega,
336 +# Theta, Rho
337 +OPTIONS_SCHEMA = {
338 + "trade_date": "DATE", "strike": "DOUBLE", "expiry": "DATE",
339 + "call_put": "VARCHAR", "last_price": "DOUBLE", "bid": "DOUBLE",
340 + "ask": "DOUBLE", "bid_iv": "DOUBLE", "ask_iv": "DOUBLE",
341 + "open_interest": "DOUBLE", "volume": "DOUBLE", "delta": "DOUBLE",
342 + "gamma": "DOUBLE", "vega": "DOUBLE", "theta": "DOUBLE", "rho": "DOUBLE",
343 +}
344 +
345 +
346 +def _sniff_column_count(csv_path: Path) -> int:
347 + with open(csv_path, "r", errors="replace") as f:
348 + for line in f:
349 + line = line.strip()
350 + if line:
351 + return line.count(",") + 1
352 + return 0
353 +
354 +
355 +def _ticker_from_filename(path: Path) -> str:
356 + """'AAPL_1min.txt' -> 'AAPL' ; 'ES_contin_adj.txt' -> 'ES'."""
357 + return path.stem.split("_")[0].upper()
358 +
359 +
360 +def csv_to_parquet(con: duckdb.DuckDBPyConnection, csv_path: Path,
361 + out_path: Path, asset_type: str) -> int:
362 + """Convert one extracted csv/txt file to zstd Parquet. Returns row count."""
363 + ncols = _sniff_column_count(csv_path)
364 + if ncols == 0:
365 + return 0
366 + out_path.parent.mkdir(parents=True, exist_ok=True)
367 + ticker = _ticker_from_filename(csv_path)
368 + src = str(csv_path).replace("'", "''")
369 + dst = str(out_path).replace("'", "''")
370 +
371 + # strict_mode=false: FirstRate files mix LF and CRLF line endings (daily
372 + # rows are appended with CRLF onto LF history), which the strict CSV
373 + # sniffer rejects outright.
374 + common = "strict_mode=false, ignore_errors=true"
375 + if asset_type == "options" and ncols == len(OPTIONS_SCHEMA):
376 + cols = json.dumps(OPTIONS_SCHEMA).replace('"', "'")
377 + select = (f"SELECT '{ticker}' AS ticker, * FROM read_csv('{src}', "
378 + f"header=false, columns={cols}, dateformat='%Y-%m-%d', "
379 + f"{common})")
380 + elif asset_type != "options" and (schema := BAR_SCHEMAS.get(ncols)):
381 + cols = json.dumps(schema).replace('"', "'")
382 + value_cols = ", ".join(c for c in schema if c != "dt_raw")
383 + select = (f"SELECT '{ticker}' AS ticker, {_DT_PARSE} AS datetime, "
384 + f"{value_cols} FROM read_csv('{src}', "
385 + f"header=false, columns={cols}, {common})")
386 + else:
387 + # Unknown layout (options files etc.): let DuckDB auto-detect types,
388 + # header presence and column names — still fully queryable.
389 + select = (f"SELECT '{ticker}' AS ticker, * FROM read_csv_auto('{src}', "
390 + f"{common})")
391 +
392 + con.execute(f"COPY ({select}) TO '{dst}' "
393 + f"(FORMAT PARQUET, COMPRESSION ZSTD, ROW_GROUP_SIZE 1000000)")
394 + rows = con.execute(f"SELECT count(*) FROM read_parquet('{dst}')").fetchone()[0]
395 + if rows == 0 and csv_path.stat().st_size > 0:
396 + log.warning("0 rows converted from non-empty file %s (ncols=%d)",
397 + csv_path.name, ncols)
398 + return rows
399 +
400 +
401 +def extract_and_convert(zip_path: Path, dest_dir: Path, asset_type: str) -> tuple[int, int]:
402 + """Extract a downloaded zip (recursively if it nests zips) and convert every
403 + csv/txt member to Parquet under dest_dir. Returns (files, rows)."""
404 + files = rows = 0
405 + con = duckdb.connect()
406 + con.execute("SET threads TO 4")
407 + try:
408 + with tempfile.TemporaryDirectory(dir=zip_path.parent) as tmp:
409 + tmp_dir = Path(tmp)
410 + _extract_recursive(zip_path, tmp_dir)
411 + members = sorted(p for p in tmp_dir.rglob("*")
412 + if p.is_file() and p.suffix.lower() in (".txt", ".csv"))
413 + for member in members:
414 + out = dest_dir / (member.stem + ".parquet")
415 + try:
416 + n = csv_to_parquet(con, member, out, asset_type)
417 + if n > 0:
418 + files += 1
419 + rows += n
420 + else:
421 + out.unlink(missing_ok=True)
422 + except duckdb.Error as e:
423 + log.error("Convert failed %s: %s", member.name, e)
424 + member.unlink() # free space as we go
425 + finally:
426 + con.close()
427 + return files, rows
428 +
429 +
430 +def _extract_recursive(zip_path: Path, dest: Path, depth: int = 0) -> None:
431 + if depth > 3:
432 + return
433 + with zipfile.ZipFile(zip_path) as zf:
434 + zf.extractall(dest)
435 + for nested in list(dest.rglob("*.zip")):
436 + sub = nested.with_suffix("")
437 + sub.mkdir(exist_ok=True)
438 + _extract_recursive(nested, sub, depth + 1)
439 + nested.unlink()
440 +
441 +
442 +# ------------------------------------------------------------------------------
443 +# Downloader
444 +# ------------------------------------------------------------------------------
445 +
446 +
447 +@dataclass
448 +class Runner:
449 + data_root: Path
450 + manifest: Manifest
451 + keep_zips: bool = False
452 + min_free_gb: int = MIN_FREE_GB
453 + session: requests.Session = field(default_factory=requests.Session)
454 +
455 + def _free_gb(self) -> float:
456 + return shutil.disk_usage(self.data_root).free / 1e9
457 +
458 + def _download(self, job: Job, dest: Path) -> Path | None:
459 + """Stream the response to dest (via .part). Returns path or None."""
460 + part = dest.with_suffix(dest.suffix + ".part")
461 + for attempt in range(1, MAX_RETRIES + 1):
462 + if self._free_gb() < self.min_free_gb:
463 + raise RuntimeError(
464 + f"Only {self._free_gb():.1f} GB free (< {self.min_free_gb} GB floor)")
465 + try:
466 + with self.session.get(job.url, stream=True,
467 + timeout=DOWNLOAD_TIMEOUT) as r:
468 + if r.status_code in (429, 500, 502, 503, 504):
469 + raise requests.HTTPError(f"HTTP {r.status_code}")
470 + if r.status_code == 404:
471 + log.warning("[%s] 404 — no data for this combination", job.key)
472 + return None
473 + r.raise_for_status()
474 + dest.parent.mkdir(parents=True, exist_ok=True)
475 + size = 0
476 + with open(part, "wb") as f:
477 + for chunk in r.iter_content(CHUNK_SIZE):
478 + f.write(chunk)
479 + size += len(chunk)
480 + part.replace(dest)
481 + log.info("[%s] downloaded %.1f MB", job.key, size / 1e6)
482 + return dest
483 + except (requests.RequestException, OSError) as e:
484 + part.unlink(missing_ok=True)
485 + if attempt == MAX_RETRIES:
486 + raise
487 + wait = RETRY_BACKOFF * attempt
488 + log.warning("[%s] attempt %d/%d failed (%s), retrying in %ds",
489 + job.key, attempt, MAX_RETRIES, e, wait)
490 + time.sleep(wait)
491 + return None
492 +
493 + def run_job(self, job: Job) -> None:
494 + if self.manifest.is_done(job.key):
495 + log.info("[%s] already done, skipping", job.key)
496 + return
497 + log.info("[%s] GET %s", job.key, job.display_url)
498 + t0 = time.time()
499 + try:
500 + if job.kind == "meta":
501 + self._run_meta(job)
502 + else:
503 + self._run_bars(job)
504 + except Exception as e:
505 + log.error("[%s] FAILED: %s", job.key, e)
506 + self.manifest.mark(job.key, "failed", error=str(e))
507 + return
508 + log.info("[%s] done in %.0fs", job.key, time.time() - t0)
509 +
510 + def _run_meta(self, job: Job) -> None:
511 + out_dir = self.data_root / job.dest_subdir
512 + out_dir.mkdir(parents=True, exist_ok=True)
513 + fname = "_".join(str(v) for v in job.params.values()) + ".csv"
514 + out = out_dir / fname
515 + r = self.session.get(job.url, timeout=600)
516 + r.raise_for_status()
517 + out.write_bytes(r.content)
518 + self.manifest.mark(job.key, "done", bytes=len(r.content), file=str(out))
519 +
520 + def _run_bars(self, job: Job) -> None:
521 + zip_dir = self.data_root / "zips"
522 + zip_dir.mkdir(parents=True, exist_ok=True)
523 + zip_path = zip_dir / (job.key.replace("|", "_") + ".zip")
524 +
525 + got = self._download(job, zip_path)
526 + if got is None:
527 + self.manifest.mark(job.key, "empty")
528 + return
529 + if not zipfile.is_zipfile(zip_path):
530 + head = zip_path.read_bytes()[:200]
531 + zip_path.unlink()
532 + raise RuntimeError(f"Response is not a zip (starts with {head[:60]!r})")
533 +
534 + asset_type = job.key.split("|")[0]
535 + dest_dir = self.data_root / job.dest_subdir
536 + files, rows = extract_and_convert(zip_path, dest_dir, asset_type)
537 + size = zip_path.stat().st_size
538 + if not self.keep_zips:
539 + zip_path.unlink()
540 + self.manifest.mark(job.key, "done", bytes=size, files=files, rows=rows)
541 + log.info("[%s] converted %d files / %s rows -> %s",
542 + job.key, files, f"{rows:,}", dest_dir)
543 +
544 +
545 +# ------------------------------------------------------------------------------
546 +# CLI
547 +# ------------------------------------------------------------------------------
548 +
549 +
550 +def setup_logging(data_root: Path) -> None:
551 + log_dir = data_root / "logs"
552 + log_dir.mkdir(parents=True, exist_ok=True)
553 + fmt = logging.Formatter("%(asctime)s %(levelname)-7s %(message)s",
554 + "%Y-%m-%d %H:%M:%S")
555 + for handler in (logging.StreamHandler(sys.stdout),
556 + logging.FileHandler(log_dir / "frd_downloader.log")):
557 + handler.setFormatter(fmt)
558 + log.addHandler(handler)
559 + log.setLevel(logging.INFO)
560 +
561 +
562 +def main() -> int:
563 + ap = argparse.ArgumentParser(
564 + description="FirstRate Data full-history downloader "
565 + f"(v{__version__} — {__author__} <{__contact__}>)")
566 + ap.add_argument("--data-root", type=Path,
567 + default=Path("/Volumes/ssd/firstratedata"),
568 + help="Root directory for all data (default: %(default)s)")
569 + ap.add_argument("--types", nargs="+",
570 + default=["options", "stock", "etf", "futures",
571 + "crypto", "index", "fx"],
572 + choices=["options", "stock", "etf", "futures",
573 + "crypto", "index", "fx"],
574 + help="Asset types to download (default: all)")
575 + ap.add_argument("--workers", type=int, default=2,
576 + help="Parallel downloads (default: %(default)s — be polite)")
577 + ap.add_argument("--keep-zips", action="store_true",
578 + help="Keep raw zip archives after conversion")
579 + ap.add_argument("--include-delisted", action="store_true",
580 + help="Also download the delisted-stocks archives")
581 + ap.add_argument("--min-free-gb", type=int, default=MIN_FREE_GB,
582 + help="Stop downloading below this free space (default: %(default)s)")
583 + ap.add_argument("--list-jobs", action="store_true",
584 + help="Print the job plan and exit")
585 + args = ap.parse_args()
586 +
587 + if not USERID:
588 + ap.error("FRD_USERID environment variable is not set "
589 + "(your FirstRate Data customer id)")
590 +
591 + jobs = build_jobs(args.types, args.include_delisted)
592 +
593 + if args.list_jobs:
594 + for j in jobs:
595 + print(f"{j.priority:3d} {j.key:45s} {j.display_url}")
596 + print(f"\nTotal: {len(jobs)} jobs")
597 + return 0
598 +
599 + args.data_root.mkdir(parents=True, exist_ok=True)
600 + setup_logging(args.data_root)
601 + log.info("FirstRate Data downloader v%s — %s <%s>",
602 + __version__, __author__, __contact__)
603 + log.info("Data root: %s | types: %s | %d jobs | %d workers",
604 + args.data_root, ",".join(args.types), len(jobs), args.workers)
605 +
606 + manifest = Manifest(args.data_root / "state" / "manifest.json")
607 + (args.data_root / "state").mkdir(parents=True, exist_ok=True)
608 + runner = Runner(args.data_root, manifest,
609 + keep_zips=args.keep_zips, min_free_gb=args.min_free_gb)
610 +
611 + pending = [j for j in jobs if not manifest.is_done(j.key)]
612 + log.info("%d jobs pending (%d already complete)",
613 + len(pending), len(jobs) - len(pending))
614 +
615 + with ThreadPoolExecutor(max_workers=args.workers) as pool:
616 + futures = {pool.submit(runner.run_job, j): j for j in pending}
617 + for fut in as_completed(futures):
618 + fut.result() # exceptions are handled inside run_job
619 +
620 + counts = manifest.summary()
621 + log.info("Run complete. Manifest: %s", counts)
622 + failed = counts.get("failed", 0)
623 + return 1 if failed else 0
624 +
625 +
626 +if __name__ == "__main__":
627 + sys.exit(main())
added hfmarketdata/api/main.py +476 −0
@@ -0,0 +1,476 @@
1 +#!/usr/bin/env python3
2 +# -*- coding: utf-8 -*-
3 +"""
4 +================================================================================
5 + HF Market Data — Open High-Frequency Market Data API
6 + https://www.hfmarketdata.io
7 +================================================================================
8 +
9 +Ultra-fast open REST API serving historical market data (stocks, ETFs,
10 +futures, crypto, indices, FX — 1-minute to daily bars — plus full end-of-day
11 +options chains with quotes, implied volatilities and Greeks).
12 +
13 +The data layer is a partitioned Parquet lake (zstd) queried in-place with
14 +DuckDB — no database server, predicate pushdown straight into the row groups.
15 +
16 +Layout expected under DATA_ROOT (built by frd_downloader.py):
17 + parquet/{type}/{timeframe}/{adjustment}/{TICKER}_*.parquet
18 + parquet/options/{year}_{quarter}/{TICKER}_*_option_chain.parquet
19 +
20 +Author : Simon-Pierre Boucher
21 +Contact : contact@spboucher.ai
22 +================================================================================
23 +"""
24 +
25 +from __future__ import annotations
26 +
27 +import os
28 +import threading
29 +import time
30 +from pathlib import Path
31 +
32 +import duckdb
33 +from fastapi import FastAPI, HTTPException, Query
34 +from fastapi.middleware.cors import CORSMiddleware
35 +from fastapi.responses import PlainTextResponse
36 +from fastapi.staticfiles import StaticFiles
37 +
38 +__author__ = "Simon-Pierre Boucher"
39 +__contact__ = "contact@spboucher.ai"
40 +__version__ = "1.0.0"
41 +
42 +DATA_ROOT = Path(os.environ.get("HFMD_DATA_ROOT", "/Volumes/ssd/firstratedata"))
43 +PARQUET = DATA_ROOT / "parquet"
44 +WEB_DIST = Path(os.environ.get("HFMD_WEB_DIST",
45 + Path(__file__).resolve().parent.parent / "web" / "dist"))
46 +
47 +BAR_TYPES = ["stock", "etf", "futures", "futures_contracts", "crypto", "index", "fx"]
48 +TIMEFRAMES = ["1min", "5min", "30min", "1hour", "1day"]
49 +DEFAULT_ADJUSTMENT = {
50 + "stock": "adj_splitdiv", "etf": "adj_splitdiv",
51 + "futures": "contin_UNadj", "futures_contracts": "archive",
52 + "crypto": "none", "index": "none", "fx": "none",
53 +}
54 +MAX_LIMIT_JSON = 50_000
55 +MAX_LIMIT_CSV = 2_000_000
56 +
57 +app = FastAPI(
58 + title="HF Market Data API",
59 + description=(
60 + "Open high-frequency market data API — intraday & daily bars for "
61 + "stocks, ETFs, futures, crypto, indices and FX, plus full historical "
62 + "options chains (quotes, implied volatility, Greeks) since 2010. "
63 + f"Built by {__author__} ({__contact__})."
64 + ),
65 + version=__version__,
66 + contact={"name": __author__, "email": __contact__,
67 + "url": "https://www.hfmarketdata.io"},
68 +)
69 +app.add_middleware(
70 + CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
71 +)
72 +
73 +# ------------------------------------------------------------------------------
74 +# DuckDB — one connection per thread, read-only usage
75 +# ------------------------------------------------------------------------------
76 +
77 +_tls = threading.local()
78 +
79 +
80 +def db() -> duckdb.DuckDBPyConnection:
81 + if not hasattr(_tls, "con"):
82 + _tls.con = duckdb.connect()
83 + _tls.con.execute("SET threads TO 4")
84 + _tls.con.execute("SET enable_object_cache=true")
85 + return _tls.con
86 +
87 +
88 +# ------------------------------------------------------------------------------
89 +# File resolution with a small TTL cache (directory scans are the slow part)
90 +# ------------------------------------------------------------------------------
91 +
92 +_cache: dict[str, tuple[float, object]] = {}
93 +_cache_lock = threading.Lock()
94 +CACHE_TTL = 300 # seconds
95 +
96 +
97 +def cached(key: str, builder):
98 + now = time.time()
99 + with _cache_lock:
100 + hit = _cache.get(key)
101 + if hit and now - hit[0] < CACHE_TTL:
102 + return hit[1]
103 + value = builder()
104 + with _cache_lock:
105 + _cache[key] = (now, value)
106 + return value
107 +
108 +
109 +def bar_dir(asset: str, timeframe: str, adjustment: str) -> Path:
110 + return PARQUET / asset / timeframe / adjustment
111 +
112 +
113 +def list_adjustments(asset: str, timeframe: str) -> list[str]:
114 + base = PARQUET / asset / timeframe
115 + if not base.is_dir():
116 + return []
117 + return sorted(p.name for p in base.iterdir() if p.is_dir())
118 +
119 +
120 +def ticker_index(asset: str, timeframe: str, adjustment: str) -> dict[str, str]:
121 + """ticker -> parquet file path, cached."""
122 + d = bar_dir(asset, timeframe, adjustment)
123 +
124 + def build():
125 + idx: dict[str, str] = {}
126 + if d.is_dir():
127 + for p in d.iterdir():
128 + if p.suffix == ".parquet":
129 + idx[p.stem.split("_")[0].upper()] = str(p)
130 + return idx
131 +
132 + return cached(f"idx|{d}", build)
133 +
134 +
135 +def options_quarters() -> list[str]:
136 + def build():
137 + base = PARQUET / "options"
138 + if not base.is_dir():
139 + return []
140 + return sorted(p.name for p in base.iterdir() if p.is_dir())
141 + return cached("opt_quarters", build)
142 +
143 +
144 +def options_files(ticker: str) -> list[str]:
145 + """All quarterly chain files for a ticker, cheap direct-path checks."""
146 + t = ticker.upper()
147 +
148 + def build():
149 + out = []
150 + for q in options_quarters():
151 + d = PARQUET / "options" / q
152 + for p in d.glob(f"{t}_*.parquet"):
153 + if p.stem.split("_")[0].upper() == t:
154 + out.append(str(p))
155 + return out
156 +
157 + return cached(f"optfiles|{t}", build)
158 +
159 +
160 +def _validate(asset: str, timeframe: str, adjustment: str | None) -> str:
161 + if asset not in BAR_TYPES:
162 + raise HTTPException(404, f"Unknown asset type '{asset}'. "
163 + f"Available: {', '.join(BAR_TYPES)}")
164 + if timeframe not in TIMEFRAMES:
165 + raise HTTPException(400, f"Unknown timeframe '{timeframe}'. "
166 + f"Available: {', '.join(TIMEFRAMES)}")
167 + adj = adjustment or DEFAULT_ADJUSTMENT[asset]
168 + available = list_adjustments(asset, timeframe)
169 + if available and adj not in available:
170 + raise HTTPException(400, f"Adjustment '{adj}' not available for "
171 + f"{asset}/{timeframe}. Available: {', '.join(available)}")
172 + return adj
173 +
174 +
175 +def _rows_to_response(rel, limit: int, fmt: str, order_col: str):
176 + if fmt == "csv":
177 + limit = min(limit, MAX_LIMIT_CSV)
178 + rel = rel.limit(limit)
179 + df = rel.df()
180 + return PlainTextResponse(df.to_csv(index=False), media_type="text/csv")
181 + limit = min(limit, MAX_LIMIT_JSON)
182 + rel = rel.limit(limit)
183 + df = rel.df()
184 + for col in df.columns:
185 + if str(df[col].dtype).startswith("datetime"):
186 + df[col] = df[col].astype(str)
187 + return {"count": len(df), "data": df.to_dict(orient="records")}
188 +
189 +
190 +# ------------------------------------------------------------------------------
191 +# Meta endpoints
192 +# ------------------------------------------------------------------------------
193 +
194 +
195 +@app.get("/health", tags=["meta"])
196 +def health():
197 + return {"status": "ok", "service": "hfmarketdata-api", "version": __version__,
198 + "data_root_present": PARQUET.is_dir()}
199 +
200 +
201 +@app.get("/v1/status", tags=["meta"])
202 +def status():
203 + """Dataset inventory: every asset type, timeframe and adjustment with
204 + the number of instruments currently available."""
205 + def build():
206 + out = {}
207 + for asset in BAR_TYPES:
208 + base = PARQUET / asset
209 + if not base.is_dir():
210 + continue
211 + tfs = {}
212 + for tf in sorted(p.name for p in base.iterdir() if p.is_dir()):
213 + adjs = {}
214 + for adj in list_adjustments(asset, tf):
215 + n = sum(1 for p in (base / tf / adj).iterdir()
216 + if p.suffix == ".parquet")
217 + adjs[adj] = n
218 + tfs[tf] = adjs
219 + out[asset] = tfs
220 + quarters = options_quarters()
221 + if quarters:
222 + out["options"] = {
223 + "quarters": quarters,
224 + "files_latest_quarter":
225 + sum(1 for _ in (PARQUET / "options" / quarters[-1]).glob("*.parquet")),
226 + }
227 + return out
228 + return {"datasets": cached("status", build)}
229 +
230 +
231 +@app.get("/v1/{asset}/tickers", tags=["bars"])
232 +def tickers(asset: str,
233 + timeframe: str = Query("1day"),
234 + adjustment: str | None = Query(None),
235 + search: str | None = Query(None, description="Substring filter"),
236 + limit: int = Query(10_000, ge=1)):
237 + """List available tickers for an asset type / timeframe / adjustment.
238 + For `options`, this delegates to the options underlyings listing."""
239 + if asset == "options":
240 + return opt_tickers(quarter=None, search=search, limit=limit)
241 + adj = _validate(asset, timeframe, adjustment)
242 + idx = ticker_index(asset, timeframe, adj)
243 + names = sorted(idx)
244 + if search:
245 + s = search.upper()
246 + names = [t for t in names if s in t]
247 + return {"asset": asset, "timeframe": timeframe, "adjustment": adj,
248 + "count": len(names[:limit]), "tickers": names[:limit]}
249 +
250 +
251 +# ------------------------------------------------------------------------------
252 +# Bars
253 +# ------------------------------------------------------------------------------
254 +
255 +
256 +@app.get("/v1/bars/{asset}/{ticker}", tags=["bars"])
257 +def bars(asset: str, ticker: str,
258 + timeframe: str = Query("1day", description="1min|5min|30min|1hour|1day"),
259 + adjustment: str | None = Query(None, description="Default depends on asset type"),
260 + start: str | None = Query(None, description="ISO date/datetime lower bound"),
261 + end: str | None = Query(None, description="ISO date/datetime upper bound"),
262 + order: str = Query("asc", pattern="^(asc|desc)$"),
263 + limit: int = Query(5_000, ge=1),
264 + format: str = Query("json", pattern="^(json|csv)$")):
265 + """OHLCV bars for one instrument. Daily futures bars also carry open
266 + interest. `format=csv` allows bulk extraction (up to 2M rows)."""
267 + adj = _validate(asset, timeframe, adjustment)
268 + idx = ticker_index(asset, timeframe, adj)
269 + path = idx.get(ticker.upper())
270 + if not path:
271 + raise HTTPException(404, f"Ticker '{ticker.upper()}' not found in "
272 + f"{asset}/{timeframe}/{adj}")
273 + con = db()
274 + conds, params = [], []
275 + if start:
276 + conds.append("datetime >= ?"); params.append(start)
277 + if end:
278 + conds.append("datetime <= ?"); params.append(end)
279 + where = ("WHERE " + " AND ".join(conds)) if conds else ""
280 + rel = con.sql(
281 + f"SELECT * FROM read_parquet(?) {where} ORDER BY datetime {order.upper()}",
282 + params=[path, *params])
283 + return _rows_to_response(rel, limit, format, "datetime")
284 +
285 +
286 +@app.get("/v1/bars/{asset}", tags=["bars"])
287 +def bars_multi(asset: str,
288 + tickers: str = Query(..., description="Comma-separated list, e.g. AAPL,MSFT,TSLA (max 50)"),
289 + timeframe: str = Query("1day"),
290 + adjustment: str | None = Query(None),
291 + start: str | None = Query(None, description="ISO date/datetime lower bound"),
292 + end: str | None = Query(None, description="ISO date/datetime upper bound"),
293 + order: str = Query("asc", pattern="^(asc|desc)$"),
294 + limit: int = Query(5_000, ge=1, description="Max rows PER TICKER"),
295 + format: str = Query("json", pattern="^(json|csv)$")):
296 + """Bars for SEVERAL instruments in one call. Combine with `start`/`end`
297 + to slice any precise window — e.g. 4 hours of 1-minute bars across a
298 + whole watchlist. The row limit applies per ticker."""
299 + adj = _validate(asset, timeframe, adjustment)
300 + idx = ticker_index(asset, timeframe, adj)
301 + names = [t.strip().upper() for t in tickers.split(",") if t.strip()][:50]
302 + if not names:
303 + raise HTTPException(400, "No tickers given")
304 + missing = [t for t in names if t not in idx]
305 + if missing:
306 + raise HTTPException(404, f"Not found in {asset}/{timeframe}/{adj}: "
307 + f"{', '.join(missing)}")
308 + paths = [idx[t] for t in names]
309 + con = db()
310 + conds, params = [], []
311 + if start:
312 + conds.append("datetime >= ?"); params.append(start)
313 + if end:
314 + conds.append("datetime <= ?"); params.append(end)
315 + where = ("WHERE " + " AND ".join(conds)) if conds else ""
316 + per_ticker = min(limit, MAX_LIMIT_CSV if format == "csv" else MAX_LIMIT_JSON)
317 + rel = con.sql(
318 + f"SELECT * FROM read_parquet(?) {where} "
319 + f"QUALIFY row_number() OVER (PARTITION BY ticker ORDER BY datetime "
320 + f"{order.upper()}) <= ? ORDER BY ticker, datetime {order.upper()}",
321 + params=[paths, *params, per_ticker])
322 + return _rows_to_response(rel, per_ticker * len(names), format, "datetime")
323 +
324 +
325 +@app.get("/v1/snapshot/{asset}", tags=["bars"])
326 +def snapshot(asset: str,
327 + tickers: str = Query(..., description="Comma-separated list, e.g. AAPL,MSFT,TSLA (max 50)"),
328 + at: str = Query(..., description="Precise moment, e.g. 2024-06-03 10:35:00"),
329 + timeframe: str = Query("1min"),
330 + adjustment: str | None = Query(None),
331 + format: str = Query("json", pattern="^(json|csv)$")):
332 + """Cross-sectional snapshot: for each requested instrument, the last bar
333 + at or before the given moment — the state of a whole watchlist at one
334 + precise point in time."""
335 + adj = _validate(asset, timeframe, adjustment)
336 + idx = ticker_index(asset, timeframe, adj)
337 + names = [t.strip().upper() for t in tickers.split(",") if t.strip()][:50]
338 + if not names:
339 + raise HTTPException(400, "No tickers given")
340 + missing = [t for t in names if t not in idx]
341 + if missing:
342 + raise HTTPException(404, f"Not found in {asset}/{timeframe}/{adj}: "
343 + f"{', '.join(missing)}")
344 + paths = [idx[t] for t in names]
345 + con = db()
346 + rel = con.sql(
347 + "SELECT * FROM read_parquet(?) WHERE datetime <= ? "
348 + "QUALIFY row_number() OVER (PARTITION BY ticker ORDER BY datetime DESC) = 1 "
349 + "ORDER BY ticker",
350 + params=[paths, at])
351 + return _rows_to_response(rel, len(names), format, "datetime")
352 +
353 +
354 +# ------------------------------------------------------------------------------
355 +# Options
356 +# ------------------------------------------------------------------------------
357 +
358 +
359 +@app.get("/v1/options/quarters", tags=["options"])
360 +def opt_quarters():
361 + """Available options archive quarters (e.g. 2010_q1 … current)."""
362 + return {"quarters": options_quarters()}
363 +
364 +
365 +@app.get("/v1/options/tickers", tags=["options"])
366 +def opt_tickers(quarter: str | None = Query(None, description="e.g. 2024_q4; default latest"),
367 + search: str | None = None,
368 + limit: int = Query(10_000, ge=1)):
369 + """Underlyings available in a given options quarter."""
370 + quarters = options_quarters()
371 + if not quarters:
372 + raise HTTPException(503, "Options dataset not yet available")
373 + q = quarter or quarters[-1]
374 + if q not in quarters:
375 + raise HTTPException(404, f"Unknown quarter '{q}'")
376 +
377 + def build():
378 + return sorted({p.stem.split("_")[0].upper()
379 + for p in (PARQUET / "options" / q).glob("*.parquet")})
380 + names = cached(f"opt_tickers|{q}", build)
381 + if search:
382 + s = search.upper()
383 + names = [t for t in names if s in t]
384 + return {"quarter": q, "count": len(names[:limit]), "tickers": names[:limit]}
385 +
386 +
387 +@app.get("/v1/options/chain/{ticker}", tags=["options"])
388 +def opt_chain(ticker: str,
389 + trade_date: str | None = Query(None, description="yyyy-mm-dd; default latest available"),
390 + expiry: str | None = Query(None, description="Filter on expiry date"),
391 + call_put: str | None = Query(None, pattern="^(c|p)$"),
392 + strike_min: float | None = None,
393 + strike_max: float | None = None,
394 + min_volume: float | None = None,
395 + limit: int = Query(20_000, ge=1),
396 + format: str = Query("json", pattern="^(json|csv)$")):
397 + """Full end-of-day option chain for an underlying: quotes, bid/ask implied
398 + volatility, open interest, volume and Greeks (delta, gamma, vega, theta, rho)."""
399 + files = options_files(ticker)
400 + if not files:
401 + raise HTTPException(404, f"No options data for '{ticker.upper()}'")
402 + con = db()
403 + if trade_date is None:
404 + trade_date = con.execute(
405 + "SELECT max(trade_date) FROM read_parquet(?)", [files[-1]]
406 + ).fetchone()[0]
407 + files_q = [files[-1]]
408 + else:
409 + files_q = files
410 + conds, params = ["trade_date = ?"], [str(trade_date)]
411 + if expiry:
412 + conds.append("expiry = ?"); params.append(expiry)
413 + if call_put:
414 + conds.append("call_put = ?"); params.append(call_put)
415 + if strike_min is not None:
416 + conds.append("strike >= ?"); params.append(strike_min)
417 + if strike_max is not None:
418 + conds.append("strike <= ?"); params.append(strike_max)
419 + if min_volume is not None:
420 + conds.append("volume >= ?"); params.append(min_volume)
421 + rel = con.sql(
422 + f"SELECT * FROM read_parquet(?) WHERE {' AND '.join(conds)} "
423 + f"ORDER BY expiry, strike, call_put",
424 + params=[files_q, *params])
425 + return _rows_to_response(rel, limit, format, "expiry")
426 +
427 +
428 +@app.get("/v1/options/expirations/{ticker}", tags=["options"])
429 +def opt_expirations(ticker: str,
430 + trade_date: str | None = Query(None, description="yyyy-mm-dd")):
431 + """Available expiry dates (optionally as of one trade date)."""
432 + files = options_files(ticker)
433 + if not files:
434 + raise HTTPException(404, f"No options data for '{ticker.upper()}'")
435 + con = db()
436 + if trade_date:
437 + rows = con.execute(
438 + "SELECT DISTINCT expiry FROM read_parquet(?) WHERE trade_date = ? ORDER BY expiry",
439 + [files, trade_date]).fetchall()
440 + else:
441 + rows = con.execute(
442 + "SELECT DISTINCT expiry FROM read_parquet(?) ORDER BY expiry",
443 + [files[-1]]).fetchall()
444 + return {"ticker": ticker.upper(), "expirations": [str(r[0]) for r in rows]}
445 +
446 +
447 +@app.get("/v1/options/history/{ticker}", tags=["options"])
448 +def opt_history(ticker: str,
449 + strike: float = Query(...),
450 + expiry: str = Query(..., description="yyyy-mm-dd"),
451 + call_put: str = Query(..., pattern="^(c|p)$"),
452 + limit: int = Query(5_000, ge=1),
453 + format: str = Query("json", pattern="^(json|csv)$")):
454 + """Daily time series for one specific contract across its whole life."""
455 + files = options_files(ticker)
456 + if not files:
457 + raise HTTPException(404, f"No options data for '{ticker.upper()}'")
458 + con = db()
459 + rel = con.sql(
460 + "SELECT * FROM read_parquet(?) WHERE strike = ? AND expiry = ? AND call_put = ? "
461 + "ORDER BY trade_date",
462 + params=[files, strike, expiry, call_put])
463 + return _rows_to_response(rel, limit, format, "trade_date")
464 +
465 +
466 +# ------------------------------------------------------------------------------
467 +# Static React platform (mounted last so it doesn't shadow the API)
468 +# ------------------------------------------------------------------------------
469 +
470 +if WEB_DIST.is_dir():
471 + app.mount("/", StaticFiles(directory=str(WEB_DIST), html=True), name="web")
472 +
473 +
474 +if __name__ == "__main__":
475 + import uvicorn
476 + uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("HFMD_PORT", 8090)))
added hfmarketdata/requirements.txt +4 −0
@@ -0,0 +1,4 @@
1 +fastapi>=0.115
2 +uvicorn>=0.30
3 +duckdb>=1.0
4 +pandas>=2.0
added hfmarketdata/web/index.html +16 −0
@@ -0,0 +1,16 @@
1 +<!doctype html>
2 +<html lang="en">
3 + <head>
4 + <meta charset="UTF-8" />
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 + <meta name="description" content="HF Market Data — free open API for high-frequency historical market data: stocks, ETFs, futures, crypto, indices, FX (1-minute to daily) and full options chains with Greeks since 2010." />
7 + <title>HF Market Data — Open High-Frequency Market Data API</title>
8 + <link rel="preconnect" href="https://fonts.googleapis.com" />
9 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10 + <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400;1,9..144,500&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
11 + </head>
12 + <body>
13 + <div id="root"></div>
14 + <script type="module" src="/src/main.jsx"></script>
15 + </body>
16 +</html>
added hfmarketdata/web/package-lock.json +1857 −0
@@ -0,0 +1,1857 @@
1 +{
2 + "name": "hfmarketdata-web",
3 + "version": "1.0.0",
4 + "lockfileVersion": 3,
5 + "requires": true,
6 + "packages": {
7 + "": {
8 + "name": "hfmarketdata-web",
9 + "version": "1.0.0",
10 + "dependencies": {
11 + "react": "^18.3.1",
12 + "react-dom": "^18.3.1"
13 + },
14 + "devDependencies": {
15 + "@vitejs/plugin-react": "^4.3.4",
16 + "vite": "^6.0.0"
17 + }
18 + },
19 + "node_modules/@babel/code-frame": {
20 + "version": "7.29.7",
21 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
22 + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
23 + "dev": true,
24 + "license": "MIT",
25 + "dependencies": {
26 + "@babel/helper-validator-identifier": "^7.29.7",
27 + "js-tokens": "^4.0.0",
28 + "picocolors": "^1.1.1"
29 + },
30 + "engines": {
31 + "node": ">=6.9.0"
32 + }
33 + },
34 + "node_modules/@babel/compat-data": {
35 + "version": "7.29.7",
36 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
37 + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
38 + "dev": true,
39 + "license": "MIT",
40 + "engines": {
41 + "node": ">=6.9.0"
42 + }
43 + },
44 + "node_modules/@babel/core": {
45 + "version": "7.29.7",
46 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
47 + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
48 + "dev": true,
49 + "license": "MIT",
50 + "dependencies": {
51 + "@babel/code-frame": "^7.29.7",
52 + "@babel/generator": "^7.29.7",
53 + "@babel/helper-compilation-targets": "^7.29.7",
54 + "@babel/helper-module-transforms": "^7.29.7",
55 + "@babel/helpers": "^7.29.7",
56 + "@babel/parser": "^7.29.7",
57 + "@babel/template": "^7.29.7",
58 + "@babel/traverse": "^7.29.7",
59 + "@babel/types": "^7.29.7",
60 + "@jridgewell/remapping": "^2.3.5",
61 + "convert-source-map": "^2.0.0",
62 + "debug": "^4.1.0",
63 + "gensync": "^1.0.0-beta.2",
64 + "json5": "^2.2.3",
65 + "semver": "^6.3.1"
66 + },
67 + "engines": {
68 + "node": ">=6.9.0"
69 + },
70 + "funding": {
71 + "type": "opencollective",
72 + "url": "https://opencollective.com/babel"
73 + }
74 + },
75 + "node_modules/@babel/generator": {
76 + "version": "7.29.8",
77 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
78 + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
79 + "dev": true,
80 + "license": "MIT",
81 + "dependencies": {
82 + "@babel/parser": "^7.29.8",
83 + "@babel/types": "^7.29.8",
84 + "@jridgewell/gen-mapping": "^0.3.12",
85 + "@jridgewell/trace-mapping": "^0.3.28",
86 + "jsesc": "^3.0.2"
87 + },
88 + "engines": {
89 + "node": ">=6.9.0"
90 + }
91 + },
92 + "node_modules/@babel/helper-compilation-targets": {
93 + "version": "7.29.7",
94 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
95 + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
96 + "dev": true,
97 + "license": "MIT",
98 + "dependencies": {
99 + "@babel/compat-data": "^7.29.7",
100 + "@babel/helper-validator-option": "^7.29.7",
101 + "browserslist": "^4.24.0",
102 + "lru-cache": "^5.1.1",
103 + "semver": "^6.3.1"
104 + },
105 + "engines": {
106 + "node": ">=6.9.0"
107 + }
108 + },
109 + "node_modules/@babel/helper-globals": {
110 + "version": "7.29.7",
111 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
112 + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
113 + "dev": true,
114 + "license": "MIT",
115 + "engines": {
116 + "node": ">=6.9.0"
117 + }
118 + },
119 + "node_modules/@babel/helper-module-imports": {
120 + "version": "7.29.7",
121 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
122 + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
123 + "dev": true,
124 + "license": "MIT",
125 + "dependencies": {
126 + "@babel/traverse": "^7.29.7",
127 + "@babel/types": "^7.29.7"
128 + },
129 + "engines": {
130 + "node": ">=6.9.0"
131 + }
132 + },
133 + "node_modules/@babel/helper-module-transforms": {
134 + "version": "7.29.7",
135 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
136 + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
137 + "dev": true,
138 + "license": "MIT",
139 + "dependencies": {
140 + "@babel/helper-module-imports": "^7.29.7",
141 + "@babel/helper-validator-identifier": "^7.29.7",
142 + "@babel/traverse": "^7.29.7"
143 + },
144 + "engines": {
145 + "node": ">=6.9.0"
146 + },
147 + "peerDependencies": {
148 + "@babel/core": "^7.0.0"
149 + }
150 + },
151 + "node_modules/@babel/helper-plugin-utils": {
152 + "version": "7.29.7",
153 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
154 + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
155 + "dev": true,
156 + "license": "MIT",
157 + "engines": {
158 + "node": ">=6.9.0"
159 + }
160 + },
161 + "node_modules/@babel/helper-string-parser": {
162 + "version": "7.29.7",
163 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
164 + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
165 + "dev": true,
166 + "license": "MIT",
167 + "engines": {
168 + "node": ">=6.9.0"
169 + }
170 + },
171 + "node_modules/@babel/helper-validator-identifier": {
172 + "version": "7.29.7",
173 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
174 + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
175 + "dev": true,
176 + "license": "MIT",
177 + "engines": {
178 + "node": ">=6.9.0"
179 + }
180 + },
181 + "node_modules/@babel/helper-validator-option": {
182 + "version": "7.29.7",
183 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
184 + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
185 + "dev": true,
186 + "license": "MIT",
187 + "engines": {
188 + "node": ">=6.9.0"
189 + }
190 + },
191 + "node_modules/@babel/helpers": {
192 + "version": "7.29.7",
193 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
194 + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
195 + "dev": true,
196 + "license": "MIT",
197 + "dependencies": {
198 + "@babel/template": "^7.29.7",
199 + "@babel/types": "^7.29.7"
200 + },
201 + "engines": {
202 + "node": ">=6.9.0"
203 + }
204 + },
205 + "node_modules/@babel/parser": {
206 + "version": "7.29.8",
207 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
208 + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
209 + "dev": true,
210 + "license": "MIT",
211 + "dependencies": {
212 + "@babel/types": "^7.29.8"
213 + },
214 + "bin": {
215 + "parser": "bin/babel-parser.js"
216 + },
217 + "engines": {
218 + "node": ">=6.0.0"
219 + }
220 + },
221 + "node_modules/@babel/plugin-transform-react-jsx-self": {
222 + "version": "7.29.7",
223 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
224 + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
225 + "dev": true,
226 + "license": "MIT",
227 + "dependencies": {
228 + "@babel/helper-plugin-utils": "^7.29.7"
229 + },
230 + "engines": {
231 + "node": ">=6.9.0"
232 + },
233 + "peerDependencies": {
234 + "@babel/core": "^7.0.0-0"
235 + }
236 + },
237 + "node_modules/@babel/plugin-transform-react-jsx-source": {
238 + "version": "7.29.7",
239 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
240 + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
241 + "dev": true,
242 + "license": "MIT",
243 + "dependencies": {
244 + "@babel/helper-plugin-utils": "^7.29.7"
245 + },
246 + "engines": {
247 + "node": ">=6.9.0"
248 + },
249 + "peerDependencies": {
250 + "@babel/core": "^7.0.0-0"
251 + }
252 + },
253 + "node_modules/@babel/template": {
254 + "version": "7.29.7",
255 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
256 + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
257 + "dev": true,
258 + "license": "MIT",
259 + "dependencies": {
260 + "@babel/code-frame": "^7.29.7",
261 + "@babel/parser": "^7.29.7",
262 + "@babel/types": "^7.29.7"
263 + },
264 + "engines": {
265 + "node": ">=6.9.0"
266 + }
267 + },
268 + "node_modules/@babel/traverse": {
269 + "version": "7.29.8",
270 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
271 + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
272 + "dev": true,
273 + "license": "MIT",
274 + "dependencies": {
275 + "@babel/code-frame": "^7.29.7",
276 + "@babel/generator": "^7.29.8",
277 + "@babel/helper-globals": "^7.29.7",
278 + "@babel/parser": "^7.29.8",
279 + "@babel/template": "^7.29.7",
280 + "@babel/types": "^7.29.8",
281 + "debug": "^4.3.1"
282 + },
283 + "engines": {
284 + "node": ">=6.9.0"
285 + }
286 + },
287 + "node_modules/@babel/types": {
288 + "version": "7.29.8",
289 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
290 + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
291 + "dev": true,
292 + "license": "MIT",
293 + "dependencies": {
294 + "@babel/helper-string-parser": "^7.29.7",
295 + "@babel/helper-validator-identifier": "^7.29.7"
296 + },
297 + "engines": {
298 + "node": ">=6.9.0"
299 + }
300 + },
301 + "node_modules/@esbuild/aix-ppc64": {
302 + "version": "0.25.12",
303 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
304 + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
305 + "cpu": [
306 + "ppc64"
307 + ],
308 + "dev": true,
309 + "license": "MIT",
310 + "optional": true,
311 + "os": [
312 + "aix"
313 + ],
314 + "engines": {
315 + "node": ">=18"
316 + }
317 + },
318 + "node_modules/@esbuild/android-arm": {
319 + "version": "0.25.12",
320 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
321 + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
322 + "cpu": [
323 + "arm"
324 + ],
325 + "dev": true,
326 + "license": "MIT",
327 + "optional": true,
328 + "os": [
329 + "android"
330 + ],
331 + "engines": {
332 + "node": ">=18"
333 + }
334 + },
335 + "node_modules/@esbuild/android-arm64": {
336 + "version": "0.25.12",
337 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
338 + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
339 + "cpu": [
340 + "arm64"
341 + ],
342 + "dev": true,
343 + "license": "MIT",
344 + "optional": true,
345 + "os": [
346 + "android"
347 + ],
348 + "engines": {
349 + "node": ">=18"
350 + }
351 + },
352 + "node_modules/@esbuild/android-x64": {
353 + "version": "0.25.12",
354 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
355 + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
356 + "cpu": [
357 + "x64"
358 + ],
359 + "dev": true,
360 + "license": "MIT",
361 + "optional": true,
362 + "os": [
363 + "android"
364 + ],
365 + "engines": {
366 + "node": ">=18"
367 + }
368 + },
369 + "node_modules/@esbuild/darwin-arm64": {
370 + "version": "0.25.12",
371 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
372 + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
373 + "cpu": [
374 + "arm64"
375 + ],
376 + "dev": true,
377 + "license": "MIT",
378 + "optional": true,
379 + "os": [
380 + "darwin"
381 + ],
382 + "engines": {
383 + "node": ">=18"
384 + }
385 + },
386 + "node_modules/@esbuild/darwin-x64": {
387 + "version": "0.25.12",
388 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
389 + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
390 + "cpu": [
391 + "x64"
392 + ],
393 + "dev": true,
394 + "license": "MIT",
395 + "optional": true,
396 + "os": [
397 + "darwin"
398 + ],
399 + "engines": {
400 + "node": ">=18"
401 + }
402 + },
403 + "node_modules/@esbuild/freebsd-arm64": {
404 + "version": "0.25.12",
405 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
406 + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
407 + "cpu": [
408 + "arm64"
409 + ],
410 + "dev": true,
411 + "license": "MIT",
412 + "optional": true,
413 + "os": [
414 + "freebsd"
415 + ],
416 + "engines": {
417 + "node": ">=18"
418 + }
419 + },
420 + "node_modules/@esbuild/freebsd-x64": {
421 + "version": "0.25.12",
422 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
423 + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
424 + "cpu": [
425 + "x64"
426 + ],
427 + "dev": true,
428 + "license": "MIT",
429 + "optional": true,
430 + "os": [
431 + "freebsd"
432 + ],
433 + "engines": {
434 + "node": ">=18"
435 + }
436 + },
437 + "node_modules/@esbuild/linux-arm": {
438 + "version": "0.25.12",
439 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
440 + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
441 + "cpu": [
442 + "arm"
443 + ],
444 + "dev": true,
445 + "license": "MIT",
446 + "optional": true,
447 + "os": [
448 + "linux"
449 + ],
450 + "engines": {
451 + "node": ">=18"
452 + }
453 + },
454 + "node_modules/@esbuild/linux-arm64": {
455 + "version": "0.25.12",
456 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
457 + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
458 + "cpu": [
459 + "arm64"
460 + ],
461 + "dev": true,
462 + "license": "MIT",
463 + "optional": true,
464 + "os": [
465 + "linux"
466 + ],
467 + "engines": {
468 + "node": ">=18"
469 + }
470 + },
471 + "node_modules/@esbuild/linux-ia32": {
472 + "version": "0.25.12",
473 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
474 + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
475 + "cpu": [
476 + "ia32"
477 + ],
478 + "dev": true,
479 + "license": "MIT",
480 + "optional": true,
481 + "os": [
482 + "linux"
483 + ],
484 + "engines": {
485 + "node": ">=18"
486 + }
487 + },
488 + "node_modules/@esbuild/linux-loong64": {
489 + "version": "0.25.12",
490 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
491 + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
492 + "cpu": [
493 + "loong64"
494 + ],
495 + "dev": true,
496 + "license": "MIT",
497 + "optional": true,
498 + "os": [
499 + "linux"
500 + ],
501 + "engines": {
502 + "node": ">=18"
503 + }
504 + },
505 + "node_modules/@esbuild/linux-mips64el": {
506 + "version": "0.25.12",
507 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
508 + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
509 + "cpu": [
510 + "mips64el"
511 + ],
512 + "dev": true,
513 + "license": "MIT",
514 + "optional": true,
515 + "os": [
516 + "linux"
517 + ],
518 + "engines": {
519 + "node": ">=18"
520 + }
521 + },
522 + "node_modules/@esbuild/linux-ppc64": {
523 + "version": "0.25.12",
524 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
525 + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
526 + "cpu": [
527 + "ppc64"
528 + ],
529 + "dev": true,
530 + "license": "MIT",
531 + "optional": true,
532 + "os": [
533 + "linux"
534 + ],
535 + "engines": {
536 + "node": ">=18"
537 + }
538 + },
539 + "node_modules/@esbuild/linux-riscv64": {
540 + "version": "0.25.12",
541 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
542 + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
543 + "cpu": [
544 + "riscv64"
545 + ],
546 + "dev": true,
547 + "license": "MIT",
548 + "optional": true,
549 + "os": [
550 + "linux"
551 + ],
552 + "engines": {
553 + "node": ">=18"
554 + }
555 + },
556 + "node_modules/@esbuild/linux-s390x": {
557 + "version": "0.25.12",
558 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
559 + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
560 + "cpu": [
561 + "s390x"
562 + ],
563 + "dev": true,
564 + "license": "MIT",
565 + "optional": true,
566 + "os": [
567 + "linux"
568 + ],
569 + "engines": {
570 + "node": ">=18"
571 + }
572 + },
573 + "node_modules/@esbuild/linux-x64": {
574 + "version": "0.25.12",
575 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
576 + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
577 + "cpu": [
578 + "x64"
579 + ],
580 + "dev": true,
581 + "license": "MIT",
582 + "optional": true,
583 + "os": [
584 + "linux"
585 + ],
586 + "engines": {
587 + "node": ">=18"
588 + }
589 + },
590 + "node_modules/@esbuild/netbsd-arm64": {
591 + "version": "0.25.12",
592 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
593 + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
594 + "cpu": [
595 + "arm64"
596 + ],
597 + "dev": true,
598 + "license": "MIT",
599 + "optional": true,
600 + "os": [
601 + "netbsd"
602 + ],
603 + "engines": {
604 + "node": ">=18"
605 + }
606 + },
607 + "node_modules/@esbuild/netbsd-x64": {
608 + "version": "0.25.12",
609 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
610 + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
611 + "cpu": [
612 + "x64"
613 + ],
614 + "dev": true,
615 + "license": "MIT",
616 + "optional": true,
617 + "os": [
618 + "netbsd"
619 + ],
620 + "engines": {
621 + "node": ">=18"
622 + }
623 + },
624 + "node_modules/@esbuild/openbsd-arm64": {
625 + "version": "0.25.12",
626 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
627 + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
628 + "cpu": [
629 + "arm64"
630 + ],
631 + "dev": true,
632 + "license": "MIT",
633 + "optional": true,
634 + "os": [
635 + "openbsd"
636 + ],
637 + "engines": {
638 + "node": ">=18"
639 + }
640 + },
641 + "node_modules/@esbuild/openbsd-x64": {
642 + "version": "0.25.12",
643 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
644 + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
645 + "cpu": [
646 + "x64"
647 + ],
648 + "dev": true,
649 + "license": "MIT",
650 + "optional": true,
651 + "os": [
652 + "openbsd"
653 + ],
654 + "engines": {
655 + "node": ">=18"
656 + }
657 + },
658 + "node_modules/@esbuild/openharmony-arm64": {
659 + "version": "0.25.12",
660 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
661 + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
662 + "cpu": [
663 + "arm64"
664 + ],
665 + "dev": true,
666 + "license": "MIT",
667 + "optional": true,
668 + "os": [
669 + "openharmony"
670 + ],
671 + "engines": {
672 + "node": ">=18"
673 + }
674 + },
675 + "node_modules/@esbuild/sunos-x64": {
676 + "version": "0.25.12",
677 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
678 + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
679 + "cpu": [
680 + "x64"
681 + ],
682 + "dev": true,
683 + "license": "MIT",
684 + "optional": true,
685 + "os": [
686 + "sunos"
687 + ],
688 + "engines": {
689 + "node": ">=18"
690 + }
691 + },
692 + "node_modules/@esbuild/win32-arm64": {
693 + "version": "0.25.12",
694 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
695 + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
696 + "cpu": [
697 + "arm64"
698 + ],
699 + "dev": true,
700 + "license": "MIT",
701 + "optional": true,
702 + "os": [
703 + "win32"
704 + ],
705 + "engines": {
706 + "node": ">=18"
707 + }
708 + },
709 + "node_modules/@esbuild/win32-ia32": {
710 + "version": "0.25.12",
711 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
712 + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
713 + "cpu": [
714 + "ia32"
715 + ],
716 + "dev": true,
717 + "license": "MIT",
718 + "optional": true,
719 + "os": [
720 + "win32"
721 + ],
722 + "engines": {
723 + "node": ">=18"
724 + }
725 + },
726 + "node_modules/@esbuild/win32-x64": {
727 + "version": "0.25.12",
728 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
729 + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
730 + "cpu": [
731 + "x64"
732 + ],
733 + "dev": true,
734 + "license": "MIT",
735 + "optional": true,
736 + "os": [
737 + "win32"
738 + ],
739 + "engines": {
740 + "node": ">=18"
741 + }
742 + },
743 + "node_modules/@jridgewell/gen-mapping": {
744 + "version": "0.3.13",
745 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
746 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
747 + "dev": true,
748 + "license": "MIT",
749 + "dependencies": {
750 + "@jridgewell/sourcemap-codec": "^1.5.0",
751 + "@jridgewell/trace-mapping": "^0.3.24"
752 + }
753 + },
754 + "node_modules/@jridgewell/remapping": {
755 + "version": "2.3.5",
756 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
757 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
758 + "dev": true,
759 + "license": "MIT",
760 + "dependencies": {
761 + "@jridgewell/gen-mapping": "^0.3.5",
762 + "@jridgewell/trace-mapping": "^0.3.24"
763 + }
764 + },
765 + "node_modules/@jridgewell/resolve-uri": {
766 + "version": "3.1.2",
767 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
768 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
769 + "dev": true,
770 + "license": "MIT",
771 + "engines": {
772 + "node": ">=6.0.0"
773 + }
774 + },
775 + "node_modules/@jridgewell/sourcemap-codec": {
776 + "version": "1.5.5",
777 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
778 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
779 + "dev": true,
780 + "license": "MIT"
781 + },
782 + "node_modules/@jridgewell/trace-mapping": {
783 + "version": "0.3.31",
784 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
785 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
786 + "dev": true,
787 + "license": "MIT",
788 + "dependencies": {
789 + "@jridgewell/resolve-uri": "^3.1.0",
790 + "@jridgewell/sourcemap-codec": "^1.4.14"
791 + }
792 + },
793 + "node_modules/@napi-rs/lzma-linux-x64-gnu": {
794 + "version": "1.5.1",
795 + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
796 + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
797 + "cpu": [
798 + "x64"
799 + ],
800 + "dev": true,
801 + "libc": [
802 + "glibc"
803 + ],
804 + "license": "MIT",
805 + "optional": true,
806 + "os": [
807 + "linux"
808 + ],
809 + "engines": {
810 + "node": "^22.20 || ^24.12 || >=25"
811 + }
812 + },
813 + "node_modules/@rolldown/pluginutils": {
814 + "version": "1.0.0-beta.27",
815 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
816 + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
817 + "dev": true,
818 + "license": "MIT"
819 + },
820 + "node_modules/@rollup/rollup-android-arm-eabi": {
821 + "version": "4.62.4",
822 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
823 + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
824 + "cpu": [
825 + "arm"
826 + ],
827 + "dev": true,
828 + "license": "MIT",
829 + "optional": true,
830 + "os": [
831 + "android"
832 + ]
833 + },
834 + "node_modules/@rollup/rollup-android-arm64": {
835 + "version": "4.62.4",
836 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
837 + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
838 + "cpu": [
839 + "arm64"
840 + ],
841 + "dev": true,
842 + "license": "MIT",
843 + "optional": true,
844 + "os": [
845 + "android"
846 + ]
847 + },
848 + "node_modules/@rollup/rollup-darwin-arm64": {
849 + "version": "4.62.4",
850 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
851 + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
852 + "cpu": [
853 + "arm64"
854 + ],
855 + "dev": true,
856 + "license": "MIT",
857 + "optional": true,
858 + "os": [
859 + "darwin"
860 + ]
861 + },
862 + "node_modules/@rollup/rollup-darwin-x64": {
863 + "version": "4.62.4",
864 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
865 + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
866 + "cpu": [
867 + "x64"
868 + ],
869 + "dev": true,
870 + "license": "MIT",
871 + "optional": true,
872 + "os": [
873 + "darwin"
874 + ]
875 + },
876 + "node_modules/@rollup/rollup-freebsd-arm64": {
877 + "version": "4.62.4",
878 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
879 + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
880 + "cpu": [
881 + "arm64"
882 + ],
883 + "dev": true,
884 + "license": "MIT",
885 + "optional": true,
886 + "os": [
887 + "freebsd"
888 + ]
889 + },
890 + "node_modules/@rollup/rollup-freebsd-x64": {
891 + "version": "4.62.4",
892 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
893 + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
894 + "cpu": [
895 + "x64"
896 + ],
897 + "dev": true,
898 + "license": "MIT",
899 + "optional": true,
900 + "os": [
901 + "freebsd"
902 + ]
903 + },
904 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
905 + "version": "4.62.4",
906 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
907 + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
908 + "cpu": [
909 + "arm"
910 + ],
911 + "dev": true,
912 + "libc": [
913 + "glibc"
914 + ],
915 + "license": "MIT",
916 + "optional": true,
917 + "os": [
918 + "linux"
919 + ]
920 + },
921 + "node_modules/@rollup/rollup-linux-arm-musleabihf": {
922 + "version": "4.62.4",
923 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
924 + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
925 + "cpu": [
926 + "arm"
927 + ],
928 + "dev": true,
929 + "libc": [
930 + "musl"
931 + ],
932 + "license": "MIT",
933 + "optional": true,
934 + "os": [
935 + "linux"
936 + ]
937 + },
938 + "node_modules/@rollup/rollup-linux-arm64-gnu": {
939 + "version": "4.62.4",
940 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
941 + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
942 + "cpu": [
943 + "arm64"
944 + ],
945 + "dev": true,
946 + "libc": [
947 + "glibc"
948 + ],
949 + "license": "MIT",
950 + "optional": true,
951 + "os": [
952 + "linux"
953 + ]
954 + },
955 + "node_modules/@rollup/rollup-linux-arm64-musl": {
956 + "version": "4.62.4",
957 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
958 + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
959 + "cpu": [
960 + "arm64"
961 + ],
962 + "dev": true,
963 + "libc": [
964 + "musl"
965 + ],
966 + "license": "MIT",
967 + "optional": true,
968 + "os": [
969 + "linux"
970 + ]
971 + },
972 + "node_modules/@rollup/rollup-linux-loong64-gnu": {
973 + "version": "4.62.4",
974 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
975 + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
976 + "cpu": [
977 + "loong64"
978 + ],
979 + "dev": true,
980 + "libc": [
981 + "glibc"
982 + ],
983 + "license": "MIT",
984 + "optional": true,
985 + "os": [
986 + "linux"
987 + ]
988 + },
989 + "node_modules/@rollup/rollup-linux-loong64-musl": {
990 + "version": "4.62.4",
991 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
992 + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
993 + "cpu": [
994 + "loong64"
995 + ],
996 + "dev": true,
997 + "libc": [
998 + "musl"
999 + ],
1000 + "license": "MIT",
1001 + "optional": true,
1002 + "os": [
1003 + "linux"
1004 + ]
1005 + },
1006 + "node_modules/@rollup/rollup-linux-ppc64-gnu": {
1007 + "version": "4.62.4",
1008 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
1009 + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
1010 + "cpu": [
1011 + "ppc64"
1012 + ],
1013 + "dev": true,
1014 + "libc": [
1015 + "glibc"
1016 + ],
1017 + "license": "MIT",
1018 + "optional": true,
1019 + "os": [
1020 + "linux"
1021 + ]
1022 + },
1023 + "node_modules/@rollup/rollup-linux-ppc64-musl": {
1024 + "version": "4.62.4",
1025 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
1026 + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
1027 + "cpu": [
1028 + "ppc64"
1029 + ],
1030 + "dev": true,
1031 + "libc": [
1032 + "musl"
1033 + ],
1034 + "license": "MIT",
1035 + "optional": true,
1036 + "os": [
1037 + "linux"
1038 + ]
1039 + },
1040 + "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1041 + "version": "4.62.4",
1042 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
1043 + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
1044 + "cpu": [
1045 + "riscv64"
1046 + ],
1047 + "dev": true,
1048 + "libc": [
1049 + "glibc"
1050 + ],
1051 + "license": "MIT",
1052 + "optional": true,
1053 + "os": [
1054 + "linux"
1055 + ]
1056 + },
1057 + "node_modules/@rollup/rollup-linux-riscv64-musl": {
1058 + "version": "4.62.4",
1059 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
1060 + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
1061 + "cpu": [
1062 + "riscv64"
1063 + ],
1064 + "dev": true,
1065 + "libc": [
1066 + "musl"
1067 + ],
1068 + "license": "MIT",
1069 + "optional": true,
1070 + "os": [
1071 + "linux"
1072 + ]
1073 + },
1074 + "node_modules/@rollup/rollup-linux-s390x-gnu": {
1075 + "version": "4.62.4",
1076 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
1077 + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
1078 + "cpu": [
1079 + "s390x"
1080 + ],
1081 + "dev": true,
1082 + "libc": [
1083 + "glibc"
1084 + ],
1085 + "license": "MIT",
1086 + "optional": true,
1087 + "os": [
1088 + "linux"
1089 + ]
1090 + },
1091 + "node_modules/@rollup/rollup-linux-x64-gnu": {
1092 + "version": "4.62.4",
1093 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
1094 + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
1095 + "cpu": [
1096 + "x64"
1097 + ],
1098 + "dev": true,
1099 + "libc": [
1100 + "glibc"
1101 + ],
1102 + "license": "MIT",
1103 + "optional": true,
1104 + "os": [
1105 + "linux"
1106 + ]
1107 + },
1108 + "node_modules/@rollup/rollup-linux-x64-musl": {
1109 + "version": "4.62.4",
1110 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
1111 + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
1112 + "cpu": [
1113 + "x64"
1114 + ],
1115 + "dev": true,
1116 + "libc": [
1117 + "musl"
1118 + ],
1119 + "license": "MIT",
1120 + "optional": true,
1121 + "os": [
1122 + "linux"
1123 + ]
1124 + },
1125 + "node_modules/@rollup/rollup-openbsd-x64": {
1126 + "version": "4.62.4",
1127 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
1128 + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
1129 + "cpu": [
1130 + "x64"
1131 + ],
1132 + "dev": true,
1133 + "license": "MIT",
1134 + "optional": true,
1135 + "os": [
1136 + "openbsd"
1137 + ]
1138 + },
1139 + "node_modules/@rollup/rollup-openharmony-arm64": {
1140 + "version": "4.62.4",
1141 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
1142 + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
1143 + "cpu": [
1144 + "arm64"
1145 + ],
1146 + "dev": true,
1147 + "license": "MIT",
1148 + "optional": true,
1149 + "os": [
1150 + "openharmony"
1151 + ]
1152 + },
1153 + "node_modules/@rollup/rollup-win32-arm64-msvc": {
1154 + "version": "4.62.4",
1155 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
1156 + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
1157 + "cpu": [
1158 + "arm64"
1159 + ],
1160 + "dev": true,
1161 + "license": "MIT",
1162 + "optional": true,
1163 + "os": [
1164 + "win32"
1165 + ]
1166 + },
1167 + "node_modules/@rollup/rollup-win32-ia32-msvc": {
1168 + "version": "4.62.4",
1169 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
1170 + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
1171 + "cpu": [
1172 + "ia32"
1173 + ],
1174 + "dev": true,
1175 + "license": "MIT",
1176 + "optional": true,
1177 + "os": [
1178 + "win32"
1179 + ]
1180 + },
1181 + "node_modules/@rollup/rollup-win32-x64-gnu": {
1182 + "version": "4.62.4",
1183 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
1184 + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
1185 + "cpu": [
1186 + "x64"
1187 + ],
1188 + "dev": true,
1189 + "license": "MIT",
1190 + "optional": true,
1191 + "os": [
1192 + "win32"
1193 + ]
1194 + },
1195 + "node_modules/@rollup/rollup-win32-x64-msvc": {
1196 + "version": "4.62.4",
1197 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
1198 + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
1199 + "cpu": [
1200 + "x64"
1201 + ],
1202 + "dev": true,
1203 + "license": "MIT",
1204 + "optional": true,
1205 + "os": [
1206 + "win32"
1207 + ]
1208 + },
1209 + "node_modules/@types/babel__core": {
1210 + "version": "7.20.5",
1211 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1212 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1213 + "dev": true,
1214 + "license": "MIT",
1215 + "dependencies": {
1216 + "@babel/parser": "^7.20.7",
1217 + "@babel/types": "^7.20.7",
1218 + "@types/babel__generator": "*",
1219 + "@types/babel__template": "*",
1220 + "@types/babel__traverse": "*"
1221 + }
1222 + },
1223 + "node_modules/@types/babel__generator": {
1224 + "version": "7.27.0",
1225 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1226 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1227 + "dev": true,
1228 + "license": "MIT",
1229 + "dependencies": {
1230 + "@babel/types": "^7.0.0"
1231 + }
1232 + },
1233 + "node_modules/@types/babel__template": {
1234 + "version": "7.4.4",
1235 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1236 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1237 + "dev": true,
1238 + "license": "MIT",
1239 + "dependencies": {
1240 + "@babel/parser": "^7.1.0",
1241 + "@babel/types": "^7.0.0"
1242 + }
1243 + },
1244 + "node_modules/@types/babel__traverse": {
1245 + "version": "7.28.0",
1246 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1247 + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1248 + "dev": true,
1249 + "license": "MIT",
1250 + "dependencies": {
1251 + "@babel/types": "^7.28.2"
1252 + }
1253 + },
1254 + "node_modules/@types/estree": {
1255 + "version": "1.0.9",
1256 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1257 + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1258 + "dev": true,
1259 + "license": "MIT"
1260 + },
1261 + "node_modules/@vitejs/plugin-react": {
1262 + "version": "4.7.0",
1263 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1264 + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1265 + "dev": true,
1266 + "license": "MIT",
1267 + "dependencies": {
1268 + "@babel/core": "^7.28.0",
1269 + "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1270 + "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1271 + "@rolldown/pluginutils": "1.0.0-beta.27",
1272 + "@types/babel__core": "^7.20.5",
1273 + "react-refresh": "^0.17.0"
1274 + },
1275 + "engines": {
1276 + "node": "^14.18.0 || >=16.0.0"
1277 + },
1278 + "peerDependencies": {
1279 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1280 + }
1281 + },
1282 + "node_modules/baseline-browser-mapping": {
1283 + "version": "2.11.13",
1284 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz",
1285 + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==",
1286 + "dev": true,
1287 + "license": "Apache-2.0",
1288 + "bin": {
1289 + "baseline-browser-mapping": "dist/cli.cjs"
1290 + },
1291 + "engines": {
1292 + "node": ">=6.0.0"
1293 + }
1294 + },
1295 + "node_modules/browserslist": {
1296 + "version": "4.28.8",
1297 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
1298 + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
1299 + "dev": true,
1300 + "funding": [
1301 + {
1302 + "type": "opencollective",
1303 + "url": "https://opencollective.com/browserslist"
1304 + },
1305 + {
1306 + "type": "tidelift",
1307 + "url": "https://tidelift.com/funding/github/npm/browserslist"
1308 + },
1309 + {
1310 + "type": "github",
1311 + "url": "https://github.com/sponsors/ai"
1312 + }
1313 + ],
1314 + "license": "MIT",
1315 + "dependencies": {
1316 + "baseline-browser-mapping": "^2.11.12",
1317 + "caniuse-lite": "^1.0.30001809",
1318 + "electron-to-chromium": "^1.5.402",
1319 + "node-releases": "^2.0.53",
1320 + "update-browserslist-db": "^1.3.0"
1321 + },
1322 + "bin": {
1323 + "browserslist": "cli.js"
1324 + },
1325 + "engines": {
1326 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1327 + }
1328 + },
1329 + "node_modules/caniuse-lite": {
1330 + "version": "1.0.30001809",
1331 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
1332 + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
1333 + "dev": true,
1334 + "funding": [
1335 + {
1336 + "type": "opencollective",
1337 + "url": "https://opencollective.com/browserslist"
1338 + },
1339 + {
1340 + "type": "tidelift",
1341 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1342 + },
1343 + {
1344 + "type": "github",
1345 + "url": "https://github.com/sponsors/ai"
1346 + }
1347 + ],
1348 + "license": "CC-BY-4.0"
1349 + },
1350 + "node_modules/convert-source-map": {
1351 + "version": "2.0.0",
1352 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1353 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1354 + "dev": true,
1355 + "license": "MIT"
1356 + },
1357 + "node_modules/debug": {
1358 + "version": "4.4.3",
1359 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1360 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1361 + "dev": true,
1362 + "license": "MIT",
1363 + "dependencies": {
1364 + "ms": "^2.1.3"
1365 + },
1366 + "engines": {
1367 + "node": ">=6.0"
1368 + },
1369 + "peerDependenciesMeta": {
1370 + "supports-color": {
1371 + "optional": true
1372 + }
1373 + }
1374 + },
1375 + "node_modules/electron-to-chromium": {
1376 + "version": "1.5.403",
1377 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz",
1378 + "integrity": "sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA==",
1379 + "dev": true,
1380 + "license": "ISC"
1381 + },
1382 + "node_modules/esbuild": {
1383 + "version": "0.25.12",
1384 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1385 + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1386 + "dev": true,
1387 + "hasInstallScript": true,
1388 + "license": "MIT",
1389 + "bin": {
1390 + "esbuild": "bin/esbuild"
1391 + },
1392 + "engines": {
1393 + "node": ">=18"
1394 + },
1395 + "optionalDependencies": {
1396 + "@esbuild/aix-ppc64": "0.25.12",
1397 + "@esbuild/android-arm": "0.25.12",
1398 + "@esbuild/android-arm64": "0.25.12",
1399 + "@esbuild/android-x64": "0.25.12",
1400 + "@esbuild/darwin-arm64": "0.25.12",
1401 + "@esbuild/darwin-x64": "0.25.12",
1402 + "@esbuild/freebsd-arm64": "0.25.12",
1403 + "@esbuild/freebsd-x64": "0.25.12",
1404 + "@esbuild/linux-arm": "0.25.12",
1405 + "@esbuild/linux-arm64": "0.25.12",
1406 + "@esbuild/linux-ia32": "0.25.12",
1407 + "@esbuild/linux-loong64": "0.25.12",
1408 + "@esbuild/linux-mips64el": "0.25.12",
1409 + "@esbuild/linux-ppc64": "0.25.12",
1410 + "@esbuild/linux-riscv64": "0.25.12",
1411 + "@esbuild/linux-s390x": "0.25.12",
1412 + "@esbuild/linux-x64": "0.25.12",
1413 + "@esbuild/netbsd-arm64": "0.25.12",
1414 + "@esbuild/netbsd-x64": "0.25.12",
1415 + "@esbuild/openbsd-arm64": "0.25.12",
1416 + "@esbuild/openbsd-x64": "0.25.12",
1417 + "@esbuild/openharmony-arm64": "0.25.12",
1418 + "@esbuild/sunos-x64": "0.25.12",
1419 + "@esbuild/win32-arm64": "0.25.12",
1420 + "@esbuild/win32-ia32": "0.25.12",
1421 + "@esbuild/win32-x64": "0.25.12"
1422 + }
1423 + },
1424 + "node_modules/escalade": {
1425 + "version": "3.2.0",
1426 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1427 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1428 + "dev": true,
1429 + "license": "MIT",
1430 + "engines": {
1431 + "node": ">=6"
1432 + }
1433 + },
1434 + "node_modules/fdir": {
1435 + "version": "6.5.0",
1436 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1437 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1438 + "dev": true,
1439 + "license": "MIT",
1440 + "engines": {
1441 + "node": ">=12.0.0"
1442 + },
1443 + "peerDependencies": {
1444 + "picomatch": "^3 || ^4"
1445 + },
1446 + "peerDependenciesMeta": {
1447 + "picomatch": {
1448 + "optional": true
1449 + }
1450 + }
1451 + },
1452 + "node_modules/fsevents": {
1453 + "version": "2.3.3",
1454 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1455 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1456 + "dev": true,
1457 + "hasInstallScript": true,
1458 + "license": "MIT",
1459 + "optional": true,
1460 + "os": [
1461 + "darwin"
1462 + ],
1463 + "engines": {
1464 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1465 + }
1466 + },
1467 + "node_modules/gensync": {
1468 + "version": "1.0.0-beta.2",
1469 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1470 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1471 + "dev": true,
1472 + "license": "MIT",
1473 + "engines": {
1474 + "node": ">=6.9.0"
1475 + }
1476 + },
1477 + "node_modules/js-tokens": {
1478 + "version": "4.0.0",
1479 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1480 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1481 + "license": "MIT"
1482 + },
1483 + "node_modules/jsesc": {
1484 + "version": "3.1.0",
1485 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1486 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1487 + "dev": true,
1488 + "license": "MIT",
1489 + "bin": {
1490 + "jsesc": "bin/jsesc"
1491 + },
1492 + "engines": {
1493 + "node": ">=6"
1494 + }
1495 + },
1496 + "node_modules/json5": {
1497 + "version": "2.2.3",
1498 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1499 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1500 + "dev": true,
1501 + "license": "MIT",
1502 + "bin": {
1503 + "json5": "lib/cli.js"
1504 + },
1505 + "engines": {
1506 + "node": ">=6"
1507 + }
1508 + },
1509 + "node_modules/loose-envify": {
1510 + "version": "1.4.0",
1511 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1512 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1513 + "license": "MIT",
1514 + "dependencies": {
1515 + "js-tokens": "^3.0.0 || ^4.0.0"
1516 + },
1517 + "bin": {
1518 + "loose-envify": "cli.js"
1519 + }
1520 + },
1521 + "node_modules/lru-cache": {
1522 + "version": "5.1.1",
1523 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1524 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1525 + "dev": true,
1526 + "license": "ISC",
1527 + "dependencies": {
1528 + "yallist": "^3.0.2"
1529 + }
1530 + },
1531 + "node_modules/ms": {
1532 + "version": "2.1.3",
1533 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1534 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1535 + "dev": true,
1536 + "license": "MIT"
1537 + },
1538 + "node_modules/nanoid": {
1539 + "version": "3.3.18",
1540 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
1541 + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
1542 + "dev": true,
1543 + "funding": [
1544 + {
1545 + "type": "github",
1546 + "url": "https://github.com/sponsors/ai"
1547 + }
1548 + ],
1549 + "license": "MIT",
1550 + "bin": {
1551 + "nanoid": "bin/nanoid.cjs"
1552 + },
1553 + "engines": {
1554 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1555 + }
1556 + },
1557 + "node_modules/node-releases": {
1558 + "version": "2.0.53",
1559 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
1560 + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
1561 + "dev": true,
1562 + "license": "MIT",
1563 + "engines": {
1564 + "node": ">=18"
1565 + }
1566 + },
1567 + "node_modules/picocolors": {
1568 + "version": "1.1.1",
1569 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1570 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1571 + "dev": true,
1572 + "license": "ISC"
1573 + },
1574 + "node_modules/picomatch": {
1575 + "version": "4.0.5",
1576 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
1577 + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
1578 + "dev": true,
1579 + "license": "MIT",
1580 + "engines": {
1581 + "node": ">=12"
1582 + },
1583 + "funding": {
1584 + "url": "https://github.com/sponsors/jonschlinkert"
1585 + }
1586 + },
1587 + "node_modules/postcss": {
1588 + "version": "8.5.26",
1589 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
1590 + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
1591 + "dev": true,
1592 + "funding": [
1593 + {
1594 + "type": "opencollective",
1595 + "url": "https://opencollective.com/postcss/"
1596 + },
1597 + {
1598 + "type": "tidelift",
1599 + "url": "https://tidelift.com/funding/github/npm/postcss"
1600 + },
1601 + {
1602 + "type": "github",
1603 + "url": "https://github.com/sponsors/ai"
1604 + }
1605 + ],
1606 + "license": "MIT",
1607 + "dependencies": {
1608 + "nanoid": "^3.3.17",
1609 + "picocolors": "^1.1.1",
1610 + "source-map-js": "^1.2.1"
1611 + },
1612 + "engines": {
1613 + "node": "^10 || ^12 || >=14"
1614 + }
1615 + },
1616 + "node_modules/react": {
1617 + "version": "18.3.1",
1618 + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1619 + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1620 + "license": "MIT",
1621 + "dependencies": {
1622 + "loose-envify": "^1.1.0"
1623 + },
1624 + "engines": {
1625 + "node": ">=0.10.0"
1626 + }
1627 + },
1628 + "node_modules/react-dom": {
1629 + "version": "18.3.1",
1630 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1631 + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1632 + "license": "MIT",
1633 + "dependencies": {
1634 + "loose-envify": "^1.1.0",
1635 + "scheduler": "^0.23.2"
1636 + },
1637 + "peerDependencies": {
1638 + "react": "^18.3.1"
1639 + }
1640 + },
1641 + "node_modules/react-refresh": {
1642 + "version": "0.17.0",
1643 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1644 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1645 + "dev": true,
1646 + "license": "MIT",
1647 + "engines": {
1648 + "node": ">=0.10.0"
1649 + }
1650 + },
1651 + "node_modules/rollup": {
1652 + "version": "4.62.4",
1653 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
1654 + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
1655 + "dev": true,
1656 + "license": "MIT",
1657 + "dependencies": {
1658 + "@types/estree": "1.0.9"
1659 + },
1660 + "bin": {
1661 + "rollup": "dist/bin/rollup"
1662 + },
1663 + "engines": {
1664 + "node": ">=18.0.0",
1665 + "npm": ">=8.0.0"
1666 + },
1667 + "optionalDependencies": {
1668 + "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
1669 + "@rollup/rollup-android-arm-eabi": "4.62.4",
1670 + "@rollup/rollup-android-arm64": "4.62.4",
1671 + "@rollup/rollup-darwin-arm64": "4.62.4",
1672 + "@rollup/rollup-darwin-x64": "4.62.4",
1673 + "@rollup/rollup-freebsd-arm64": "4.62.4",
1674 + "@rollup/rollup-freebsd-x64": "4.62.4",
1675 + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
1676 + "@rollup/rollup-linux-arm-musleabihf": "4.62.4",
1677 + "@rollup/rollup-linux-arm64-gnu": "4.62.4",
1678 + "@rollup/rollup-linux-arm64-musl": "4.62.4",
1679 + "@rollup/rollup-linux-loong64-gnu": "4.62.4",
1680 + "@rollup/rollup-linux-loong64-musl": "4.62.4",
1681 + "@rollup/rollup-linux-ppc64-gnu": "4.62.4",
1682 + "@rollup/rollup-linux-ppc64-musl": "4.62.4",
1683 + "@rollup/rollup-linux-riscv64-gnu": "4.62.4",
1684 + "@rollup/rollup-linux-riscv64-musl": "4.62.4",
1685 + "@rollup/rollup-linux-s390x-gnu": "4.62.4",
1686 + "@rollup/rollup-linux-x64-gnu": "4.62.4",
1687 + "@rollup/rollup-linux-x64-musl": "4.62.4",
1688 + "@rollup/rollup-openbsd-x64": "4.62.4",
1689 + "@rollup/rollup-openharmony-arm64": "4.62.4",
1690 + "@rollup/rollup-win32-arm64-msvc": "4.62.4",
1691 + "@rollup/rollup-win32-ia32-msvc": "4.62.4",
1692 + "@rollup/rollup-win32-x64-gnu": "4.62.4",
1693 + "@rollup/rollup-win32-x64-msvc": "4.62.4",
1694 + "fsevents": "~2.3.2"
1695 + }
1696 + },
1697 + "node_modules/scheduler": {
1698 + "version": "0.23.2",
1699 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1700 + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1701 + "license": "MIT",
1702 + "dependencies": {
1703 + "loose-envify": "^1.1.0"
1704 + }
1705 + },
1706 + "node_modules/semver": {
1707 + "version": "6.3.1",
1708 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1709 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1710 + "dev": true,
1711 + "license": "ISC",
1712 + "bin": {
1713 + "semver": "bin/semver.js"
1714 + }
1715 + },
1716 + "node_modules/source-map-js": {
1717 + "version": "1.2.1",
1718 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1719 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1720 + "dev": true,
1721 + "license": "BSD-3-Clause",
1722 + "engines": {
1723 + "node": ">=0.10.0"
1724 + }
1725 + },
1726 + "node_modules/tinyglobby": {
1727 + "version": "0.2.17",
1728 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
1729 + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
1730 + "dev": true,
1731 + "license": "MIT",
1732 + "dependencies": {
1733 + "fdir": "^6.5.0",
1734 + "picomatch": "^4.0.4"
1735 + },
1736 + "engines": {
1737 + "node": ">=12.0.0"
1738 + },
1739 + "funding": {
1740 + "url": "https://github.com/sponsors/SuperchupuDev"
1741 + }
1742 + },
1743 + "node_modules/update-browserslist-db": {
1744 + "version": "1.3.0",
1745 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz",
1746 + "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==",
1747 + "dev": true,
1748 + "funding": [
1749 + {
1750 + "type": "opencollective",
1751 + "url": "https://opencollective.com/browserslist"
1752 + },
1753 + {
1754 + "type": "tidelift",
1755 + "url": "https://tidelift.com/funding/github/npm/browserslist"
1756 + },
1757 + {
1758 + "type": "github",
1759 + "url": "https://github.com/sponsors/ai"
1760 + }
1761 + ],
1762 + "license": "MIT",
1763 + "dependencies": {
1764 + "escalade": "^3.2.0",
1765 + "picocolors": "^1.1.1"
1766 + },
1767 + "bin": {
1768 + "update-browserslist-db": "cli.js"
1769 + },
1770 + "peerDependencies": {
1771 + "browserslist": ">= 4.21.0"
1772 + }
1773 + },
1774 + "node_modules/vite": {
1775 + "version": "6.4.3",
1776 + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
1777 + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
1778 + "dev": true,
1779 + "license": "MIT",
1780 + "dependencies": {
1781 + "esbuild": "^0.25.0",
1782 + "fdir": "^6.4.4",
1783 + "picomatch": "^4.0.2",
1784 + "postcss": "^8.5.3",
1785 + "rollup": "^4.34.9",
1786 + "tinyglobby": "^0.2.13"
1787 + },
1788 + "bin": {
1789 + "vite": "bin/vite.js"
1790 + },
1791 + "engines": {
1792 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1793 + },
1794 + "funding": {
1795 + "url": "https://github.com/vitejs/vite?sponsor=1"
1796 + },
1797 + "optionalDependencies": {
1798 + "fsevents": "~2.3.3"
1799 + },
1800 + "peerDependencies": {
1801 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1802 + "jiti": ">=1.21.0",
1803 + "less": "*",
1804 + "lightningcss": "^1.21.0",
1805 + "sass": "*",
1806 + "sass-embedded": "*",
1807 + "stylus": "*",
1808 + "sugarss": "*",
1809 + "terser": "^5.16.0",
1810 + "tsx": "^4.8.1",
1811 + "yaml": "^2.4.2"
1812 + },
1813 + "peerDependenciesMeta": {
1814 + "@types/node": {
1815 + "optional": true
1816 + },
1817 + "jiti": {
1818 + "optional": true
1819 + },
1820 + "less": {
1821 + "optional": true
1822 + },
1823 + "lightningcss": {
1824 + "optional": true
1825 + },
1826 + "sass": {
1827 + "optional": true
1828 + },
1829 + "sass-embedded": {
1830 + "optional": true
1831 + },
1832 + "stylus": {
1833 + "optional": true
1834 + },
1835 + "sugarss": {
1836 + "optional": true
1837 + },
1838 + "terser": {
1839 + "optional": true
1840 + },
1841 + "tsx": {
1842 + "optional": true
1843 + },
1844 + "yaml": {
1845 + "optional": true
1846 + }
1847 + }
1848 + },
1849 + "node_modules/yallist": {
1850 + "version": "3.1.1",
1851 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1852 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1853 + "dev": true,
1854 + "license": "ISC"
1855 + }
1856 + }
1857 +}
added hfmarketdata/web/package.json +21 −0
@@ -0,0 +1,21 @@
1 +{
2 + "name": "hfmarketdata-web",
3 + "private": true,
4 + "version": "1.0.0",
5 + "description": "HF Market Data — open high-frequency market data API platform",
6 + "author": "Simon-Pierre Boucher <contact@spboucher.ai>",
7 + "type": "module",
8 + "scripts": {
9 + "dev": "vite",
10 + "build": "vite build",
11 + "preview": "vite preview"
12 + },
13 + "dependencies": {
14 + "react": "^18.3.1",
15 + "react-dom": "^18.3.1"
16 + },
17 + "devDependencies": {
18 + "@vitejs/plugin-react": "^4.3.4",
19 + "vite": "^6.0.0"
20 + }
21 +}
added hfmarketdata/web/src/App.jsx +403 −0
@@ -0,0 +1,403 @@
1 +import React, { useEffect, useMemo, useState } from 'react'
2 +import {
3 + ASSET_TYPES, BAR_FIELDS, BASE_URL, ENDPOINTS, ERRORS, OPTION_FIELDS,
4 + TIMEFRAMES, curlSnippet, jsSnippet, pythonSnippet,
5 +} from './spec.js'
6 +
7 +// ---------------------------------------------------------------- atoms
8 +
9 +function Kicker({ children }) {
10 + return <div className="kicker">{children}</div>
11 +}
12 +
13 +function MethodChip({ method }) {
14 + return <span className="method">{method}</span>
15 +}
16 +
17 +function CodeTabs({ ep }) {
18 + const tabs = {
19 + curl: curlSnippet(ep),
20 + Python: pythonSnippet(ep),
21 + JavaScript: jsSnippet(ep),
22 + }
23 + const [active, setActive] = useState('curl')
24 + const [copied, setCopied] = useState(false)
25 + const copy = () => {
26 + navigator.clipboard.writeText(tabs[active]).then(() => {
27 + setCopied(true)
28 + setTimeout(() => setCopied(false), 1200)
29 + })
30 + }
31 + return (
32 + <div className="code-tabs">
33 + <div className="code-tabs-bar">
34 + <span className="code-tabs-label">Request</span>
35 + {Object.keys(tabs).map(t => (
36 + <button key={t} className={t === active ? 'tab active' : 'tab'}
37 + onClick={() => setActive(t)}>{t}</button>
38 + ))}
39 + <button className="tab copy" onClick={copy}>{copied ? 'Copied ✓' : 'Copy'}</button>
40 + </div>
41 + <pre className="ink"><code>{tabs[active]}</code></pre>
42 + </div>
43 + )
44 +}
45 +
46 +function ResponseBlock({ ep }) {
47 + const [open, setOpen] = useState(false)
48 + const [live, setLive] = useState(null)
49 + const [busy, setBusy] = useState(false)
50 + const runLive = async () => {
51 + setBusy(true)
52 + try {
53 + const r = await fetch(ep.example)
54 + const text = await r.text()
55 + let pretty = text
56 + try { pretty = JSON.stringify(JSON.parse(text), null, 2) } catch { /* csv */ }
57 + setLive(pretty.length > 5000 ? pretty.slice(0, 5000) + '\n… (truncated)' : pretty)
58 + setOpen(true)
59 + } catch (e) {
60 + setLive(`Request failed: ${e.message}`)
61 + setOpen(true)
62 + } finally {
63 + setBusy(false)
64 + }
65 + }
66 + return (
67 + <div className="response-block">
68 + <div className="code-tabs-bar">
69 + <span className="code-tabs-label">Response</span>
70 + <button className={!open ? 'tab active' : 'tab'} onClick={() => setOpen(false)}>Example</button>
71 + {live && <button className={open ? 'tab active' : 'tab'} onClick={() => setOpen(true)}>Live</button>}
72 + <button className="tab live" onClick={runLive} disabled={busy}>
73 + {busy ? 'Running…' : '▶ Run live'}
74 + </button>
75 + </div>
76 + <pre className="ink response"><code>{open && live ? live : ep.response}</code></pre>
77 + </div>
78 + )
79 +}
80 +
81 +function FieldsTable({ kind }) {
82 + const fields = kind === 'options' ? OPTION_FIELDS : BAR_FIELDS
83 + const [open, setOpen] = useState(false)
84 + return (
85 + <div className="fields">
86 + <button className="fields-toggle" onClick={() => setOpen(!open)}>
87 + {open ? '−' : '+'} Response fields <span className="fields-n">{fields.length}</span>
88 + </button>
89 + {open && (
90 + <table className="params">
91 + <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
92 + <tbody>
93 + {fields.map(f => (
94 + <tr key={f.name}>
95 + <td><code>{f.name}</code></td>
96 + <td className="type">{f.type}</td>
97 + <td>{f.desc}</td>
98 + </tr>
99 + ))}
100 + </tbody>
101 + </table>
102 + )}
103 + </div>
104 + )
105 +}
106 +
107 +function Endpoint({ ep, index }) {
108 + return (
109 + <article className="endpoint" id={ep.id}>
110 + <div className="endpoint-index">{String(index + 1).padStart(2, '0')}</div>
111 + <div className="endpoint-body">
112 + <div className="endpoint-head">
113 + <MethodChip method={ep.method} />
114 + <code className="path">{ep.path}</code>
115 + </div>
116 + <h3>{ep.title}</h3>
117 + <p className="endpoint-desc">{ep.desc}</p>
118 + {ep.params.length > 0 && (
119 + <table className="params">
120 + <thead>
121 + <tr><th>Parameter</th><th>In</th><th></th><th>Description</th></tr>
122 + </thead>
123 + <tbody>
124 + {ep.params.map(p => (
125 + <tr key={p.name}>
126 + <td><code>{p.name}</code></td>
127 + <td className="type">{p.in}</td>
128 + <td>{p.req ? <span className="req">required</span> : <span className="opt">optional</span>}</td>
129 + <td>{p.desc}</td>
130 + </tr>
131 + ))}
132 + </tbody>
133 + </table>
134 + )}
135 + <CodeTabs ep={ep} />
136 + <ResponseBlock ep={ep} />
137 + {ep.fields && <FieldsTable kind={ep.fields} />}
138 + </div>
139 + </article>
140 + )
141 +}
142 +
143 +// ---------------------------------------------------------------- live status
144 +
145 +function LiveStatus() {
146 + const [status, setStatus] = useState(null)
147 + const [err, setErr] = useState(null)
148 + useEffect(() => {
149 + fetch('/v1/status')
150 + .then(r => r.json())
151 + .then(d => setStatus(d.datasets))
152 + .catch(e => setErr(e.message))
153 + }, [])
154 + if (err) return <p className="muted">Live status unavailable ({err}).</p>
155 + if (!status) return <p className="muted">Loading live dataset status…</p>
156 + const rows = []
157 + for (const [asset, tfs] of Object.entries(status)) {
158 + if (asset === 'options') continue
159 + for (const [tf, adjs] of Object.entries(tfs)) {
160 + for (const [adj, n] of Object.entries(adjs)) {
161 + rows.push({ asset, tf, adj, n })
162 + }
163 + }
164 + }
165 + const total = rows.reduce((s, r) => s + r.n, 0)
166 + return (
167 + <div>
168 + <div className="stat-chips">
169 + <div className="chip"><b>{total.toLocaleString('en-US')}</b><span>instrument files live</span></div>
170 + {status.options && (
171 + <div className="chip"><b>{status.options.quarters.length}</b><span>options quarters ({status.options.quarters[0]} → {status.options.quarters.at(-1)})</span></div>
172 + )}
173 + </div>
174 + <div className="table-scroll">
175 + <table className="params">
176 + <thead>
177 + <tr><th>Asset</th><th>Timeframe</th><th>Adjustment</th><th>Instruments</th></tr>
178 + </thead>
179 + <tbody>
180 + {rows.map((r, i) => (
181 + <tr key={i}>
182 + <td>{r.asset}</td><td>{r.tf}</td><td><code>{r.adj}</code></td>
183 + <td className="num">{r.n.toLocaleString('en-US')}</td>
184 + </tr>
185 + ))}
186 + </tbody>
187 + </table>
188 + </div>
189 + <p className="muted small">Ingestion is continuous — this inventory refreshes as new segments land in the lake.</p>
190 + </div>
191 + )
192 +}
193 +
194 +// ---------------------------------------------------------------- app
195 +
196 +export default function App() {
197 + const grouped = useMemo(() => {
198 + const g = {}
199 + for (const e of ENDPOINTS) (g[e.tag] ||= []).push(e)
200 + return g
201 + }, [])
202 +
203 + return (
204 + <>
205 + <div className="layout">
206 + <aside className="sidebar">
207 + <a className="brand" href="#top">
208 + <span className="logo">HF</span>
209 + <span className="brand-text">
210 + <span className="brand-name">HF Market Data</span>
211 + <span className="brand-sub">Open API · v1</span>
212 + </span>
213 + </a>
214 + <nav>
215 + <Kicker>Getting started</Kicker>
216 + <a href="#overview">Overview</a>
217 + <a href="#quickstart">Quick start</a>
218 + <a href="#conventions">Conventions</a>
219 + <a href="#coverage">Data coverage</a>
220 + <Kicker>Endpoints</Kicker>
221 + {ENDPOINTS.map(e => (
222 + <a key={e.id} href={`#${e.id}`} className="ep-link">
223 + <span className="ep-method">{e.method}</span>{e.title}
224 + </a>
225 + ))}
226 + <Kicker>Resources</Kicker>
227 + <a href="#errors">Errors</a>
228 + <a href="#live">Live status</a>
229 + <a href="#about">About</a>
230 + <a href="/docs" target="_blank" rel="noreferrer">OpenAPI / Swagger ↗</a>
231 + </nav>
232 + </aside>
233 +
234 + <main id="top">
235 + <section className="hero" id="overview">
236 + <Kicker>Open high-frequency market data</Kicker>
237 + <h1>
238 + Market data,<br />
239 + <em className="accent-word">wide open.</em>
240 + </h1>
241 + <p className="lede">
242 + A free, keyless REST API over a deep historical archive:
243 + <strong> 1-minute to daily bars</strong> for stocks, ETFs, futures,
244 + crypto, indices and FX — and <strong>complete end-of-day options
245 + chains</strong> with quotes, implied volatility and Greeks back to 2010.
246 + </p>
247 + <div className="hero-actions">
248 + <a className="btn" href="#quickstart">Get started</a>
249 + <a className="btn ghost" href="/docs" target="_blank" rel="noreferrer">OpenAPI spec ↗</a>
250 + </div>
251 + <div className="stat-chips hero-chips">
252 + <div className="chip"><b>7</b><span>asset classes</span></div>
253 + <div className="chip"><b>5</b><span>timeframes — 1min → 1day</span></div>
254 + <div className="chip"><b>5,800+</b><span>options underlyings</span></div>
255 + <div className="chip"><b>2010</b><span>options history since</span></div>
256 + </div>
257 + </section>
258 +
259 + <section id="quickstart">
260 + <Kicker>01 — Getting started</Kicker>
261 + <h2>Quick start</h2>
262 + <p>
263 + No key, no registration, no SDK required. Every endpoint is a plain
264 + GET on <code>{BASE_URL}</code> returning JSON — or CSV when you ask for it.
265 + </p>
266 + <pre className="ink"><code>{`# Daily AAPL bars, split+dividend adjusted
267 +curl "${BASE_URL}/v1/bars/stock/AAPL?timeframe=1day&start=2024-01-01"
268 +
269 +# A precise 4-hour window of 1-minute bars, across three tickers at once
270 +curl "${BASE_URL}/v1/bars/stock?tickers=AAPL,MSFT,NVDA&timeframe=1min\\
271 + &start=2024-06-03%2009:30:00&end=2024-06-03%2013:30:00"
272 +
273 +# The whole watchlist at one exact moment
274 +curl "${BASE_URL}/v1/snapshot/stock?tickers=AAPL,MSFT,NVDA&at=2024-06-03%2010:35:00"
275 +
276 +# Full AAPL option chain — quotes, IV, Greeks
277 +curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
278 + <div className="callout">
279 + <b>Response envelope.</b>&nbsp; Every JSON data endpoint returns
280 + <code>{'{ "count": n, "data": [ … ] }'}</code>. CSV responses stream a
281 + header row followed by the records — ideal for <code>pandas.read_csv</code> straight from the URL.
282 + </div>
283 + </section>
284 +
285 + <section id="conventions">
286 + <Kicker>02 — Getting started</Kicker>
287 + <h2>Conventions</h2>
288 + <div className="conv-grid">
289 + <div className="conv">
290 + <h4>Timeframes</h4>
291 + <p>{TIMEFRAMES.join(' · ')}. Intraday timestamps are <b>US Eastern Time</b>; daily bars carry the session date. Zero-volume bars are excluded.</p>
292 + </div>
293 + <div className="conv">
294 + <h4>Adjustments</h4>
295 + <p>Stocks & ETFs ship in three variants — <code>adj_split</code>, <code>adj_splitdiv</code> (default), <code>UNADJUSTED</code> (1min & 1day only). Futures continuous series come unadjusted, ratio-adjusted or absolute-adjusted for roll dates.</p>
296 + </div>
297 + <div className="conv">
298 + <h4>Formats</h4>
299 + <p><code>format=json</code> (default, capped at 50k rows) or <code>format=csv</code> (capped at 2M rows) on every data endpoint. Filter with <code>start</code>/<code>end</code>, page with <code>limit</code> + <code>order</code>.</p>
300 + </div>
301 + <div className="conv">
302 + <h4>Options snapshots</h4>
303 + <p>End-of-day chains sampled <b>30 seconds before the close</b> to avoid rebalancing-order noise — with bid/ask IV and the full Greek set on every row.</p>
304 + </div>
305 + </div>
306 + </section>
307 +
308 + <section id="coverage">
309 + <Kicker>03 — Getting started</Kicker>
310 + <h2>Data coverage</h2>
311 + <div className="cards">
312 + {ASSET_TYPES.map(a => (
313 + <div className="card" key={a.id}>
314 + <div className="card-head">
315 + <h3>{a.name}</h3>
316 + <code className="card-ex">{a.example}</code>
317 + </div>
318 + <p>{a.desc}</p>
319 + <p className="muted small">{a.adjustments.map(x => <code key={x}>{x}</code>)}</p>
320 + </div>
321 + ))}
322 + <div className="card accent">
323 + <div className="card-head">
324 + <h3>Options</h3>
325 + <code className="card-ex">AAPL 200c</code>
326 + </div>
327 + <p>End-of-day chains for 5,800+ US equities & indices since 2010 —
328 + last price, bid/ask, bid/ask IV, open interest, volume,
329 + delta, gamma, vega, theta, rho.</p>
330 + </div>
331 + </div>
332 + </section>
333 +
334 + <section id="reference">
335 + {Object.entries(grouped).map(([tag, eps]) => (
336 + <div key={tag} className="ep-group">
337 + <Kicker>API reference</Kicker>
338 + <h2>{tag}</h2>
339 + {eps.map(e => (
340 + <Endpoint key={e.id} ep={e}
341 + index={ENDPOINTS.findIndex(x => x.id === e.id)} />
342 + ))}
343 + </div>
344 + ))}
345 + </section>
346 +
347 + <section id="errors">
348 + <Kicker>Resources</Kicker>
349 + <h2>Errors</h2>
350 + <p>
351 + Errors are plain HTTP status codes with a JSON body:&nbsp;
352 + <code>{'{ "detail": "…" }'}</code>. The detail message always names the
353 + invalid parameter and lists the valid values.
354 + </p>
355 + <table className="params">
356 + <thead><tr><th>Code</th><th>Meaning</th><th>When</th></tr></thead>
357 + <tbody>
358 + {ERRORS.map(e => (
359 + <tr key={e.code}>
360 + <td><code>{e.code}</code></td>
361 + <td>{e.meaning}</td>
362 + <td>{e.desc}</td>
363 + </tr>
364 + ))}
365 + </tbody>
366 + </table>
367 + </section>
368 +
369 + <section id="live">
370 + <Kicker>Resources</Kicker>
371 + <h2>Live dataset status</h2>
372 + <LiveStatus />
373 + </section>
374 +
375 + <section id="about" className="about">
376 + <Kicker>Resources</Kicker>
377 + <h2>About</h2>
378 + <p>
379 + HF Market Data is built and maintained by <strong>Simon-Pierre
380 + Boucher</strong>. Under the hood it is a DuckDB-over-Parquet data
381 + lake: every instrument is a zstd-compressed Parquet file queried
382 + in place with predicate pushdown — no database server between you
383 + and the data, which is what keeps responses fast across billions
384 + of rows.
385 + </p>
386 + <p>
387 + Contact — <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>
388 + </p>
389 + </section>
390 +
391 + <footer>
392 + <div className="foot-brand">HF MARKET DATA</div>
393 + <div>
394 + © {new Date().getFullYear()} Simon-Pierre Boucher ·{' '}
395 + <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a> ·{' '}
396 + <a href="/docs" target="_blank" rel="noreferrer">OpenAPI</a>
397 + </div>
398 + </footer>
399 + </main>
400 + </div>
401 + </>
402 + )
403 +}
added hfmarketdata/web/src/main.jsx +10 −0
@@ -0,0 +1,10 @@
1 +import React from 'react'
2 +import ReactDOM from 'react-dom/client'
3 +import App from './App.jsx'
4 +import './styles.css'
5 +
6 +ReactDOM.createRoot(document.getElementById('root')).render(
7 + <React.StrictMode>
8 + <App />
9 + </React.StrictMode>,
10 +)
added hfmarketdata/web/src/spec.js +329 −0
@@ -0,0 +1,329 @@
1 +// HF Market Data — API reference specification (rendered by the docs UI)
2 +// Author: Simon-Pierre Boucher <contact@spboucher.ai>
3 +
4 +export const BASE_URL = 'https://www.hfmarketdata.io'
5 +
6 +export const ASSET_TYPES = [
7 + { id: 'stock', name: 'Stocks', desc: 'US equities — every listed ticker, split & dividend adjusted variants.', adjustments: ['adj_split', 'adj_splitdiv', 'UNADJUSTED'], example: 'AAPL' },
8 + { id: 'etf', name: 'ETFs', desc: 'US exchange-traded funds, same adjustment variants as stocks.', adjustments: ['adj_split', 'adj_splitdiv', 'UNADJUSTED'], example: 'SPY' },
9 + { id: 'futures', name: 'Futures', desc: 'Continuous front-month series, roll-adjusted variants.', adjustments: ['contin_UNadj', 'contin_adj_ratio', 'contin_adj_absolute'], example: 'ES' },
10 + { id: 'futures_contracts', name: 'Futures contracts', desc: 'Individual expiry contracts, archive and current year.', adjustments: ['archive', 'update'], example: 'ESH24' },
11 + { id: 'crypto', name: 'Crypto', desc: 'Major cryptocurrency pairs.', adjustments: ['none'], example: 'BTCUSD' },
12 + { id: 'index', name: 'Indices', desc: 'Equity & volatility indices.', adjustments: ['none'], example: 'SPX' },
13 + { id: 'fx', name: 'FX', desc: 'Foreign-exchange pairs.', adjustments: ['none'], example: 'EURUSD' },
14 +]
15 +
16 +export const TIMEFRAMES = ['1min', '5min', '30min', '1hour', '1day']
17 +
18 +export const BAR_FIELDS = [
19 + { name: 'ticker', type: 'string', desc: 'Instrument symbol.' },
20 + { name: 'datetime', type: 'timestamp', desc: 'Bar timestamp — US Eastern Time for intraday, date for daily.' },
21 + { name: 'open', type: 'double', desc: 'Opening price of the bar.' },
22 + { name: 'high', type: 'double', desc: 'Highest traded price.' },
23 + { name: 'low', type: 'double', desc: 'Lowest traded price.' },
24 + { name: 'close', type: 'double', desc: 'Closing price of the bar.' },
25 + { name: 'volume', type: 'double', desc: 'Traded volume. Zero-volume bars are excluded from the dataset.' },
26 + { name: 'open_interest', type: 'double', desc: 'Open interest — daily futures bars only.' },
27 +]
28 +
29 +export const OPTION_FIELDS = [
30 + { name: 'ticker', type: 'string', desc: 'Underlying symbol.' },
31 + { name: 'trade_date', type: 'date', desc: 'Observation date (end-of-day snapshot, sampled 30s before the close).' },
32 + { name: 'strike', type: 'double', desc: 'Contract strike price.' },
33 + { name: 'expiry', type: 'date', desc: 'Contract expiration date.' },
34 + { name: 'call_put', type: 'string', desc: '"c" for calls, "p" for puts.' },
35 + { name: 'last_price', type: 'double', desc: 'Last traded price of the contract.' },
36 + { name: 'bid', type: 'double', desc: 'End-of-day best bid.' },
37 + { name: 'ask', type: 'double', desc: 'End-of-day best ask.' },
38 + { name: 'bid_iv', type: 'double', desc: 'Implied volatility computed from the bid.' },
39 + { name: 'ask_iv', type: 'double', desc: 'Implied volatility computed from the ask.' },
40 + { name: 'open_interest', type: 'double', desc: 'Open interest.' },
41 + { name: 'volume', type: 'double', desc: 'Contracts traded that day.' },
42 + { name: 'delta', type: 'double', desc: 'Δ — sensitivity of the option price to the underlying.' },
43 + { name: 'gamma', type: 'double', desc: 'Γ — rate of change of delta.' },
44 + { name: 'vega', type: 'double', desc: 'ν — sensitivity to implied volatility.' },
45 + { name: 'theta', type: 'double', desc: 'Θ — time decay.' },
46 + { name: 'rho', type: 'double', desc: 'ρ — sensitivity to interest rates.' },
47 +]
48 +
49 +export const ERRORS = [
50 + { code: 200, meaning: 'OK', desc: 'Request succeeded.' },
51 + { code: 400, meaning: 'Bad request', desc: 'Invalid parameter — unknown timeframe, adjustment not available for this asset type, malformed value. The detail field explains what to fix and lists valid values.' },
52 + { code: 404, meaning: 'Not found', desc: 'Unknown asset type, ticker or quarter. The detail field lists what is available.' },
53 + { code: 422, meaning: 'Validation error', desc: 'A query parameter failed type validation (FastAPI validation payload).' },
54 + { code: 503, meaning: 'Unavailable', desc: 'The requested dataset segment has not been ingested yet.' },
55 +]
56 +
57 +export const ENDPOINTS = [
58 + {
59 + id: 'health',
60 + method: 'GET',
61 + path: '/health',
62 + title: 'Health check',
63 + tag: 'Meta',
64 + desc: 'Service liveness probe. Returns service name, version and whether the data lake is mounted.',
65 + params: [],
66 + example: '/health',
67 + response: `{
68 + "status": "ok",
69 + "service": "hfmarketdata-api",
70 + "version": "1.0.0",
71 + "data_root_present": true
72 +}`,
73 + },
74 + {
75 + id: 'status',
76 + method: 'GET',
77 + path: '/v1/status',
78 + title: 'Dataset inventory',
79 + tag: 'Meta',
80 + desc: 'Full inventory of the data lake: every asset type, timeframe and adjustment with the number of instruments available, plus the list of options quarters. Coverage grows continuously as ingestion progresses.',
81 + params: [],
82 + example: '/v1/status',
83 + response: `{
84 + "datasets": {
85 + "stock": {
86 + "1day": { "adj_split": 7664, "adj_splitdiv": 7664, "UNADJUSTED": 7665 },
87 + "1min": { "adj_split": 7659, "adj_splitdiv": 7659, "UNADJUSTED": 7660 }
88 + },
89 + "fx": { "1day": { "none": 79 } },
90 + "options": {
91 + "quarters": ["2010_q1", "2010_q2", "…", "2026_q2"],
92 + "files_latest_quarter": 5972
93 + }
94 + }
95 +}`,
96 + },
97 + {
98 + id: 'tickers',
99 + method: 'GET',
100 + path: '/v1/{asset}/tickers',
101 + title: 'List tickers',
102 + tag: 'Bars',
103 + desc: 'Every instrument available for an asset type / timeframe / adjustment combination. Use it to discover coverage before requesting bars.',
104 + params: [
105 + { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
106 + { name: 'timeframe', in: 'query', req: false, desc: '1min · 5min · 30min · 1hour · 1day — default 1day' },
107 + { name: 'adjustment', in: 'query', req: false, desc: 'Adjustment variant; a sensible default is applied per asset type' },
108 + { name: 'search', in: 'query', req: false, desc: 'Case-insensitive substring filter' },
109 + { name: 'limit', in: 'query', req: false, desc: 'Max results — default 10 000' },
110 + ],
111 + example: '/v1/stock/tickers?timeframe=1day&search=AAP',
112 + response: `{
113 + "asset": "stock",
114 + "timeframe": "1day",
115 + "adjustment": "adj_splitdiv",
116 + "count": 4,
117 + "tickers": ["AAP", "AAPD", "AAPL", "AAPU"]
118 +}`,
119 + },
120 + {
121 + id: 'bars',
122 + method: 'GET',
123 + path: '/v1/bars/{asset}/{ticker}',
124 + title: 'OHLCV bars',
125 + tag: 'Bars',
126 + desc: 'Historical bars for one instrument, 1-minute to daily. Daily futures bars also carry open interest. Switch to format=csv for bulk extraction — up to 2 million rows per request.',
127 + params: [
128 + { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
129 + { name: 'ticker', in: 'path', req: true, desc: 'Instrument symbol — AAPL, SPY, ES, BTCUSD, EURUSD…' },
130 + { name: 'timeframe', in: 'query', req: false, desc: '1min · 5min · 30min · 1hour · 1day — default 1day' },
131 + { name: 'adjustment', in: 'query', req: false, desc: 'stock/etf: adj_split · adj_splitdiv · UNADJUSTED — futures: contin_UNadj · contin_adj_ratio · contin_adj_absolute' },
132 + { name: 'start', in: 'query', req: false, desc: 'ISO lower bound — 2024-01-01 or 2024-01-01 09:30:00' },
133 + { name: 'end', in: 'query', req: false, desc: 'ISO upper bound' },
134 + { name: 'order', in: 'query', req: false, desc: 'asc (default) · desc' },
135 + { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 5 000 · JSON cap 50 000 · CSV cap 2 000 000' },
136 + { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
137 + ],
138 + example: '/v1/bars/stock/AAPL?timeframe=1day&start=2024-06-03&limit=2',
139 + response: `{
140 + "count": 2,
141 + "data": [
142 + { "ticker": "AAPL", "datetime": "2024-06-03", "open": 192.9,
143 + "high": 194.99, "low": 192.52, "close": 194.03, "volume": 50080500.0 },
144 + { "ticker": "AAPL", "datetime": "2024-06-04", "open": 194.64,
145 + "high": 195.32, "low": 193.03, "close": 194.35, "volume": 47471400.0 }
146 + ]
147 +}`,
148 + fields: 'bars',
149 + },
150 + {
151 + id: 'bars-multi',
152 + method: 'GET',
153 + path: '/v1/bars/{asset}',
154 + title: 'Multi-ticker bars',
155 + tag: 'Bars',
156 + desc: 'Bars for several instruments in ONE call. Combine tickers with start/end to slice any precise window — e.g. 4 hours of 1-minute bars across a whole watchlist. The row limit applies per ticker, and rows come back grouped by ticker.',
157 + params: [
158 + { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
159 + { name: 'tickers', in: 'query', req: true, desc: 'Comma-separated list — AAPL,MSFT,TSLA (max 50)' },
160 + { name: 'timeframe', in: 'query', req: false, desc: '1min · 5min · 30min · 1hour · 1day — default 1day' },
161 + { name: 'adjustment', in: 'query', req: false, desc: 'Same variants as single-ticker bars' },
162 + { name: 'start', in: 'query', req: false, desc: 'ISO lower bound — precise to the minute' },
163 + { name: 'end', in: 'query', req: false, desc: 'ISO upper bound' },
164 + { name: 'order', in: 'query', req: false, desc: 'asc (default) · desc' },
165 + { name: 'limit', in: 'query', req: false, desc: 'Max rows PER TICKER — default 5 000' },
166 + { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
167 + ],
168 + example: '/v1/bars/stock?tickers=AAPL,MSFT,NVDA&timeframe=1min&start=2024-06-03%2009:30:00&end=2024-06-03%2013:30:00',
169 + response: `{
170 + "count": 720,
171 + "data": [
172 + { "ticker": "AAPL", "datetime": "2024-06-03 09:30:00", "open": 192.9, "…": "…" },
173 + { "ticker": "AAPL", "datetime": "2024-06-03 09:31:00", "open": 192.7, "…": "…" },
174 + { "ticker": "MSFT", "datetime": "2024-06-03 09:30:00", "open": 415.2, "…": "…" }
175 + ]
176 +}`,
177 + fields: 'bars',
178 + },
179 + {
180 + id: 'snapshot',
181 + method: 'GET',
182 + path: '/v1/snapshot/{asset}',
183 + title: 'Point-in-time snapshot',
184 + tag: 'Bars',
185 + desc: 'The state of a whole watchlist at one precise moment: for each requested instrument, the last bar at or before the given timestamp. Perfect for reconstructing a cross-section of the market at 10:35:00 on any given day.',
186 + params: [
187 + { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
188 + { name: 'tickers', in: 'query', req: true, desc: 'Comma-separated list — AAPL,MSFT,TSLA (max 50)' },
189 + { name: 'at', in: 'query', req: true, desc: 'The precise moment — 2024-06-03 10:35:00' },
190 + { name: 'timeframe', in: 'query', req: false, desc: '1min (default) · 5min · 30min · 1hour · 1day' },
191 + { name: 'adjustment', in: 'query', req: false, desc: 'Same variants as bars' },
192 + { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
193 + ],
194 + example: '/v1/snapshot/stock?tickers=AAPL,MSFT,NVDA&at=2024-06-03%2010:35:00',
195 + response: `{
196 + "count": 3,
197 + "data": [
198 + { "ticker": "AAPL", "datetime": "2024-06-03 10:35:00", "close": 193.1, "…": "…" },
199 + { "ticker": "MSFT", "datetime": "2024-06-03 10:35:00", "close": 414.8, "…": "…" },
200 + { "ticker": "NVDA", "datetime": "2024-06-03 10:34:00", "close": 113.9, "…": "…" }
201 + ]
202 +}`,
203 + fields: 'bars',
204 + },
205 + {
206 + id: 'opt-quarters',
207 + method: 'GET',
208 + path: '/v1/options/quarters',
209 + title: 'Options quarters',
210 + tag: 'Options',
211 + desc: 'The quarterly archives available, from 2010_q1 through the current quarter. Each quarter holds one end-of-day chain file per underlying.',
212 + params: [],
213 + example: '/v1/options/quarters',
214 + response: `{
215 + "quarters": ["2010_q1", "2010_q2", "2010_q3", "…", "2026_q1", "2026_q2"]
216 +}`,
217 + },
218 + {
219 + id: 'opt-tickers',
220 + method: 'GET',
221 + path: '/v1/options/tickers',
222 + title: 'Options underlyings',
223 + tag: 'Options',
224 + desc: 'Underlyings with options data in a given quarter — 5,800+ US equities and indices.',
225 + params: [
226 + { name: 'quarter', in: 'query', req: false, desc: 'e.g. 2024_q4 — default: latest quarter' },
227 + { name: 'search', in: 'query', req: false, desc: 'Substring filter' },
228 + { name: 'limit', in: 'query', req: false, desc: 'Max results — default 10 000' },
229 + ],
230 + example: '/v1/options/tickers?search=TSL',
231 + response: `{
232 + "quarter": "2026_q2",
233 + "count": 14,
234 + "tickers": ["TSL", "TSLA", "TSLG", "TSLL", "…"]
235 +}`,
236 + },
237 + {
238 + id: 'opt-chain',
239 + method: 'GET',
240 + path: '/v1/options/chain/{ticker}',
241 + title: 'Option chain',
242 + tag: 'Options',
243 + desc: 'The complete end-of-day chain for an underlying on one trade date: last price, bid/ask quotes, bid/ask implied volatility, open interest, volume and the full set of Greeks. Snapshots are sampled 30 seconds before the close to avoid rebalancing-order noise.',
244 + params: [
245 + { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol — AAPL, SPX…' },
246 + { name: 'trade_date', in: 'query', req: false, desc: 'yyyy-mm-dd — default: latest available date' },
247 + { name: 'expiry', in: 'query', req: false, desc: 'Restrict to a single expiry date' },
248 + { name: 'call_put', in: 'query', req: false, desc: 'c · p' },
249 + { name: 'strike_min', in: 'query', req: false, desc: 'Minimum strike' },
250 + { name: 'strike_max', in: 'query', req: false, desc: 'Maximum strike' },
251 + { name: 'min_volume', in: 'query', req: false, desc: 'Minimum traded volume' },
252 + { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 20 000' },
253 + { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
254 + ],
255 + example: '/v1/options/chain/AAPL?call_put=c&strike_min=200&strike_max=210&limit=1',
256 + response: `{
257 + "count": 1,
258 + "data": [
259 + { "ticker": "AAPL", "trade_date": "2026-08-07", "strike": 200.0,
260 + "expiry": "2026-08-21", "call_put": "c", "last_price": 113.25,
261 + "bid": 111.25, "ask": 115.0, "bid_iv": 0.412, "ask_iv": 0.487,
262 + "open_interest": 5.0, "volume": 12.0, "delta": 0.998,
263 + "gamma": 0.0001, "vega": 0.0012, "theta": -0.0034, "rho": 0.0274 }
264 + ]
265 +}`,
266 + fields: 'options',
267 + },
268 + {
269 + id: 'opt-expirations',
270 + method: 'GET',
271 + path: '/v1/options/expirations/{ticker}',
272 + title: 'Expiration dates',
273 + tag: 'Options',
274 + desc: 'Available expiry dates for an underlying, optionally as of one trade date — the natural first call before requesting a filtered chain.',
275 + params: [
276 + { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol' },
277 + { name: 'trade_date', in: 'query', req: false, desc: 'yyyy-mm-dd' },
278 + ],
279 + example: '/v1/options/expirations/AAPL',
280 + response: `{
281 + "ticker": "AAPL",
282 + "expirations": ["2026-08-21", "2026-08-28", "2026-09-18", "…"]
283 +}`,
284 + },
285 + {
286 + id: 'opt-history',
287 + method: 'GET',
288 + path: '/v1/options/history/{ticker}',
289 + title: 'Contract history',
290 + tag: 'Options',
291 + desc: 'The daily time series of one specific contract — strike + expiry + side — across its entire life: price, quotes, implied volatility, open interest and Greeks, day by day.',
292 + params: [
293 + { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol' },
294 + { name: 'strike', in: 'query', req: true, desc: 'Contract strike' },
295 + { name: 'expiry', in: 'query', req: true, desc: 'Contract expiry — yyyy-mm-dd' },
296 + { name: 'call_put', in: 'query', req: true, desc: 'c · p' },
297 + { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 5 000' },
298 + { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
299 + ],
300 + example: '/v1/options/history/AAPL?strike=200&expiry=2026-12-18&call_put=c',
301 + response: `{
302 + "count": 214,
303 + "data": [
304 + { "ticker": "AAPL", "trade_date": "2026-01-02", "strike": 200.0,
305 + "expiry": "2026-12-18", "call_put": "c", "last_price": 41.3, "…": "…" }
306 + ]
307 +}`,
308 + fields: 'options',
309 + },
310 +]
311 +
312 +export function curlSnippet(ep) {
313 + return `curl "${BASE_URL}${ep.example}"`
314 +}
315 +
316 +export function pythonSnippet(ep) {
317 + return `import requests
318 +
319 +r = requests.get("${BASE_URL}${ep.example}")
320 +r.raise_for_status()
321 +data = r.json()
322 +print(f"{data.get('count', len(data))} rows")`
323 +}
324 +
325 +export function jsSnippet(ep) {
326 + return `const res = await fetch("${BASE_URL}${ep.example}");
327 +const data = await res.json();
328 +console.log(data);`
329 +}
added hfmarketdata/web/src/styles.css +457 −0
@@ -0,0 +1,457 @@
1 +/* HF Market Data — refined editorial light theme
2 + Author: Simon-Pierre Boucher <contact@spboucher.ai> */
3 +
4 +:root {
5 + --bg: #fbfaf7;
6 + --bg-soft: #f4f2ec;
7 + --card: #ffffff;
8 + --line: #e7e3d8;
9 + --line-strong: #d8d3c4;
10 + --ink: #1c2430;
11 + --ink-soft: #4a5568;
12 + --muted: #8a8f98;
13 + --accent: #2e5ce6;
14 + --accent-deep: #1e3fa8;
15 + --accent-wash: #eef2fd;
16 + --mono: 'JetBrains Mono', ui-monospace, monospace;
17 + --sans: 'Inter', system-ui, sans-serif;
18 + --serif: 'Fraunces', Georgia, serif;
19 + --shadow: 0 1px 2px rgba(28, 36, 48, 0.05), 0 8px 24px -12px rgba(28, 36, 48, 0.12);
20 + --shadow-sm: 0 1px 2px rgba(28, 36, 48, 0.05), 0 4px 12px -6px rgba(28, 36, 48, 0.08);
21 +}
22 +
23 +* { box-sizing: border-box; }
24 +html { scroll-behavior: smooth; scroll-padding-top: 24px; }
25 +body {
26 + margin: 0;
27 + font-family: var(--sans);
28 + background: var(--bg);
29 + color: var(--ink);
30 + line-height: 1.7;
31 + font-size: 15.5px;
32 + -webkit-font-smoothing: antialiased;
33 +}
34 +
35 +a { color: var(--accent); text-decoration: none; }
36 +a:hover { text-decoration: underline; text-underline-offset: 3px; }
37 +
38 +code {
39 + font-family: var(--mono);
40 + font-size: 0.82em;
41 + background: var(--bg-soft);
42 + border: 1px solid var(--line);
43 + border-radius: 5px;
44 + padding: 1px 6px;
45 + margin: 0 2px;
46 + white-space: nowrap;
47 + color: var(--accent-deep);
48 +}
49 +
50 +pre {
51 + background: var(--bg-soft);
52 + border: 1px solid var(--line);
53 + border-radius: 10px;
54 + padding: 18px 20px;
55 + overflow-x: auto;
56 + margin: 0;
57 +}
58 +pre code {
59 + background: none; border: none; padding: 0; margin: 0;
60 + font-size: 0.8rem; white-space: pre;
61 + color: var(--ink-soft);
62 +}
63 +
64 +/* ---------------------------------------------------------------- layout */
65 +
66 +.layout { display: flex; min-height: 100vh; }
67 +
68 +.sidebar {
69 + width: 268px;
70 + flex-shrink: 0;
71 + border-right: 1px solid var(--line);
72 + padding: 30px 22px 40px;
73 + position: sticky;
74 + top: 0;
75 + height: 100vh;
76 + overflow-y: auto;
77 + background: var(--bg);
78 +}
79 +
80 +main {
81 + flex: 1;
82 + max-width: 880px;
83 + padding: 56px 56px 60px;
84 + margin: 0 auto;
85 + min-width: 0;
86 +}
87 +section { margin-bottom: 84px; }
88 +
89 +/* brand */
90 +.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 30px; color: var(--ink); }
91 +.brand:hover { text-decoration: none; }
92 +.logo {
93 + display: grid; place-items: center;
94 + width: 40px; height: 40px;
95 + background: var(--accent);
96 + color: white;
97 + border-radius: 10px;
98 + font-family: var(--serif);
99 + font-weight: 600;
100 + font-size: 0.95rem;
101 + box-shadow: var(--shadow-sm);
102 +}
103 +.brand-text { display: flex; flex-direction: column; line-height: 1.3; }
104 +.brand-name { font-family: var(--serif); font-weight: 600; font-size: 1.02rem; letter-spacing: 0.01em; }
105 +.brand-sub { font-size: 0.7rem; color: var(--muted); letter-spacing: 0.08em; text-transform: uppercase; }
106 +
107 +/* nav */
108 +.sidebar nav { display: flex; flex-direction: column; gap: 1px; }
109 +.kicker {
110 + font-size: 0.66rem;
111 + font-weight: 700;
112 + letter-spacing: 0.16em;
113 + text-transform: uppercase;
114 + color: var(--muted);
115 + margin: 20px 0 8px;
116 +}
117 +.hero .kicker, section > .kicker, .ep-group > .kicker { color: var(--accent); }
118 +.sidebar nav a {
119 + color: var(--ink-soft);
120 + font-size: 0.86rem;
121 + padding: 5px 10px;
122 + border-radius: 7px;
123 + transition: background 0.12s, color 0.12s;
124 +}
125 +.sidebar nav a:hover {
126 + background: var(--accent-wash);
127 + color: var(--accent-deep);
128 + text-decoration: none;
129 +}
130 +.ep-link { display: flex; align-items: baseline; gap: 8px; }
131 +.ep-method {
132 + font-family: var(--mono);
133 + font-size: 0.56rem;
134 + font-weight: 600;
135 + color: var(--accent);
136 + letter-spacing: 0.06em;
137 +}
138 +
139 +/* ---------------------------------------------------------------- hero */
140 +
141 +.hero { padding-top: 8px; }
142 +.hero h1 {
143 + font-family: var(--serif);
144 + font-size: clamp(2.7rem, 5.5vw, 4rem);
145 + font-weight: 500;
146 + line-height: 1.06;
147 + letter-spacing: -0.01em;
148 + margin: 16px 0 22px;
149 +}
150 +.accent-word {
151 + font-style: italic;
152 + color: var(--accent);
153 +}
154 +.lede { font-size: 1.1rem; color: var(--ink-soft); max-width: 620px; }
155 +.lede strong { color: var(--ink); font-weight: 600; }
156 +
157 +.hero-actions { display: flex; gap: 12px; margin: 28px 0 40px; flex-wrap: wrap; }
158 +.btn {
159 + display: inline-block;
160 + font-weight: 600;
161 + font-size: 0.9rem;
162 + background: var(--accent);
163 + color: white;
164 + padding: 11px 26px;
165 + border: 1px solid var(--accent);
166 + border-radius: 999px;
167 + box-shadow: var(--shadow-sm);
168 + cursor: pointer;
169 + transition: background 0.12s, transform 0.12s;
170 +}
171 +.btn:hover { text-decoration: none; background: var(--accent-deep); transform: translateY(-1px); }
172 +.btn.ghost {
173 + background: var(--card);
174 + color: var(--ink);
175 + border-color: var(--line-strong);
176 +}
177 +.btn.ghost:hover { background: var(--bg-soft); }
178 +
179 +/* stat row */
180 +.stat-chips { display: flex; flex-wrap: wrap; gap: 0; }
181 +.chip {
182 + display: flex;
183 + flex-direction: column;
184 + padding: 4px 28px 4px 18px;
185 + border-left: 1px solid var(--line-strong);
186 + min-width: 120px;
187 +}
188 +.chip b {
189 + font-family: var(--serif);
190 + font-size: 1.7rem;
191 + font-weight: 500;
192 + color: var(--ink);
193 + line-height: 1.15;
194 +}
195 +.chip span { font-size: 0.74rem; color: var(--muted); }
196 +.hero-chips { margin-top: 0; }
197 +
198 +/* ---------------------------------------------------------------- headings */
199 +
200 +h2 {
201 + font-family: var(--serif);
202 + font-size: 1.8rem;
203 + font-weight: 500;
204 + letter-spacing: -0.005em;
205 + margin: 6px 0 16px;
206 +}
207 +h3 { font-family: var(--serif); font-weight: 500; margin: 12px 0 6px; }
208 +h4 { font-weight: 600; margin: 0 0 6px; font-size: 0.92rem; }
209 +.muted { color: var(--muted); }
210 +.small { font-size: 0.8rem; }
211 +
212 +/* callout */
213 +.callout {
214 + margin-top: 20px;
215 + background: var(--accent-wash);
216 + border: 1px solid #d9e2fa;
217 + border-radius: 10px;
218 + padding: 16px 20px;
219 + font-size: 0.9rem;
220 + color: var(--ink-soft);
221 +}
222 +.callout b { color: var(--ink); }
223 +
224 +/* conventions grid */
225 +.conv-grid {
226 + display: grid;
227 + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
228 + gap: 16px;
229 +}
230 +.conv {
231 + background: var(--card);
232 + border: 1px solid var(--line);
233 + border-radius: 12px;
234 + padding: 18px 20px;
235 + box-shadow: var(--shadow-sm);
236 +}
237 +.conv h4 { font-family: var(--serif); font-size: 1.05rem; font-weight: 500; }
238 +.conv p { font-size: 0.85rem; color: var(--ink-soft); margin: 0; }
239 +
240 +/* coverage cards */
241 +.cards {
242 + display: grid;
243 + grid-template-columns: repeat(auto-fill, minmax(255px, 1fr));
244 + gap: 16px;
245 + margin-bottom: 16px;
246 +}
247 +.card {
248 + background: var(--card);
249 + border: 1px solid var(--line);
250 + border-radius: 12px;
251 + padding: 18px 20px;
252 + box-shadow: var(--shadow-sm);
253 +}
254 +.card.accent {
255 + border-color: #c9d7f8;
256 + background: linear-gradient(160deg, var(--accent-wash), #ffffff 70%);
257 +}
258 +.card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
259 +.card-head h3 { margin: 0; font-size: 1.15rem; }
260 +.card-ex { font-size: 0.66rem; color: var(--muted); }
261 +.card p { font-size: 0.85rem; color: var(--ink-soft); margin: 8px 0 0; }
262 +.card .small code { margin: 2px 3px 0 0; display: inline-block; }
263 +
264 +/* ---------------------------------------------------------------- endpoints */
265 +
266 +.ep-group { margin-bottom: 40px; }
267 +.ep-group > h2 { margin-bottom: 26px; }
268 +
269 +.endpoint {
270 + display: flex;
271 + background: var(--card);
272 + border: 1px solid var(--line);
273 + border-radius: 14px;
274 + box-shadow: var(--shadow);
275 + margin-bottom: 30px;
276 + overflow: hidden;
277 +}
278 +.endpoint-index {
279 + flex-shrink: 0;
280 + width: 58px;
281 + display: flex;
282 + align-items: flex-start;
283 + justify-content: center;
284 + padding-top: 26px;
285 + font-family: var(--serif);
286 + font-style: italic;
287 + font-size: 0.95rem;
288 + color: var(--muted);
289 + background: var(--bg-soft);
290 + border-right: 1px solid var(--line);
291 +}
292 +.endpoint-body { padding: 22px 28px 26px; min-width: 0; flex: 1; }
293 +.endpoint-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
294 +.method {
295 + font-family: var(--mono);
296 + font-size: 0.64rem;
297 + font-weight: 600;
298 + letter-spacing: 0.08em;
299 + color: var(--accent-deep);
300 + background: var(--accent-wash);
301 + border: 1px solid #d9e2fa;
302 + border-radius: 999px;
303 + padding: 2px 11px;
304 +}
305 +.path {
306 + font-size: 0.86rem;
307 + background: var(--bg-soft);
308 + white-space: normal;
309 + word-break: break-all;
310 + color: var(--ink);
311 +}
312 +.endpoint h3 { font-size: 1.35rem; margin: 14px 0 4px; }
313 +.endpoint-desc { font-size: 0.9rem; color: var(--ink-soft); margin: 0 0 16px; }
314 +
315 +/* params table */
316 +table.params {
317 + width: 100%;
318 + border-collapse: collapse;
319 + font-size: 0.82rem;
320 + margin: 8px 0 20px;
321 +}
322 +table.params th, table.params td {
323 + text-align: left;
324 + padding: 9px 12px;
325 + border-bottom: 1px solid var(--line);
326 + vertical-align: top;
327 +}
328 +table.params thead th {
329 + font-size: 0.64rem;
330 + font-weight: 700;
331 + text-transform: uppercase;
332 + letter-spacing: 0.12em;
333 + color: var(--muted);
334 + border-bottom: 1px solid var(--line-strong);
335 +}
336 +table.params tr:last-child td { border-bottom: none; }
337 +table.params td code { white-space: nowrap; }
338 +td.type { font-family: var(--mono); font-size: 0.7rem; color: var(--muted); white-space: nowrap; }
339 +td.num { font-family: var(--mono); font-size: 0.78rem; text-align: right; }
340 +.req {
341 + font-family: var(--mono); font-size: 0.6rem; font-weight: 600;
342 + color: var(--accent-deep); background: var(--accent-wash);
343 + border: 1px solid #d9e2fa;
344 + border-radius: 999px; padding: 2px 8px; letter-spacing: 0.04em;
345 +}
346 +.opt { font-family: var(--mono); font-size: 0.6rem; color: var(--muted); }
347 +
348 +/* code tabs + response */
349 +.code-tabs, .response-block { margin-top: 16px; }
350 +.code-tabs-bar { display: flex; gap: 5px; align-items: center; margin-bottom: 8px; }
351 +.code-tabs-label {
352 + font-size: 0.62rem;
353 + font-weight: 700;
354 + letter-spacing: 0.14em;
355 + text-transform: uppercase;
356 + color: var(--muted);
357 + margin-right: 8px;
358 +}
359 +.tab {
360 + font-family: var(--sans);
361 + font-size: 0.74rem;
362 + font-weight: 500;
363 + background: transparent;
364 + border: 1px solid var(--line);
365 + border-radius: 999px;
366 + color: var(--ink-soft);
367 + padding: 4px 14px;
368 + cursor: pointer;
369 + transition: all 0.12s;
370 +}
371 +.tab:hover { border-color: var(--line-strong); background: var(--bg-soft); }
372 +.tab.active {
373 + background: var(--ink);
374 + border-color: var(--ink);
375 + color: #fff;
376 +}
377 +.tab.copy, .tab.live { margin-left: auto; }
378 +.tab.live {
379 + background: var(--accent);
380 + border-color: var(--accent);
381 + color: white;
382 +}
383 +.tab.live:hover { background: var(--accent-deep); }
384 +.tab:disabled { opacity: 0.6; cursor: default; }
385 +pre.response { max-height: 340px; overflow: auto; }
386 +
387 +/* fields accordion */
388 +.fields { margin-top: 16px; }
389 +.fields-toggle {
390 + font-family: var(--sans);
391 + font-size: 0.78rem;
392 + font-weight: 600;
393 + background: transparent;
394 + border: 1px solid var(--line);
395 + border-radius: 999px;
396 + padding: 6px 16px;
397 + cursor: pointer;
398 + color: var(--ink-soft);
399 + transition: all 0.12s;
400 +}
401 +.fields-toggle:hover { background: var(--accent-wash); border-color: #d9e2fa; color: var(--accent-deep); }
402 +.fields-n {
403 + font-family: var(--mono);
404 + font-size: 0.62rem;
405 + background: var(--accent-wash);
406 + color: var(--accent-deep);
407 + border-radius: 999px;
408 + padding: 1px 8px;
409 + margin-left: 6px;
410 +}
411 +.fields table.params { margin-top: 12px; }
412 +
413 +/* live status */
414 +.table-scroll {
415 + max-height: 430px;
416 + overflow-y: auto;
417 + border: 1px solid var(--line);
418 + border-radius: 12px;
419 + box-shadow: var(--shadow-sm);
420 + background: var(--card);
421 + margin-top: 18px;
422 +}
423 +.table-scroll table.params { margin: 0; }
424 +.table-scroll thead th { position: sticky; top: 0; background: var(--card); }
425 +#live .stat-chips { margin-top: 8px; }
426 +
427 +/* about + footer */
428 +.about p { max-width: 640px; }
429 +footer {
430 + border-top: 1px solid var(--line);
431 + padding-top: 24px;
432 + color: var(--muted);
433 + font-size: 0.8rem;
434 + display: flex;
435 + justify-content: space-between;
436 + align-items: baseline;
437 + gap: 16px;
438 + flex-wrap: wrap;
439 +}
440 +.foot-brand {
441 + font-family: var(--serif);
442 + font-style: italic;
443 + letter-spacing: 0.02em;
444 + font-size: 0.85rem;
445 + color: var(--ink-soft);
446 + text-transform: none;
447 +}
448 +
449 +/* ---------------------------------------------------------------- responsive */
450 +
451 +@media (max-width: 860px) {
452 + .sidebar { display: none; }
453 + main { padding: 30px 22px 50px; }
454 + .endpoint-index { display: none; }
455 + .hero h1 { font-size: 2.5rem; }
456 + .chip { padding-right: 18px; }
457 +}
added hfmarketdata/web/vite.config.js +12 −0
@@ -0,0 +1,12 @@
1 +import { defineConfig } from 'vite'
2 +import react from '@vitejs/plugin-react'
3 +
4 +export default defineConfig({
5 + plugins: [react()],
6 + server: {
7 + proxy: {
8 + '/v1': 'http://localhost:8090',
9 + '/health': 'http://localhost:8090',
10 + },
11 + },
12 +})
added requirements.txt +2 −0
@@ -0,0 +1,2 @@
1 +requests>=2.31
2 +duckdb>=1.0
3