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>
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 | +[](https://www.hfmarketdata.io) | |
| 6 | +[](https://www.hfmarketdata.io/docs) | |
| 7 | +[](#) | |
| 8 | +[](#) | |
| 9 | +[](#) | |
| 10 | +[](#) | |
| 11 | +[](#) | |
| 12 | +[](#) | |
| 13 | + | |
| 14 | +[](mailto:contact@spboucher.ai) | |
| 15 | +[](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> 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: | |
| 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 | ||