SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

v2 socle : core (config, erreurs uniformes, SQLite, DuckDB, réponses json/csv/parquet), OpenAPI 3.1, chargeur de modules v2, SPA fallback, tests de non-régression + lac synthétique, shell web (router, layout, tokens, client API), plan d'upgrade

Simon-Pierre Boucher committed 20 days ago (Sep 4, 2026) parent 5e6a3cc

38 changed files +3,248 −521

modified .gitignore +5 −0
@@ -22,3 +22,8 @@ hfmarketdata/venv/
22 22
23 23 # OS / editor
24 24 .DS_Store
25 +
26 +# python env local
27 +.venv/
28 +.pytest_cache/
29 +.ruff_cache/
added CLAUDE.md +20 −0
@@ -0,0 +1,20 @@
1 +# hfmarketdata — conventions de travail (lues par tous les agents)
2 +
3 +Plateforme : **www.hfmarketdata.io** — API FastAPI + DuckDB sur un lac Parquet (350 Go, FirstRate Data), site React/Vite servi par l'API. Plan complet : `docs/UPGRADE-PLAN.md` (lire en premier). Prod : nœud **M3U96b** (`~/hfmarketdata`, PM2 `hfmarketdata-api` :8090, lac `~/firstratedata`, Redis local), déployé par `mld` depuis la passerelle M1M32. Dépôt de vérité : spbgit `gitsrv:srv/git/hfmarketdata.git` (branche `main`).
4 +
5 +## Règles absolues
6 +- **Aucun breaking change** : les endpoints `/v1/*` existants (main.py) gardent paths, paramètres, formes de réponse (`{"count","data"}`) et codes. Tout le neuf est additif sous `/v1/`.
7 +- **Erreurs** : lever `core.errors.ApiError(status, CODE, message, type=…, details=…)` ; les codes vivent dans `core/errors.py::CODES` (en ajouter si besoin, jamais de code inconnu). Jamais de `HTTPException` dans le code v2.
8 +- **Réponses v2** : `core.responses.frame_response(df, fmt, meta=…, request=…)` pour les tableaux (json/csv/parquet, enveloppe `{"data","meta"}`, `X-Row-Count`), `json_response(...)` pour le reste, `clamp_limit(...)` pour `limit`, curseurs via `encode_cursor/decode_cursor`. Timestamps UTC ISO 8601.
9 +- **Données** : DuckDB via `core.duck.con()` (connexion par thread) + `core.duck.cached(key, builder)` pour les scans de répertoires. `settings.parquet` = racine du lac. **Jamais de valeur inventée** : absence → `null` + raison dans `coverage`.
10 +- **Métadonnées** : SQLite via `core.db` (`Base`, `session()`, `get_session` dépendance FastAPI, `create_all()` idempotent au démarrage du module). Un module = un fichier `models.py`.
11 +- **Config** : uniquement `core.config.settings` (variables `HFMD_*`). Secrets jamais en dur, jamais dans les exemples publics.
12 +- **Structure** : `hfmarketdata/api/<module>/{routes.py,models.py,service.py,…}` ; `routes.py` expose `router` (APIRouter avec `prefix="/v1/…"`, `tags=[…]`) OU `install(app)` (middleware). `main.py` charge les modules listés dans `V2_MODULES` — ne pas éditer main.py au-delà de cette liste. Imports absolus depuis `hfmarketdata/api` (ex. `from core.errors import ApiError`).
13 +- **Quota lignes** : toujours poser `X-Row-Count` (fait par `frame_response`). Endpoints coûteux : `request.state.request_cost = 2` (screener, frames). Bulk : `request.state.quota_exempt = True`.
14 +- **Style** : Python 3.12+ typé, docstrings en anglais (le produit est en anglais), commentaires courts. Frontend : React 18 + Vite, dark par défaut, pas de framework CSS lourd (CSS modules / variables), composants dans `hfmarketdata/web/src`.
15 +- **Tests** : pytest dans `tests/` (`pytest.ini` met `hfmarketdata/api` sur le path) ; lac synthétique `tests/fixtures/make_fixtures.py` (l'étendre si un module a besoin d'autres fichiers) ; Redis = `fakeredis` quand `HFMD_REDIS_URL=fakeredis://`. Chaque module livre ses tests unitaires + intégration (TestClient). `./.venv/bin/python -m pytest` doit rester vert.
16 +- **OpenAPI 3.1** : chaque route a `summary`, `description` (markdown), `response_model` ou `responses={…}` avec exemples réels, et déclare ses erreurs possibles via `openapi_extra={"x-errors": ["CONTRACT_NOT_FOUND", …]}`. La doc du site est générée depuis `/openapi.json` : la qualité des descriptions EST la doc.
17 +- **Git** : commits atomiques en français, préfixe du chantier (`futures:`, `accounts:`, `ratelimit:`, `fundamentals:`, `web:`, `mcp:`, `docs:`). Ne pas committer `.venv`, `node_modules`, `dist`, données.
18 +
19 +## Données réelles (référence, pas pour les tests)
20 +Layout : `parquet/{stock|etf|crypto|index|fx}/{1min|5min|30min|1hour|1day}/{adj}/{TICKER}_{tf}.parquet` · `parquet/futures/{tf}/{contin_UNadj|contin_adj_ratio|contin_adj_absolute}/{ROOT}_{tf}.parquet` · `parquet/futures_contracts/{tf}/{archive|update}/{ROOT}_{MonthCode}{YY}_{tf}.parquet` (142 racines, ~15 000 contrats/tf, la colonne `ticker` = racine ; archive ≤ 2025, update ≥ 2025 avec chevauchement → dédupliquer sur `datetime`, priorité update) · `parquet/options/{yyyy}_{qN}/{TICKER}_month_option_chain.parquet` · `meta/futures/futures.csv` (Ticker, Name, First Date, Last Date). Colonnes barres : ticker, datetime (TIMESTAMP naïf, heure US/Eastern pour l'intraday), open, high, low, close, volume, open_interest (futures 1day). API publique pour vérifier : `https://www.hfmarketdata.io/v1/status`. Accès au nœud si indispensable : `ssh M3U96b` (lecture seule sur `~/firstratedata`).
added docs/UPGRADE-PLAN.md +164 −0
@@ -0,0 +1,164 @@
1 +# HF Market Data — Upgrade majeur (v2) : plan d'exécution
2 +
3 +Date : 2026-09-04 · Auteur : Simon-Pierre Boucher · Statut : livrable 1 (schéma · plan de doc · maquettes), puis implémentation chantier par chantier.
4 +
5 +Contraintes retenues : aucun breaking change (`/v1/*` existants inchangés, erreurs enrichies mais `detail` conservé), UTC/ISO 8601, erreurs JSON uniformes `{"error":{"code","message","docs"}}`, p95 < 300 ms pour 10 000 barres, clés hashées, rate limit aussi sur l'inscription.
6 +
7 +## 0. Architecture cible
8 +
9 +```
10 +hfmarketdata/
11 + api/ FastAPI (Python 3.14, DuckDB sur le lac Parquet, SQLite métadonnées, Redis quotas)
12 + main.py app + endpoints historiques (inchangés) + montage des routeurs v2
13 + core/config.py env (HFMD_*) · core/errors.py (format uniforme) · core/db.py (SQLite/SQLAlchemy)
14 + futures/ chantier 1 : symbols.py (parsing), specs.py (référentiel racines), rolls.py, backfill.py, routes.py
15 + accounts/ chantier 2 : models.py, security.py (hash/PAT), routes_auth.py, routes_me.py, routes_admin.py, mailer.py, cli.py
16 + ratelimit/ chantier 2 : redis_limiter.py (Lua), middleware.py, tiers.py
17 + openapi.py spec 3.1 enrichie (tags, exemples, codes d'erreur) = source unique de la doc
18 + web/ Vite + React 18 + react-router · dark par défaut · SPA code-splittée (pré-rendu des pages statiques au build)
19 + src/app/ shell, nav, thème, auth context
20 + src/pages/{home,docs,playground,integrations,pricing,status,auth,dashboard,admin}
21 + src/docs/ générateur 3 colonnes depuis /openapi.json + guides MDX (content/guides/*.mdx) + changelog
22 + src/playground/ constructeur de requêtes, exécution, tableau, mini-graphique, export de code, headers quota
23 + mcp/ chantier 5 : serveur MCP `hfmarketdata-mcp` (TypeScript, stdio) + README vitrine
24 + skills/ chantier 5 : pack de skills (SKILL.md + scripts) → web/public/downloads/hfmarketdata-skills.zip
25 + tests/ pytest (unit + intégration TestClient + Redis) · web/e2e (Playwright)
26 + scripts/backfill_contracts.py chantier 1 : backfill idempotent des échéances (2010 →)
27 +frd_downloader.py inchangé
28 +```
29 +
30 +Stockage : **SQLite** (`HFMD_STATE_DB`, défaut `~/firstratedata/state/hfmd.db`) pour comptes, clés, tiers, usage journalier et `futures_contracts` ; **Redis** (`HFMD_REDIS_URL`) pour les fenêtres glissantes de quotas (requêtes + lignes), scripts Lua atomiques.
31 +
32 +## 1. Schéma de base de données
33 +
34 +### 1.1 `futures_roots` (référentiel des produits)
35 +| colonne | type | note |
36 +|---|---|---|
37 +| root | TEXT PK | ex. ES, CL, 6E (FirstRate : `E6` → alias `6E` accepté) |
38 +| name | TEXT | « E-mini S&P 500 » |
39 +| exchange | TEXT | CME, CBOT, NYMEX, COMEX, ICE, EUREX, … |
40 +| asset_class | TEXT | equity_index, energy, metals, rates, ags, fx, crypto, volatility, softs, livestock |
41 +| currency | TEXT | USD, EUR, … |
42 +| contract_size | REAL / TEXT | « 50 × index », 1 000 bbl… (valeur numérique + `contract_size_unit`) |
43 +| tick_size | REAL | |
44 +| tick_value | REAL | |
45 +| settlement_type | TEXT | cash / physical |
46 +| expiry_rule | TEXT | clé de règle (ex. `third_friday`, `cl_rule`, `last_business_day`, `data`) |
47 +| month_cycle | TEXT | ex. `HMUZ`, `FGHJKMNQUVXZ` |
48 +| first_data_date, last_data_date | DATE | dérivés du lac (min/max sur tous les contrats) |
49 +| contracts_count | INT | |
50 +| source | TEXT | `reference` (table statique) / `derived` |
51 +
52 +### 1.2 `futures_contracts`
53 +| colonne | type | note |
54 +|---|---|---|
55 +| symbol | TEXT PK | forme courte `ESZ25` ; forme longue `ESZ2025` acceptée en entrée |
56 +| root | TEXT FK | |
57 +| month_code | CHAR(1) | F G H J K M N Q U V X Z |
58 +| contract_month | INT (1-12) · contract_year | INT | |
59 +| expiration_date | DATE | règle par racine, sinon `last_data_date` ; `expiration_source` = rule / data |
60 +| last_trading_date | DATE | |
61 +| first_notice_date | DATE | NULL si cash-settled |
62 +| settlement_type, contract_size, tick_size, tick_value, currency, exchange | | copiés de la racine |
63 +| first_data_date, last_data_date | DATE | plage réelle (1day) |
64 +| status | TEXT | active / expired (expired si `last_data_date` < aujourd'hui − 7 j ET mois d'échéance passé) |
65 +| volume_avg_daily | REAL | 20 dernières séances |
66 +| open_interest_last | REAL | dernier OI non nul |
67 +| timeframes | TEXT | JSON des granularités disponibles |
68 +| files | TEXT | JSON `{tf: [archive_path, update_path]}` |
69 +| updated_at | TIMESTAMP | |
70 +Index : (root, expiration_date), (status), (root, contract_year, contract_month).
71 +
72 +### 1.3 `futures_contract_gaps` — `symbol, timeframe, gap_start, gap_end, bars_missing` (gaps > 3 jours ouvrés, calculés au backfill).
73 +
74 +### 1.4 Comptes / clés / quotas
75 +| table | colonnes |
76 +|---|---|
77 +| users | id PK, email UNIQUE, name, password_hash (argon2), email_verified_at, role (`user`/`admin`), tier (`free`/`high_usage`), status (`invited`/`active`/`disabled`), created_at, last_login_at |
78 +| email_tokens | id, user_id FK, kind (`verify`/`reset`/`invite`), token_hash, expires_at, used_at |
79 +| api_keys | id PK, user_id FK, name, prefix (8 car. affichés : `hfmd_live_ab12cd34`), key_hash (sha256 + sel serveur), tier_override (nullable), status (`active`/`revoked`), created_at, last_used_at, revoked_at |
80 +| usage_daily | day, principal (`key:<id>` ou `ip:<hash>`), requests, rows, rows_parquet, bytes, status_2xx, status_429 — PK (day, principal) — agrégé depuis Redis toutes les minutes |
81 +| usage_minute | minute, principal, requests, rows — 7 j glissants (graphes 24 h / 7 j) |
82 +| audit_log | ts, actor, action, target, meta |
83 +
84 +Format d'une clé : `hfmd_live_<32 car. base62>` ; on n'en stocke que le hash et le préfixe ; affichée une seule fois à la création ; révocable ; « régénérer » = révoque + crée.
85 +
86 +### 1.5 Tiers (constantes `ratelimit/tiers.py`)
87 +| tier | fenêtre | requêtes | lignes | lignes max / requête |
88 +|---|---|---|---|---|
89 +| keyless (IP) | 1 h | 30 | 100 000 | 5 000 |
90 +| free (compte + clé) | 1 min | 120 | 1 000 000 | 50 000 |
91 +| high_usage (sur demande à contact@spboucher.ai) | 1 min | 600 | 10 000 000 | 200 000 |
92 +
93 +Redis : clé `rl:{principal}:{req|rows}` — fenêtre glissante par buckets de 1 s (hash) évaluée en Lua : `EVALSHA limiter <key> <now_ms> <window_ms> <cost> <limit>` → `{allowed, remaining, reset_ms}` ; deux appels dans un même script pour requêtes et lignes. Parquet = coût lignes ÷ 2 ; `/v1/bulk/*` et réponses 304 = 0 ligne. Headers `X-RateLimit-*` sur toute réponse ; 429 `Retry-After` + `error.type`.
94 +
95 +## 2. Contrat d'API — nouveautés (toutes additives)
96 +
97 +Chantier 1 · `GET /v1/futures/roots` · `GET /v1/futures/{root}/contracts` · `GET /v1/futures/contract/{symbol}/bars` · `GET /v1/futures/contract/{symbol}/coverage` · `GET /v1/futures/{root}/chain?as_of=` · `GET /v1/futures/{root}/continuous?roll=&adjust=&depth=` · `GET /v1/futures/{root}/term-structure?as_of=`.
98 +Paramètres communs : `interval` (`1m|5m|30m|1h|1d`, alias de `timeframe`), `from`/`to`, `session=rth|eth|all`, `cursor`/`limit`, `format=json|csv|parquet`. Réponses : `{"data":[...], "meta":{"symbol","interval","count","next_cursor","roll_dates":[...]}}`.
99 +Erreurs : 400 `INVALID_CONTRACT_SYMBOL`, 404 `CONTRACT_NOT_FOUND`, 404 `ROOT_NOT_FOUND`, 400 `INVALID_PARAMETER`, 429 `RATE_LIMIT_EXCEEDED`, 401 `INVALID_API_KEY`.
100 +
101 +Chantier 2 · `/v1/auth/{signup,verify,login,logout,forgot,reset,accept-invite}` · `/v1/me` · `/v1/me/keys` (POST/GET/DELETE/POST …/rotate) · `/v1/me/usage?range=24h|7d|30d` · `/v1/admin/users` (CRUD, tier, invitation) · `/v1/admin/usage` · `/v1/limits` (public : tiers + limites courantes du principal).
102 +
103 +## 3. Plan de la documentation (`/docs`)
104 +
105 +Génération : `/openapi.json` (3.1, exemples réels capturés au build) → référence ; `content/guides/*.mdx` → guides ; `content/changelog.mdx`. Layout 3 colonnes (nav · texte · code curl/Python/JS/R avec langue persistante), Cmd+K, « Try it » → `/playground?ep=…&params…`, lien « Edit / Report issue » (mailto contact@spboucher.ai + lien repo), dark par défaut, mobile : nav en tiroir, colonne code repliable.
106 +
107 +```
108 +Getting started Guides Reference (OpenAPI) More
109 + Quickstart (30 s) Futures individual contracts Meta & status Changelog
110 + Authentication Options chains & Greeks Bars (stocks/ETF/crypto/…) Versioning & deprecation
111 + Rate limits Time zones & sessions (RTH/ETH) Futures v2 (7 endpoints) Limits & pricing
112 + Data formats Bulk downloads Options Integrations (MCP, skills)
113 + Recipes: pandas backtest · Accounts & keys · Usage Status
114 + custom continuous · CL term structure Errors
115 +```
116 +
117 +## 4. Maquettes
118 +
119 +### 4.1 Playground (`/playground`, aussi intégré au dashboard avec clé injectée)
120 +```
121 +┌ HF Market Data ─ Docs · Playground · Integrations · Pricing · Status ─────────────── [Sign in] ┐
122 +│ ⓘ Keyless mode: 30 req/h. Create a free account for 120 req/min → [Create free account] │
123 +├──────────────────────────────┬─────────────────────────────────────────────────────────────────┤
124 +│ Request type ▾ │ GET https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=… │ [Copy]
125 +│ ○ Stock/ETF bars │ ──────────────────────────────────────────────────────────────── │
126 +│ ○ Crypto · FX │ [Run ▶] 200 OK · 184 ms · 2 512 rows │
127 +│ ● Futures continuous │ ┌ Table ─┬ JSON ─┬ Chart ──────────────────────────────────────┐ │
128 +│ ○ Individual contract │ │ ▂▃▅▆▇█▇▆▅ candlesticks (lightweight-charts) roll markers ▲ │ │
129 +│ ○ Contract chain │ └─────────────────────────────────────────────────────────────┘ │
130 +│ ○ Term structure │ Rate limit: requests 29/30 · rows 97 488/100 000 · reset 41 min │
131 +│ ○ Options chain (Greeks) │ ┌ curl ─┬ Python (requests+pandas) ─┬ JavaScript (fetch) ─────┐ │
132 +│ ○ Symbols / roots │ │ import requests, pandas as pd … │ │
133 +│ ─ Form (dynamic) ─ │ └─────────────────────────────────────────────────────────────┘ │
134 +│ root [ES ▾ autocomplete] │ Examples: AAPL 1-min today · CL chain · ES continuous back-adj │
135 +│ roll [volume ▾] adjust [back_adjusted ▾] depth [1 ▾] 2015-2025 · NG term structure │
136 +│ from [2015-01-01] to [2025-12-31] interval [1d ▾] format [json ▾] session [all ▾] │
137 +└──────────────────────────────┴─────────────────────────────────────────────────────────────────┘
138 +```
139 +
140 +### 4.2 Dashboard (`/dashboard`)
141 +```
142 +┌ Sidebar: Overview · API keys · Usage · Playground · Account Tier: Free · Need more? contact@spboucher.ai ┐
143 +│ Overview ─ Requests today 1 240 / (120/min) · Rows today 3.1 M · Last request 2 min ago │
144 +│ API keys ─ ┌ name ─ prefix ─ created ─ last used ─ status ─ actions ┐ [+ Create key] │
145 +│ │ default hfmd_live_ab12cd34… 2026-09-04 just now active [Rotate] [Revoke] │ (clé montrée 1 fois) │
146 +│ Usage ─ [24h] [7d] [30d] ▁▂▃▅▆▇█ requests / min ▁▁▂▃▅▆ rows / min · table par jour · export CSV │
147 +│ Playground─ même composant qu'en public, clé injectée automatiquement, bandeau « authenticated as … » │
148 +│ Admin (rôle admin) ─ users (créer · inviter · tier · désactiver) · consommation globale · top principals │
149 +└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
150 +```
151 +
152 +### 4.3 Pricing / Limits (`/pricing`)
153 +Tableau 3 colonnes (Keyless · Free account · High usage) avec les 6 chiffres, CTA « Create free account » et « Request high usage → contact@spboucher.ai », encart « incitations » (Parquet = ½ coût, bulk hors quota, ETag/304 gratuits).
154 +
155 +### 4.4 Homepage
156 +Hero (« Open high-frequency market data. 1-minute to daily, since 2010. ») · bloc « Get an API key » en 3 étapes (keyless → free → high usage) · aperçu live du playground (requête ES continu) · Integrations (Claude Code · Cursor · Codex · MCP) · datasets live (compteurs `/v1/status`).
157 +
158 +## 5. Ordre d'implémentation
159 +1. Socle : config, erreurs uniformes, SQLite, OpenAPI 3.1 enrichie, tests de non-régression des endpoints v1.
160 +2. Chantier 1 (futures) + backfill + tests unitaires symboles/rolls.
161 +3. Chantier 2 (Redis limiter, clés, comptes, dashboard API, CLI `hfmd users add`, seed des 3 utilisateurs).
162 +4. Chantier 6 + 3 + 4 (site, docs 3 colonnes, playground) — frontend.
163 +5. Chantier 5 (MCP + skills + page Integrations).
164 +6. Tests E2E Playwright, déploiement via `mld` sur M3U96b (Redis + backfill dans le manifeste).
added hfmarketdata/api/core/__init__.py +1 −0
@@ -0,0 +1 @@
1 +"""Shared runtime for the HF Market Data API (config · errors · SQLite · DuckDB · responses)."""
added hfmarketdata/api/core/config.py +49 −0
@@ -0,0 +1,49 @@
1 +"""Runtime configuration — every knob is an HFMD_* environment variable.
2 +
3 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
4 +"""
5 +from __future__ import annotations
6 +
7 +import os
8 +from dataclasses import dataclass, field
9 +from pathlib import Path
10 +
11 +
12 +def _env(name: str, default: str | None = None) -> str | None:
13 + v = os.environ.get(name)
14 + return v if v not in (None, "") else default
15 +
16 +
17 +@dataclass(frozen=True)
18 +class Settings:
19 + data_root: Path = field(default_factory=lambda: Path(_env("HFMD_DATA_ROOT", "/Volumes/ssd/firstratedata")))
20 + web_dist: Path = field(default_factory=lambda: Path(_env("HFMD_WEB_DIST", str(Path(__file__).resolve().parents[2] / "web" / "dist"))))
21 + port: int = field(default_factory=lambda: int(_env("HFMD_PORT", "8090")))
22 + public_url: str = field(default_factory=lambda: _env("HFMD_PUBLIC_URL", "https://www.hfmarketdata.io"))
23 + docs_url: str = field(default_factory=lambda: _env("HFMD_DOCS_URL", "https://www.hfmarketdata.io/docs"))
24 + contact_email: str = field(default_factory=lambda: _env("HFMD_CONTACT_EMAIL", "contact@spboucher.ai"))
25 + # metadata store (accounts, keys, usage, futures contracts, fundamentals) — SQLite, WAL mode
26 + state_db: Path = field(default_factory=lambda: Path(_env("HFMD_STATE_DB", str(Path(_env("HFMD_DATA_ROOT", "/Volumes/ssd/firstratedata")) / "state" / "hfmd.db"))))
27 + # rate limiting / sessions
28 + redis_url: str = field(default_factory=lambda: _env("HFMD_REDIS_URL", "redis://127.0.0.1:6379/0"))
29 + ratelimit_enabled: bool = field(default_factory=lambda: _env("HFMD_RATELIMIT", "1") not in ("0", "false", "no"))
30 + # secrets
31 + secret_key: str = field(default_factory=lambda: _env("HFMD_SECRET_KEY", "dev-only-change-me"))
32 + key_hash_salt: str = field(default_factory=lambda: _env("HFMD_KEY_SALT", "hfmd-key-salt-dev"))
33 + # e-mail (Resend) — invitations / verification / password reset
34 + resend_api_key: str | None = field(default_factory=lambda: _env("HFMD_RESEND_API_KEY"))
35 + mail_from: str = field(default_factory=lambda: _env("HFMD_MAIL_FROM", "HF Market Data <noreply@hfmarketdata.io>"))
36 + # SEC EDGAR
37 + sec_user_agent: str = field(default_factory=lambda: _env("HFMD_SEC_USER_AGENT", "HF Market Data (Simon-Pierre Boucher, contact@spboucher.ai)"))
38 + env: str = field(default_factory=lambda: _env("HFMD_ENV", "production"))
39 +
40 + @property
41 + def parquet(self) -> Path:
42 + return self.data_root / "parquet"
43 +
44 + @property
45 + def is_dev(self) -> bool:
46 + return self.env != "production"
47 +
48 +
49 +settings = Settings()
added hfmarketdata/api/core/db.py +73 −0
@@ -0,0 +1,73 @@
1 +"""SQLite metadata store (accounts, API keys, usage, futures contracts, fundamentals).
2 +
3 +SQLAlchemy 2.x, WAL mode, foreign keys on. One engine per process; `session()` is a context manager.
4 +Modules declare their tables on `Base` and call `create_all()` at import/startup (idempotent).
5 +
6 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
7 +"""
8 +from __future__ import annotations
9 +
10 +import contextlib
11 +from collections.abc import Iterator
12 +from pathlib import Path
13 +
14 +from sqlalchemy import create_engine, event, text
15 +from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
16 +
17 +from .config import settings
18 +
19 +
20 +class Base(DeclarativeBase):
21 + pass
22 +
23 +
24 +def _make_engine(path: Path):
25 + path.parent.mkdir(parents=True, exist_ok=True)
26 + eng = create_engine(f"sqlite:///{path}", future=True, connect_args={"check_same_thread": False, "timeout": 30})
27 +
28 + @event.listens_for(eng, "connect")
29 + def _pragmas(dbapi_con, _):
30 + cur = dbapi_con.cursor()
31 + cur.execute("PRAGMA journal_mode=WAL")
32 + cur.execute("PRAGMA synchronous=NORMAL")
33 + cur.execute("PRAGMA foreign_keys=ON")
34 + cur.execute("PRAGMA busy_timeout=30000")
35 + cur.close()
36 +
37 + return eng
38 +
39 +
40 +engine = _make_engine(settings.state_db)
41 +SessionLocal = sessionmaker(bind=engine, expire_on_commit=False, future=True)
42 +
43 +
44 +def create_all() -> None:
45 + Base.metadata.create_all(engine)
46 +
47 +
48 +@contextlib.contextmanager
49 +def session() -> Iterator[Session]:
50 + s = SessionLocal()
51 + try:
52 + yield s
53 + s.commit()
54 + except Exception:
55 + s.rollback()
56 + raise
57 + finally:
58 + s.close()
59 +
60 +
61 +def get_session() -> Iterator[Session]:
62 + """FastAPI dependency."""
63 + with session() as s:
64 + yield s
65 +
66 +
67 +def healthcheck() -> bool:
68 + try:
69 + with engine.connect() as c:
70 + c.execute(text("SELECT 1"))
71 + return True
72 + except Exception:
73 + return False
added hfmarketdata/api/core/duck.py +49 −0
@@ -0,0 +1,49 @@
1 +"""DuckDB access to the Parquet lake — one read-only connection per thread + a small TTL cache.
2 +
3 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
4 +"""
5 +from __future__ import annotations
6 +
7 +import threading
8 +import time
9 +from collections.abc import Callable
10 +from typing import Any
11 +
12 +import duckdb
13 +
14 +from .config import settings
15 +
16 +_tls = threading.local()
17 +_cache: dict[str, tuple[float, Any]] = {}
18 +_cache_lock = threading.Lock()
19 +CACHE_TTL = 300
20 +
21 +
22 +def con() -> duckdb.DuckDBPyConnection:
23 + if not hasattr(_tls, "con"):
24 + c = duckdb.connect()
25 + c.execute("SET threads TO 4")
26 + c.execute("SET enable_object_cache=true")
27 + _tls.con = c
28 + return _tls.con
29 +
30 +
31 +def cached(key: str, builder: Callable[[], Any], ttl: int = CACHE_TTL) -> Any:
32 + now = time.time()
33 + with _cache_lock:
34 + hit = _cache.get(key)
35 + if hit and now - hit[0] < ttl:
36 + return hit[1]
37 + value = builder()
38 + with _cache_lock:
39 + _cache[key] = (now, value)
40 + return value
41 +
42 +
43 +def invalidate(prefix: str = "") -> None:
44 + with _cache_lock:
45 + for k in [k for k in _cache if k.startswith(prefix)]:
46 + del _cache[k]
47 +
48 +
49 +PARQUET = settings.parquet
added hfmarketdata/api/core/errors.py +122 −0
@@ -0,0 +1,122 @@
1 +"""Uniform error envelope for the whole API.
2 +
3 +Every error response has the shape:
4 +
5 + {"error": {"code": "CONTRACT_NOT_FOUND", "message": "…", "docs": "https://…/docs/errors#contract_not_found",
6 + "type": "…" (optional), "details": {…} (optional)},
7 + "detail": "…"} # legacy field kept for the v1 endpoints (no breaking change)
8 +
9 +Raise `ApiError(status, code, message, **extra)` anywhere; FastAPI HTTPException and validation
10 +errors are converted by the handlers registered in `install(app)`.
11 +
12 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
13 +"""
14 +from __future__ import annotations
15 +
16 +from typing import Any
17 +
18 +from fastapi import FastAPI, HTTPException, Request
19 +from fastapi.exceptions import RequestValidationError
20 +from fastapi.responses import JSONResponse
21 +
22 +from .config import settings
23 +
24 +# Canonical codes (documented in /docs/errors). Add new ones here so the docs stay exhaustive.
25 +CODES: dict[str, str] = {
26 + "INVALID_PARAMETER": "A query or path parameter is malformed or out of range.",
27 + "VALIDATION_ERROR": "The request did not match the endpoint schema.",
28 + "NOT_FOUND": "The requested resource does not exist.",
29 + "TICKER_NOT_FOUND": "Unknown ticker for this asset type / timeframe / adjustment.",
30 + "ASSET_NOT_FOUND": "Unknown asset type.",
31 + "INVALID_CONTRACT_SYMBOL": "Futures contract symbol must look like ESZ25 or ESZ2025.",
32 + "CONTRACT_NOT_FOUND": "No data for this futures contract.",
33 + "ROOT_NOT_FOUND": "Unknown futures root.",
34 + "OPTIONS_UNAVAILABLE": "Options dataset not available.",
35 + "INVALID_API_KEY": "The API key is missing, malformed, revoked or unknown.",
36 + "AUTH_REQUIRED": "Authentication is required for this endpoint.",
37 + "FORBIDDEN": "Your tier or role does not allow this action.",
38 + "RATE_LIMIT_EXCEEDED": "Request or row quota exhausted for the current window.",
39 + "ROW_LIMIT_EXCEEDED": "`limit` is above the maximum rows per request for your tier.",
40 + "CONFLICT": "The resource already exists or is in a conflicting state.",
41 + "INTERNAL_ERROR": "Unexpected server error.",
42 + "SERVICE_UNAVAILABLE": "A dependency (Redis, data lake) is unavailable.",
43 +}
44 +
45 +
46 +def docs_link(code: str) -> str:
47 + return f"{settings.docs_url}/errors#{code.lower()}"
48 +
49 +
50 +class ApiError(Exception):
51 + def __init__(self, status: int, code: str, message: str, *, type: str | None = None,
52 + details: dict[str, Any] | None = None, headers: dict[str, str] | None = None):
53 + super().__init__(message)
54 + self.status = status
55 + self.code = code if code in CODES else "INTERNAL_ERROR"
56 + self.message = message
57 + self.type = type
58 + self.details = details
59 + self.headers = headers or {}
60 +
61 + def payload(self) -> dict[str, Any]:
62 + err: dict[str, Any] = {"code": self.code, "message": self.message, "docs": docs_link(self.code)}
63 + if self.type:
64 + err["type"] = self.type
65 + if self.details:
66 + err["details"] = self.details
67 + return {"error": err, "detail": self.message}
68 +
69 + def response(self) -> JSONResponse:
70 + return JSONResponse(self.payload(), status_code=self.status, headers=self.headers)
71 +
72 +
73 +def _code_for_http(status: int, message: str) -> str:
74 + m = message.lower()
75 + if status == 404:
76 + if "ticker" in m or "not found in" in m:
77 + return "TICKER_NOT_FOUND"
78 + if "asset" in m:
79 + return "ASSET_NOT_FOUND"
80 + if "options" in m:
81 + return "OPTIONS_UNAVAILABLE"
82 + return "NOT_FOUND"
83 + if status == 400:
84 + return "INVALID_PARAMETER"
85 + if status == 401:
86 + return "INVALID_API_KEY"
87 + if status == 403:
88 + return "FORBIDDEN"
89 + if status == 429:
90 + return "RATE_LIMIT_EXCEEDED"
91 + if status == 503:
92 + return "SERVICE_UNAVAILABLE"
93 + return "INTERNAL_ERROR"
94 +
95 +
96 +def install(app: FastAPI) -> None:
97 + @app.exception_handler(ApiError)
98 + async def _api_error(_: Request, exc: ApiError):
99 + return exc.response()
100 +
101 + @app.exception_handler(HTTPException)
102 + async def _http_error(_: Request, exc: HTTPException):
103 + message = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
104 + err = ApiError(exc.status_code, _code_for_http(exc.status_code, message), message,
105 + headers=dict(exc.headers or {}))
106 + return err.response()
107 +
108 + @app.exception_handler(RequestValidationError)
109 + async def _validation_error(_: Request, exc: RequestValidationError):
110 + errs = exc.errors()
111 + first = errs[0] if errs else {}
112 + loc = ".".join(str(x) for x in first.get("loc", []) if x not in ("query", "path", "body"))
113 + message = f"{loc}: {first.get('msg', 'invalid value')}" if loc else "invalid request"
114 + err = ApiError(422, "VALIDATION_ERROR", message, details={"errors": [
115 + {"loc": [str(x) for x in e.get("loc", [])], "msg": e.get("msg"), "type": e.get("type")} for e in errs]})
116 + return err.response()
117 +
118 + @app.exception_handler(Exception)
119 + async def _unhandled(_: Request, exc: Exception): # pragma: no cover
120 + err = ApiError(500, "INTERNAL_ERROR", "Unexpected server error. Please retry or contact "
121 + f"{settings.contact_email}.")
122 + return err.response()
added hfmarketdata/api/core/responses.py +109 −0
@@ -0,0 +1,109 @@
1 +"""Response helpers shared by every v2 endpoint.
2 +
3 +* `Format = json | csv | parquet` — negotiated with the `format` query parameter.
4 +* JSON envelope: `{"data": [...], "meta": {"count": n, "next_cursor": "...", ...}}`.
5 +* Cursor pagination: opaque base64url of the last sort key (`encode_cursor` / `decode_cursor`).
6 +* Row accounting: every response sets `X-Row-Count` so the rate limiter can charge the rows quota
7 + (Parquet is charged at half price, see ratelimit/tiers.py).
8 +* Timestamps are serialised as ISO 8601 strings (UTC, `Z` suffix when tz-aware).
9 +
10 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
11 +"""
12 +from __future__ import annotations
13 +
14 +import base64
15 +import io
16 +import json
17 +import math
18 +from typing import Any, Literal
19 +
20 +import pandas as pd
21 +from fastapi import Request, Response
22 +from fastapi.responses import JSONResponse, PlainTextResponse
23 +
24 +from .errors import ApiError
25 +
26 +Format = Literal["json", "csv", "parquet"]
27 +FORMATS = ("json", "csv", "parquet")
28 +MEDIA = {"json": "application/json", "csv": "text/csv; charset=utf-8", "parquet": "application/vnd.apache.parquet"}
29 +
30 +
31 +def parse_format(value: str | None) -> Format:
32 + v = (value or "json").lower()
33 + if v not in FORMATS:
34 + raise ApiError(400, "INVALID_PARAMETER", f"format must be one of {', '.join(FORMATS)}")
35 + return v # type: ignore[return-value]
36 +
37 +
38 +def encode_cursor(value: Any) -> str:
39 + raw = json.dumps(value, default=str).encode()
40 + return base64.urlsafe_b64encode(raw).decode().rstrip("=")
41 +
42 +
43 +def decode_cursor(cursor: str | None) -> Any:
44 + if not cursor:
45 + return None
46 + try:
47 + pad = "=" * (-len(cursor) % 4)
48 + return json.loads(base64.urlsafe_b64decode(cursor + pad).decode())
49 + except Exception:
50 + raise ApiError(400, "INVALID_PARAMETER", "cursor is not valid")
51 +
52 +
53 +def _clean_frame(df: pd.DataFrame) -> pd.DataFrame:
54 + df = df.copy()
55 + for col in df.columns:
56 + dt = str(df[col].dtype)
57 + if dt.startswith("datetime64[ns, "):
58 + df[col] = df[col].dt.tz_convert("UTC").dt.strftime("%Y-%m-%dT%H:%M:%SZ")
59 + elif dt.startswith("datetime"):
60 + df[col] = df[col].dt.strftime("%Y-%m-%dT%H:%M:%S").str.replace("T00:00:00", "", regex=False)
61 + elif dt == "object":
62 + df[col] = df[col].map(lambda v: v.isoformat() if hasattr(v, "isoformat") else v)
63 + df = df.replace([math.inf, -math.inf], None)
64 + return df.astype(object).where(df.notna(), None)
65 +
66 +
67 +def frame_response(df: pd.DataFrame, fmt: Format, *, meta: dict[str, Any] | None = None,
68 + request: Request | None = None, filename: str = "data") -> Response:
69 + """Serialise a DataFrame in the requested format, with envelope + row accounting."""
70 + n = int(len(df))
71 + headers = {"X-Row-Count": str(n)}
72 + if fmt == "csv":
73 + buf = io.StringIO()
74 + df.to_csv(buf, index=False)
75 + return PlainTextResponse(buf.getvalue(), media_type=MEDIA["csv"], headers={
76 + **headers, "Content-Disposition": f'inline; filename="{filename}.csv"'})
77 + if fmt == "parquet":
78 + buf = io.BytesIO()
79 + df.to_parquet(buf, index=False, compression="zstd")
80 + return Response(buf.getvalue(), media_type=MEDIA["parquet"], headers={
81 + **headers, "Content-Disposition": f'attachment; filename="{filename}.parquet"'})
82 + body = {"data": _clean_frame(df).to_dict(orient="records"), "meta": {"count": n, **(meta or {})}}
83 + return JSONResponse(body, headers=headers)
84 +
85 +
86 +def json_response(data: Any, *, meta: dict[str, Any] | None = None, rows: int | None = None,
87 + status: int = 200) -> JSONResponse:
88 + if rows is None:
89 + rows = len(data) if isinstance(data, list) else 1
90 + body = {"data": data, "meta": {"count": rows, **(meta or {})}}
91 + return JSONResponse(body, status_code=status, headers={"X-Row-Count": str(rows)})
92 +
93 +
94 +def clamp_limit(limit: int | None, default: int, hard_max: int, request: Request | None = None) -> int:
95 + """Clamp `limit` to the hard maximum of the endpoint AND the tier's rows-per-request cap
96 + (set by the rate-limit middleware in `request.state.max_rows`)."""
97 + lim = default if limit is None else int(limit)
98 + if lim < 1:
99 + raise ApiError(400, "INVALID_PARAMETER", "limit must be >= 1")
100 + cap = hard_max
101 + tier_cap = getattr(getattr(request, "state", None), "max_rows", None) if request is not None else None
102 + if tier_cap:
103 + if lim > tier_cap:
104 + raise ApiError(400, "ROW_LIMIT_EXCEEDED",
105 + f"limit {lim} exceeds the maximum rows per request for your tier ({tier_cap}). "
106 + "Create a free account or use Parquet/bulk endpoints for larger extracts.",
107 + details={"max_rows": tier_cap})
108 + cap = min(cap, tier_cap)
109 + return min(lim, cap)
modified hfmarketdata/api/main.py +47 −6
@@ -32,12 +32,15 @@ from pathlib import Path
32 32 import duckdb
33 33 from fastapi import FastAPI, HTTPException, Query
34 34 from fastapi.middleware.cors import CORSMiddleware
35 −from fastapi.responses import PlainTextResponse
35 +from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
36 36 from fastapi.staticfiles import StaticFiles
37 37
38 +from core import errors as _errors
39 +from core.config import settings as _settings
40 +
38 41 __author__ = "Simon-Pierre Boucher"
39 42 __contact__ = "contact@spboucher.ai"
40 −__version__ = "1.0.0"
43 +__version__ = "2.0.0"
41 44
42 45 DATA_ROOT = Path(os.environ.get("HFMD_DATA_ROOT", "/Volumes/ssd/firstratedata"))
43 46 PARQUET = DATA_ROOT / "parquet"
@@ -68,7 +71,10 @@ app = FastAPI(
68 71 )
69 72 app.add_middleware(
70 73 CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
74 + expose_headers=["X-Row-Count", "X-RateLimit-Limit-Requests", "X-RateLimit-Remaining-Requests",
75 + "X-RateLimit-Limit-Rows", "X-RateLimit-Remaining-Rows", "X-RateLimit-Reset", "Retry-After"],
71 76 )
77 +_errors.install(app) # uniform {"error": {code, message, docs}} envelope (legacy "detail" kept)
72 78
73 79 # ------------------------------------------------------------------------------
74 80 # DuckDB — one connection per thread, read-only usage
@@ -177,7 +183,8 @@ def _rows_to_response(rel, limit: int, fmt: str, order_col: str):
177 183 limit = min(limit, MAX_LIMIT_CSV)
178 184 rel = rel.limit(limit)
179 185 df = rel.df()
180 − return PlainTextResponse(df.to_csv(index=False), media_type="text/csv")
186 + return PlainTextResponse(df.to_csv(index=False), media_type="text/csv",
187 + headers={"X-Row-Count": str(len(df))})
181 188 limit = min(limit, MAX_LIMIT_JSON)
182 189 rel = rel.limit(limit)
183 190 df = rel.df()
@@ -188,7 +195,8 @@ def _rows_to_response(rel, limit: int, fmt: str, order_col: str):
188 195 # sérialisables en JSON strict → null
189 196 df = df.replace([float("inf"), float("-inf")], None)
190 197 df = df.astype(object).where(df.notna(), None)
191 − return {"count": len(df), "data": df.to_dict(orient="records")}
198 + return JSONResponse({"count": len(df), "data": df.to_dict(orient="records")},
199 + headers={"X-Row-Count": str(len(df))})
192 200
193 201
194 202 # ------------------------------------------------------------------------------
@@ -468,11 +476,44 @@ def opt_history(ticker: str,
468 476
469 477
470 478 # ------------------------------------------------------------------------------
471 −# Static React platform (mounted last so it doesn't shadow the API)
479 +# v2 modules (each one is optional at import time so a broken module never takes the API down)
480 +# ------------------------------------------------------------------------------
481 +
482 +import importlib
483 +import logging
484 +
485 +_log = logging.getLogger("hfmarketdata")
486 +V2_MODULES = ("ratelimit.middleware", "accounts.routes", "futures.routes", "fundamentals.routes",
487 + "bulk.routes", "stream.routes", "openapi")
488 +for _mod in V2_MODULES:
489 + try:
490 + m = importlib.import_module(_mod)
491 + if hasattr(m, "install"):
492 + m.install(app)
493 + elif hasattr(m, "router"):
494 + app.include_router(m.router)
495 + except ModuleNotFoundError as e: # module not written yet
496 + if e.name and e.name.split(".")[0] not in _mod:
497 + _log.warning("module %s: missing dependency %s", _mod, e.name)
498 + except Exception as e: # pragma: no cover
499 + _log.exception("module %s failed to load: %s", _mod, e)
500 +
501 +
502 +# ------------------------------------------------------------------------------
503 +# Static React platform (mounted last so it doesn't shadow the API) + SPA fallback
472 504 # ------------------------------------------------------------------------------
473 505
474 506 if WEB_DIST.is_dir():
475 − app.mount("/", StaticFiles(directory=str(WEB_DIST), html=True), name="web")
507 + app.mount("/assets", StaticFiles(directory=str(WEB_DIST / "assets")), name="assets")
508 +
509 + @app.get("/{path:path}", include_in_schema=False)
510 + def spa(path: str):
511 + if path.startswith(("v1/", "health", "openapi", "docs/", "redoc")) and not path.startswith("docs/"):
512 + raise HTTPException(404, "Not found")
513 + candidate = (WEB_DIST / path).resolve()
514 + if path and candidate.is_file() and str(candidate).startswith(str(WEB_DIST.resolve())):
515 + return FileResponse(candidate)
516 + return FileResponse(WEB_DIST / "index.html")
476 517
477 518
478 519 if __name__ == "__main__":
added hfmarketdata/api/openapi.py +111 −0
@@ -0,0 +1,111 @@
1 +"""OpenAPI 3.1 customisation — the single source of truth for the reference documentation.
2 +
3 +Adds: servers, tag ordering/descriptions, the uniform error schema, per-endpoint `x-errors`
4 +expansion into `responses`, and rate-limit headers on every operation.
5 +
6 +Author: Simon-Pierre Boucher <contact@spboucher.ai>
7 +"""
8 +from __future__ import annotations
9 +
10 +from fastapi import FastAPI
11 +from fastapi.openapi.utils import get_openapi
12 +
13 +from core.config import settings
14 +from core.errors import CODES, docs_link
15 +
16 +TAGS = [
17 + {"name": "meta", "description": "Health, live dataset inventory and your current limits."},
18 + {"name": "bars", "description": "Intraday & daily OHLCV bars for stocks, ETFs, crypto, indices, FX and continuous futures."},
19 + {"name": "futures", "description": "Individual futures contracts, expiry chains, continuous series, term structure."},
20 + {"name": "options", "description": "End-of-day options chains with quotes, implied volatility and Greeks (since 2010)."},
21 + {"name": "fundamentals", "description": "SEC EDGAR financial statements, ratios, screener — point-in-time, since 2010."},
22 + {"name": "stream", "description": "WebSocket streams (filings)."},
23 + {"name": "bulk", "description": "Whole-universe Parquet extracts, outside the rows quota."},
24 + {"name": "auth", "description": "Sign-up, e-mail verification, login, password reset."},
25 + {"name": "me", "description": "Your account: API keys, usage, tier."},
26 + {"name": "admin", "description": "Administration (admin role)."},
27 +]
28 +
29 +RATE_HEADERS = {
30 + "X-RateLimit-Limit-Requests": {"schema": {"type": "integer"}, "description": "Requests allowed in the current window."},
31 + "X-RateLimit-Remaining-Requests": {"schema": {"type": "integer"}, "description": "Requests left in the current window."},
32 + "X-RateLimit-Limit-Rows": {"schema": {"type": "integer"}, "description": "Data rows allowed in the current window."},
33 + "X-RateLimit-Remaining-Rows": {"schema": {"type": "integer"}, "description": "Data rows left in the current window."},
34 + "X-RateLimit-Reset": {"schema": {"type": "integer"}, "description": "Unix timestamp (seconds) when the window resets."},
35 + "X-Row-Count": {"schema": {"type": "integer"}, "description": "Rows returned by this response."},
36 +}
37 +
38 +ERROR_SCHEMA = {
39 + "type": "object",
40 + "required": ["error", "detail"],
41 + "properties": {
42 + "error": {"type": "object", "required": ["code", "message", "docs"], "properties": {
43 + "code": {"type": "string", "enum": sorted(CODES)},
44 + "message": {"type": "string"},
45 + "docs": {"type": "string", "format": "uri"},
46 + "type": {"type": "string", "description": "Sub-type, e.g. requests_per_hour / rows_per_minute for 429."},
47 + "details": {"type": "object", "additionalProperties": True},
48 + }},
49 + "detail": {"type": "string", "description": "Legacy field (same as error.message)."},
50 + },
51 + "example": {"error": {"code": "CONTRACT_NOT_FOUND", "message": "No data for contract ESZ19",
52 + "docs": docs_link("CONTRACT_NOT_FOUND")}, "detail": "No data for contract ESZ19"},
53 +}
54 +
55 +STATUS_FOR = {"INVALID_PARAMETER": 400, "VALIDATION_ERROR": 422, "NOT_FOUND": 404, "TICKER_NOT_FOUND": 404,
56 + "ASSET_NOT_FOUND": 404, "INVALID_CONTRACT_SYMBOL": 400, "CONTRACT_NOT_FOUND": 404, "ROOT_NOT_FOUND": 404,
57 + "OPTIONS_UNAVAILABLE": 503, "INVALID_API_KEY": 401, "AUTH_REQUIRED": 401, "FORBIDDEN": 403,
58 + "RATE_LIMIT_EXCEEDED": 429, "ROW_LIMIT_EXCEEDED": 400, "CONFLICT": 409, "INTERNAL_ERROR": 500,
59 + "SERVICE_UNAVAILABLE": 503}
60 +
61 +
62 +def build(app: FastAPI) -> dict:
63 + schema = get_openapi(title=app.title, version=app.version, description=app.description, routes=app.routes,
64 + tags=TAGS, servers=[{"url": settings.public_url, "description": "Production"}],
65 + contact={"name": "Simon-Pierre Boucher", "email": settings.contact_email, "url": settings.public_url},
66 + license_info={"name": "Data: FirstRate Data & SEC EDGAR — see terms", "url": settings.public_url + "/docs/terms"})
67 + schema["openapi"] = "3.1.0"
68 + comps = schema.setdefault("components", {})
69 + comps.setdefault("schemas", {})["Error"] = ERROR_SCHEMA
70 + comps.setdefault("securitySchemes", {})["ApiKey"] = {
71 + "type": "http", "scheme": "bearer", "bearerFormat": "hfmd_live_…",
72 + "description": "Optional. `Authorization: Bearer <key>` or `?api_key=<key>`. Without a key you get the keyless "
73 + "(per-IP, hourly) limits; a free account raises them a lot. High usage: contact@spboucher.ai."}
74 + comps["securitySchemes"]["ApiKeyQuery"] = {"type": "apiKey", "in": "query", "name": "api_key"}
75 + schema["security"] = [{}, {"ApiKey": []}, {"ApiKeyQuery": []}]
76 + for path, ops in schema.get("paths", {}).items():
77 + for method, op in ops.items():
78 + if method not in ("get", "post", "put", "patch", "delete"):
79 + continue
80 + responses = op.setdefault("responses", {})
81 + for code in list(responses):
82 + if code.startswith("2"):
83 + responses[code].setdefault("headers", {}).update(RATE_HEADERS)
84 + errs = set(op.pop("x-errors", []) or [])
85 + errs |= {"RATE_LIMIT_EXCEEDED", "VALIDATION_ERROR", "INTERNAL_ERROR"}
86 + for code in sorted(errs):
87 + st = str(STATUS_FOR.get(code, 400))
88 + r = responses.setdefault(st, {"description": ""})
89 + r["description"] = (r.get("description") or "").strip()
90 + desc = f"`{code}` — {CODES.get(code, '')}"
91 + r["description"] = (r["description"] + "\n\n" + desc).strip() if r["description"] and code not in r["description"] else (r["description"] or desc)
92 + r.setdefault("content", {})["application/json"] = {"schema": {"$ref": "#/components/schemas/Error"}}
93 + if "422" in responses and "VALIDATION_ERROR" not in responses["422"].get("description", ""):
94 + responses["422"]["description"] = "`VALIDATION_ERROR` — " + CODES["VALIDATION_ERROR"]
95 + responses["422"]["content"] = {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
96 + schema["info"]["x-logo"] = {"url": settings.public_url + "/logo.svg"}
97 + schema["info"]["x-tiers"] = {
98 + "keyless": {"window": "1h", "requests": 30, "rows": 100_000, "max_rows_per_request": 5_000},
99 + "free": {"window": "1m", "requests": 120, "rows": 1_000_000, "max_rows_per_request": 50_000},
100 + "high_usage": {"window": "1m", "requests": 600, "rows": 10_000_000, "max_rows_per_request": 200_000,
101 + "how": f"e-mail {settings.contact_email}"},
102 + }
103 + return schema
104 +
105 +
106 +def install(app: FastAPI) -> None:
107 + def custom_openapi():
108 + if not app.openapi_schema:
109 + app.openapi_schema = build(app)
110 + return app.openapi_schema
111 + app.openapi = custom_openapi # type: ignore[method-assign]
modified hfmarketdata/requirements.txt +10 −1
@@ -1,4 +1,13 @@
1 1 fastapi>=0.115
2 −uvicorn>=0.30
2 +uvicorn[standard]>=0.30
3 3 duckdb>=1.0
4 4 pandas>=2.0
5 +pyarrow>=17
6 +sqlalchemy>=2.0
7 +redis>=5.0
8 +argon2-cffi>=23
9 +httpx>=0.27
10 +websockets>=12
11 +python-multipart>=0.0.9
12 +itsdangerous>=2.2
13 +pydantic>=2.7
modified hfmarketdata/web/package-lock.json +1978 −23
@@ -1,17 +1,22 @@
1 1 {
2 2 "name": "hfmarketdata-web",
3 − "version": "1.0.0",
3 + "version": "2.0.0",
4 4 "lockfileVersion": 3,
5 5 "requires": true,
6 6 "packages": {
7 7 "": {
8 8 "name": "hfmarketdata-web",
9 − "version": "1.0.0",
9 + "version": "2.0.0",
10 10 "dependencies": {
11 + "lightweight-charts": "^4.2.0",
12 + "prism-react-renderer": "^2.4.0",
11 13 "react": "^18.3.1",
12 − "react-dom": "^18.3.1"
14 + "react-dom": "^18.3.1",
15 + "react-router-dom": "^6.28.0"
13 16 },
14 17 "devDependencies": {
18 + "@mdx-js/rollup": "^3.1.0",
19 + "@playwright/test": "^1.49.0",
15 20 "@vitejs/plugin-react": "^4.3.4",
16 21 "vite": "^6.0.0"
17 22 }
@@ -790,6 +795,64 @@
790 795 "@jridgewell/sourcemap-codec": "^1.4.14"
791 796 }
792 797 },
798 + "node_modules/@mdx-js/mdx": {
799 + "version": "3.1.1",
800 + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz",
801 + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==",
802 + "dev": true,
803 + "license": "MIT",
804 + "dependencies": {
805 + "@types/estree": "^1.0.0",
806 + "@types/estree-jsx": "^1.0.0",
807 + "@types/hast": "^3.0.0",
808 + "@types/mdx": "^2.0.0",
809 + "acorn": "^8.0.0",
810 + "collapse-white-space": "^2.0.0",
811 + "devlop": "^1.0.0",
812 + "estree-util-is-identifier-name": "^3.0.0",
813 + "estree-util-scope": "^1.0.0",
814 + "estree-walker": "^3.0.0",
815 + "hast-util-to-jsx-runtime": "^2.0.0",
816 + "markdown-extensions": "^2.0.0",
817 + "recma-build-jsx": "^1.0.0",
818 + "recma-jsx": "^1.0.0",
819 + "recma-stringify": "^1.0.0",
820 + "rehype-recma": "^1.0.0",
821 + "remark-mdx": "^3.0.0",
822 + "remark-parse": "^11.0.0",
823 + "remark-rehype": "^11.0.0",
824 + "source-map": "^0.7.0",
825 + "unified": "^11.0.0",
826 + "unist-util-position-from-estree": "^2.0.0",
827 + "unist-util-stringify-position": "^4.0.0",
828 + "unist-util-visit": "^5.0.0",
829 + "vfile": "^6.0.0"
830 + },
831 + "funding": {
832 + "type": "opencollective",
833 + "url": "https://opencollective.com/unified"
834 + }
835 + },
836 + "node_modules/@mdx-js/rollup": {
837 + "version": "3.1.1",
838 + "resolved": "https://registry.npmjs.org/@mdx-js/rollup/-/rollup-3.1.1.tgz",
839 + "integrity": "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==",
840 + "dev": true,
841 + "license": "MIT",
842 + "dependencies": {
843 + "@mdx-js/mdx": "^3.0.0",
844 + "@rollup/pluginutils": "^5.0.0",
845 + "source-map": "^0.7.0",
846 + "vfile": "^6.0.0"
847 + },
848 + "funding": {
849 + "type": "opencollective",
850 + "url": "https://opencollective.com/unified"
851 + },
852 + "peerDependencies": {
853 + "rollup": ">=2"
854 + }
855 + },
793 856 "node_modules/@napi-rs/lzma-linux-x64-gnu": {
794 857 "version": "1.5.1",
795 858 "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@@ -810,6 +873,31 @@
810 873 "node": "^22.20 || ^24.12 || >=25"
811 874 }
812 875 },
876 + "node_modules/@playwright/test": {
877 + "version": "1.63.0",
878 + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
879 + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
880 + "dev": true,
881 + "license": "Apache-2.0",
882 + "dependencies": {
883 + "playwright": "1.63.0"
884 + },
885 + "bin": {
886 + "playwright": "cli.js"
887 + },
888 + "engines": {
889 + "node": ">=20"
890 + }
891 + },
892 + "node_modules/@remix-run/router": {
893 + "version": "1.23.4",
894 + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.4.tgz",
895 + "integrity": "sha512-q7j5geK7xs3UJSdm9/iytUNclBnLmYx1EnSeCFXHPeutdqgIMeFeHtUZgS3EhlKxdBEAu8OwtJCwmLrEzpSs7Q==",
896 + "license": "MIT",
897 + "engines": {
898 + "node": ">=14.0.0"
899 + }
900 + },
813 901 "node_modules/@rolldown/pluginutils": {
814 902 "version": "1.0.0-beta.27",
815 903 "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -817,6 +905,36 @@
817 905 "dev": true,
818 906 "license": "MIT"
819 907 },
908 + "node_modules/@rollup/pluginutils": {
909 + "version": "5.4.0",
910 + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
911 + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==",
912 + "dev": true,
913 + "license": "MIT",
914 + "dependencies": {
915 + "@types/estree": "^1.0.0",
916 + "estree-walker": "^2.0.2",
917 + "picomatch": "^4.0.2"
918 + },
919 + "engines": {
920 + "node": ">=14.0.0"
921 + },
922 + "peerDependencies": {
923 + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
924 + },
925 + "peerDependenciesMeta": {
926 + "rollup": {
927 + "optional": true
928 + }
929 + }
930 + },
931 + "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
932 + "version": "2.0.2",
933 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
934 + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
935 + "dev": true,
936 + "license": "MIT"
937 + },
820 938 "node_modules/@rollup/rollup-android-arm-eabi": {
821 939 "version": "4.62.4",
822 940 "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
@@ -1251,6 +1369,16 @@
1251 1369 "@babel/types": "^7.28.2"
1252 1370 }
1253 1371 },
1372 + "node_modules/@types/debug": {
1373 + "version": "4.1.13",
1374 + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
1375 + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
1376 + "dev": true,
1377 + "license": "MIT",
1378 + "dependencies": {
1379 + "@types/ms": "*"
1380 + }
1381 + },
1254 1382 "node_modules/@types/estree": {
1255 1383 "version": "1.0.9",
1256 1384 "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -1258,6 +1386,70 @@
1258 1386 "dev": true,
1259 1387 "license": "MIT"
1260 1388 },
1389 + "node_modules/@types/estree-jsx": {
1390 + "version": "1.0.5",
1391 + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
1392 + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
1393 + "dev": true,
1394 + "license": "MIT",
1395 + "dependencies": {
1396 + "@types/estree": "*"
1397 + }
1398 + },
1399 + "node_modules/@types/hast": {
1400 + "version": "3.0.5",
1401 + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
1402 + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
1403 + "dev": true,
1404 + "license": "MIT",
1405 + "dependencies": {
1406 + "@types/unist": "*"
1407 + }
1408 + },
1409 + "node_modules/@types/mdast": {
1410 + "version": "4.0.4",
1411 + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1412 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1413 + "dev": true,
1414 + "license": "MIT",
1415 + "dependencies": {
1416 + "@types/unist": "*"
1417 + }
1418 + },
1419 + "node_modules/@types/mdx": {
1420 + "version": "2.0.14",
1421 + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz",
1422 + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==",
1423 + "dev": true,
1424 + "license": "MIT"
1425 + },
1426 + "node_modules/@types/ms": {
1427 + "version": "2.1.0",
1428 + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
1429 + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1430 + "dev": true,
1431 + "license": "MIT"
1432 + },
1433 + "node_modules/@types/prismjs": {
1434 + "version": "1.26.6",
1435 + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz",
1436 + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==",
1437 + "license": "MIT"
1438 + },
1439 + "node_modules/@types/unist": {
1440 + "version": "3.0.3",
1441 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1442 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1443 + "dev": true,
1444 + "license": "MIT"
1445 + },
1446 + "node_modules/@ungap/structured-clone": {
1447 + "version": "1.4.0",
1448 + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
1449 + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
1450 + "dev": true,
1451 + "license": "ISC"
1452 + },
1261 1453 "node_modules/@vitejs/plugin-react": {
1262 1454 "version": "4.7.0",
1263 1455 "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@@ -1279,6 +1471,50 @@
1279 1471 "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1280 1472 }
1281 1473 },
1474 + "node_modules/acorn": {
1475 + "version": "8.18.0",
1476 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
1477 + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
1478 + "dev": true,
1479 + "license": "MIT",
1480 + "bin": {
1481 + "acorn": "bin/acorn"
1482 + },
1483 + "engines": {
1484 + "node": ">=0.4.0"
1485 + }
1486 + },
1487 + "node_modules/acorn-jsx": {
1488 + "version": "5.3.2",
1489 + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
1490 + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
1491 + "dev": true,
1492 + "license": "MIT",
1493 + "peerDependencies": {
1494 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1495 + }
1496 + },
1497 + "node_modules/astring": {
1498 + "version": "1.9.0",
1499 + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
1500 + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",
1501 + "dev": true,
1502 + "license": "MIT",
1503 + "bin": {
1504 + "astring": "bin/astring"
1505 + }
1506 + },
1507 + "node_modules/bail": {
1508 + "version": "2.0.2",
1509 + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
1510 + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
1511 + "dev": true,
1512 + "license": "MIT",
1513 + "funding": {
1514 + "type": "github",
1515 + "url": "https://github.com/sponsors/wooorm"
1516 + }
1517 + },
1282 1518 "node_modules/baseline-browser-mapping": {
1283 1519 "version": "2.11.13",
1284 1520 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz",
@@ -1347,6 +1583,92 @@
1347 1583 ],
1348 1584 "license": "CC-BY-4.0"
1349 1585 },
1586 + "node_modules/ccount": {
1587 + "version": "2.0.1",
1588 + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
1589 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
1590 + "dev": true,
1591 + "license": "MIT",
1592 + "funding": {
1593 + "type": "github",
1594 + "url": "https://github.com/sponsors/wooorm"
1595 + }
1596 + },
1597 + "node_modules/character-entities": {
1598 + "version": "2.0.2",
1599 + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
1600 + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
1601 + "dev": true,
1602 + "license": "MIT",
1603 + "funding": {
1604 + "type": "github",
1605 + "url": "https://github.com/sponsors/wooorm"
1606 + }
1607 + },
1608 + "node_modules/character-entities-html4": {
1609 + "version": "2.1.0",
1610 + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
1611 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
1612 + "dev": true,
1613 + "license": "MIT",
1614 + "funding": {
1615 + "type": "github",
1616 + "url": "https://github.com/sponsors/wooorm"
1617 + }
1618 + },
1619 + "node_modules/character-entities-legacy": {
1620 + "version": "3.0.0",
1621 + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
1622 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
1623 + "dev": true,
1624 + "license": "MIT",
1625 + "funding": {
1626 + "type": "github",
1627 + "url": "https://github.com/sponsors/wooorm"
1628 + }
1629 + },
1630 + "node_modules/character-reference-invalid": {
1631 + "version": "2.0.1",
1632 + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
1633 + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
1634 + "dev": true,
1635 + "license": "MIT",
1636 + "funding": {
1637 + "type": "github",
1638 + "url": "https://github.com/sponsors/wooorm"
1639 + }
1640 + },
1641 + "node_modules/clsx": {
1642 + "version": "2.1.1",
1643 + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1644 + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1645 + "license": "MIT",
1646 + "engines": {
1647 + "node": ">=6"
1648 + }
1649 + },
1650 + "node_modules/collapse-white-space": {
1651 + "version": "2.1.0",
1652 + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz",
1653 + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==",
1654 + "dev": true,
1655 + "license": "MIT",
1656 + "funding": {
1657 + "type": "github",
1658 + "url": "https://github.com/sponsors/wooorm"
1659 + }
1660 + },
1661 + "node_modules/comma-separated-tokens": {
1662 + "version": "2.0.3",
1663 + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
1664 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
1665 + "dev": true,
1666 + "license": "MIT",
1667 + "funding": {
1668 + "type": "github",
1669 + "url": "https://github.com/sponsors/wooorm"
1670 + }
1671 + },
1350 1672 "node_modules/convert-source-map": {
1351 1673 "version": "2.0.0",
1352 1674 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1372,6 +1694,44 @@
1372 1694 }
1373 1695 }
1374 1696 },
1697 + "node_modules/decode-named-character-reference": {
1698 + "version": "1.3.0",
1699 + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
1700 + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
1701 + "dev": true,
1702 + "license": "MIT",
1703 + "dependencies": {
1704 + "character-entities": "^2.0.0"
1705 + },
1706 + "funding": {
1707 + "type": "github",
1708 + "url": "https://github.com/sponsors/wooorm"
1709 + }
1710 + },
1711 + "node_modules/dequal": {
1712 + "version": "2.0.3",
1713 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
1714 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
1715 + "dev": true,
1716 + "license": "MIT",
1717 + "engines": {
1718 + "node": ">=6"
1719 + }
1720 + },
1721 + "node_modules/devlop": {
1722 + "version": "1.1.0",
1723 + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
1724 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
1725 + "dev": true,
1726 + "license": "MIT",
1727 + "dependencies": {
1728 + "dequal": "^2.0.0"
1729 + },
1730 + "funding": {
1731 + "type": "github",
1732 + "url": "https://github.com/sponsors/wooorm"
1733 + }
1734 + },
1375 1735 "node_modules/electron-to-chromium": {
1376 1736 "version": "1.5.403",
1377 1737 "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz",
@@ -1379,6 +1739,40 @@
1379 1739 "dev": true,
1380 1740 "license": "ISC"
1381 1741 },
1742 + "node_modules/esast-util-from-estree": {
1743 + "version": "2.0.0",
1744 + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz",
1745 + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==",
1746 + "dev": true,
1747 + "license": "MIT",
1748 + "dependencies": {
1749 + "@types/estree-jsx": "^1.0.0",
1750 + "devlop": "^1.0.0",
1751 + "estree-util-visit": "^2.0.0",
1752 + "unist-util-position-from-estree": "^2.0.0"
1753 + },
1754 + "funding": {
1755 + "type": "opencollective",
1756 + "url": "https://opencollective.com/unified"
1757 + }
1758 + },
1759 + "node_modules/esast-util-from-js": {
1760 + "version": "2.0.1",
1761 + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz",
1762 + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==",
1763 + "dev": true,
1764 + "license": "MIT",
1765 + "dependencies": {
1766 + "@types/estree-jsx": "^1.0.0",
1767 + "acorn": "^8.0.0",
1768 + "esast-util-from-estree": "^2.0.0",
1769 + "vfile-message": "^4.0.0"
1770 + },
1771 + "funding": {
1772 + "type": "opencollective",
1773 + "url": "https://opencollective.com/unified"
1774 + }
1775 + },
1382 1776 "node_modules/esbuild": {
1383 1777 "version": "0.25.12",
1384 1778 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -1431,6 +1825,117 @@
1431 1825 "node": ">=6"
1432 1826 }
1433 1827 },
1828 + "node_modules/estree-util-attach-comments": {
1829 + "version": "3.0.0",
1830 + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz",
1831 + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==",
1832 + "dev": true,
1833 + "license": "MIT",
1834 + "dependencies": {
1835 + "@types/estree": "^1.0.0"
1836 + },
1837 + "funding": {
1838 + "type": "opencollective",
1839 + "url": "https://opencollective.com/unified"
1840 + }
1841 + },
1842 + "node_modules/estree-util-build-jsx": {
1843 + "version": "3.0.1",
1844 + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz",
1845 + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==",
1846 + "dev": true,
1847 + "license": "MIT",
1848 + "dependencies": {
1849 + "@types/estree-jsx": "^1.0.0",
1850 + "devlop": "^1.0.0",
1851 + "estree-util-is-identifier-name": "^3.0.0",
1852 + "estree-walker": "^3.0.0"
1853 + },
1854 + "funding": {
1855 + "type": "opencollective",
1856 + "url": "https://opencollective.com/unified"
1857 + }
1858 + },
1859 + "node_modules/estree-util-is-identifier-name": {
1860 + "version": "3.0.0",
1861 + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
1862 + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
1863 + "dev": true,
1864 + "license": "MIT",
1865 + "funding": {
1866 + "type": "opencollective",
1867 + "url": "https://opencollective.com/unified"
1868 + }
1869 + },
1870 + "node_modules/estree-util-scope": {
1871 + "version": "1.0.1",
1872 + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.1.tgz",
1873 + "integrity": "sha512-B0np3dcdxqILX5e9nEi5/Fr4K7gL4oYFVPV1zRa2e9wRCbQoZZNWOZFYyoInvXUPJXBXjss+QXlWLJChDEHDkA==",
1874 + "dev": true,
1875 + "license": "MIT",
1876 + "dependencies": {
1877 + "@types/estree": "^1.0.0",
1878 + "devlop": "^1.0.0"
1879 + },
1880 + "funding": {
1881 + "type": "opencollective",
1882 + "url": "https://opencollective.com/unified"
1883 + }
1884 + },
1885 + "node_modules/estree-util-to-js": {
1886 + "version": "2.0.0",
1887 + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz",
1888 + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==",
1889 + "dev": true,
1890 + "license": "MIT",
1891 + "dependencies": {
1892 + "@types/estree-jsx": "^1.0.0",
1893 + "astring": "^1.8.0",
1894 + "source-map": "^0.7.0"
1895 + },
1896 + "funding": {
1897 + "type": "opencollective",
1898 + "url": "https://opencollective.com/unified"
1899 + }
1900 + },
1901 + "node_modules/estree-util-visit": {
1902 + "version": "2.0.0",
1903 + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz",
1904 + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==",
1905 + "dev": true,
1906 + "license": "MIT",
1907 + "dependencies": {
1908 + "@types/estree-jsx": "^1.0.0",
1909 + "@types/unist": "^3.0.0"
1910 + },
1911 + "funding": {
1912 + "type": "opencollective",
1913 + "url": "https://opencollective.com/unified"
1914 + }
1915 + },
1916 + "node_modules/estree-walker": {
1917 + "version": "3.0.3",
1918 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
1919 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
1920 + "dev": true,
1921 + "license": "MIT",
1922 + "dependencies": {
1923 + "@types/estree": "^1.0.0"
1924 + }
1925 + },
1926 + "node_modules/extend": {
1927 + "version": "3.0.2",
1928 + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
1929 + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
1930 + "dev": true,
1931 + "license": "MIT"
1932 + },
1933 + "node_modules/fancy-canvas": {
1934 + "version": "2.1.0",
1935 + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz",
1936 + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==",
1937 + "license": "MIT"
1938 + },
1434 1939 "node_modules/fdir": {
1435 1940 "version": "6.5.0",
1436 1941 "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1474,20 +1979,159 @@
1474 1979 "node": ">=6.9.0"
1475 1980 }
1476 1981 },
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==",
1982 + "node_modules/hast-util-to-estree": {
1983 + "version": "3.1.3",
1984 + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz",
1985 + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==",
1487 1986 "dev": true,
1488 1987 "license": "MIT",
1489 − "bin": {
1490 − "jsesc": "bin/jsesc"
1988 + "dependencies": {
1989 + "@types/estree": "^1.0.0",
1990 + "@types/estree-jsx": "^1.0.0",
1991 + "@types/hast": "^3.0.0",
1992 + "comma-separated-tokens": "^2.0.0",
1993 + "devlop": "^1.0.0",
1994 + "estree-util-attach-comments": "^3.0.0",
1995 + "estree-util-is-identifier-name": "^3.0.0",
1996 + "hast-util-whitespace": "^3.0.0",
1997 + "mdast-util-mdx-expression": "^2.0.0",
1998 + "mdast-util-mdx-jsx": "^3.0.0",
1999 + "mdast-util-mdxjs-esm": "^2.0.0",
2000 + "property-information": "^7.0.0",
2001 + "space-separated-tokens": "^2.0.0",
2002 + "style-to-js": "^1.0.0",
2003 + "unist-util-position": "^5.0.0",
2004 + "zwitch": "^2.0.0"
2005 + },
2006 + "funding": {
2007 + "type": "opencollective",
2008 + "url": "https://opencollective.com/unified"
2009 + }
2010 + },
2011 + "node_modules/hast-util-to-jsx-runtime": {
2012 + "version": "2.3.6",
2013 + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
2014 + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
2015 + "dev": true,
2016 + "license": "MIT",
2017 + "dependencies": {
2018 + "@types/estree": "^1.0.0",
2019 + "@types/hast": "^3.0.0",
2020 + "@types/unist": "^3.0.0",
2021 + "comma-separated-tokens": "^2.0.0",
2022 + "devlop": "^1.0.0",
2023 + "estree-util-is-identifier-name": "^3.0.0",
2024 + "hast-util-whitespace": "^3.0.0",
2025 + "mdast-util-mdx-expression": "^2.0.0",
2026 + "mdast-util-mdx-jsx": "^3.0.0",
2027 + "mdast-util-mdxjs-esm": "^2.0.0",
2028 + "property-information": "^7.0.0",
2029 + "space-separated-tokens": "^2.0.0",
2030 + "style-to-js": "^1.0.0",
2031 + "unist-util-position": "^5.0.0",
2032 + "vfile-message": "^4.0.0"
2033 + },
2034 + "funding": {
2035 + "type": "opencollective",
2036 + "url": "https://opencollective.com/unified"
2037 + }
2038 + },
2039 + "node_modules/hast-util-whitespace": {
2040 + "version": "3.0.0",
2041 + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
2042 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
2043 + "dev": true,
2044 + "license": "MIT",
2045 + "dependencies": {
2046 + "@types/hast": "^3.0.0"
2047 + },
2048 + "funding": {
2049 + "type": "opencollective",
2050 + "url": "https://opencollective.com/unified"
2051 + }
2052 + },
2053 + "node_modules/inline-style-parser": {
2054 + "version": "0.2.7",
2055 + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
2056 + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
2057 + "dev": true,
2058 + "license": "MIT"
2059 + },
2060 + "node_modules/is-alphabetical": {
2061 + "version": "2.0.1",
2062 + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
2063 + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
2064 + "dev": true,
2065 + "license": "MIT",
2066 + "funding": {
2067 + "type": "github",
2068 + "url": "https://github.com/sponsors/wooorm"
2069 + }
2070 + },
2071 + "node_modules/is-alphanumerical": {
2072 + "version": "2.0.1",
2073 + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
2074 + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
2075 + "dev": true,
2076 + "license": "MIT",
2077 + "dependencies": {
2078 + "is-alphabetical": "^2.0.0",
2079 + "is-decimal": "^2.0.0"
2080 + },
2081 + "funding": {
2082 + "type": "github",
2083 + "url": "https://github.com/sponsors/wooorm"
2084 + }
2085 + },
2086 + "node_modules/is-decimal": {
2087 + "version": "2.0.1",
2088 + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
2089 + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
2090 + "dev": true,
2091 + "license": "MIT",
2092 + "funding": {
2093 + "type": "github",
2094 + "url": "https://github.com/sponsors/wooorm"
2095 + }
2096 + },
2097 + "node_modules/is-hexadecimal": {
2098 + "version": "2.0.1",
2099 + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
2100 + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
2101 + "dev": true,
2102 + "license": "MIT",
2103 + "funding": {
2104 + "type": "github",
2105 + "url": "https://github.com/sponsors/wooorm"
2106 + }
2107 + },
2108 + "node_modules/is-plain-obj": {
2109 + "version": "4.1.0",
2110 + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
2111 + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
2112 + "dev": true,
2113 + "license": "MIT",
2114 + "engines": {
2115 + "node": ">=12"
2116 + },
2117 + "funding": {
2118 + "url": "https://github.com/sponsors/sindresorhus"
2119 + }
2120 + },
2121 + "node_modules/js-tokens": {
2122 + "version": "4.0.0",
2123 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
2124 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
2125 + "license": "MIT"
2126 + },
2127 + "node_modules/jsesc": {
2128 + "version": "3.1.0",
2129 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
2130 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
2131 + "dev": true,
2132 + "license": "MIT",
2133 + "bin": {
2134 + "jsesc": "bin/jsesc"
1491 2135 },
1492 2136 "engines": {
1493 2137 "node": ">=6"
@@ -1506,6 +2150,26 @@
1506 2150 "node": ">=6"
1507 2151 }
1508 2152 },
2153 + "node_modules/lightweight-charts": {
2154 + "version": "4.2.3",
2155 + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.3.tgz",
2156 + "integrity": "sha512-5kS/2hY3wNYNzhnS8Gb+GAS07DX8GPF2YVDnd2NMC85gJVQ6RLU6YrXNgNJ6eg0AnWPwCnvaGtYmGky3HiLQEw==",
2157 + "license": "Apache-2.0",
2158 + "dependencies": {
2159 + "fancy-canvas": "2.1.0"
2160 + }
2161 + },
2162 + "node_modules/longest-streak": {
2163 + "version": "3.1.0",
2164 + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
2165 + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
2166 + "dev": true,
2167 + "license": "MIT",
2168 + "funding": {
2169 + "type": "github",
2170 + "url": "https://github.com/sponsors/wooorm"
2171 + }
2172 + },
1509 2173 "node_modules/loose-envify": {
1510 2174 "version": "1.4.0",
1511 2175 "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -1518,15 +2182,831 @@
1518 2182 "loose-envify": "cli.js"
1519 2183 }
1520 2184 },
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==",
2185 + "node_modules/lru-cache": {
2186 + "version": "5.1.1",
2187 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
2188 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
2189 + "dev": true,
2190 + "license": "ISC",
2191 + "dependencies": {
2192 + "yallist": "^3.0.2"
2193 + }
2194 + },
2195 + "node_modules/markdown-extensions": {
2196 + "version": "2.0.0",
2197 + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz",
2198 + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==",
2199 + "dev": true,
2200 + "license": "MIT",
2201 + "engines": {
2202 + "node": ">=16"
2203 + },
2204 + "funding": {
2205 + "url": "https://github.com/sponsors/sindresorhus"
2206 + }
2207 + },
2208 + "node_modules/mdast-util-from-markdown": {
2209 + "version": "2.0.3",
2210 + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
2211 + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==",
2212 + "dev": true,
2213 + "license": "MIT",
2214 + "dependencies": {
2215 + "@types/mdast": "^4.0.0",
2216 + "@types/unist": "^3.0.0",
2217 + "decode-named-character-reference": "^1.0.0",
2218 + "devlop": "^1.0.0",
2219 + "mdast-util-to-string": "^4.0.0",
2220 + "micromark": "^4.0.0",
2221 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
2222 + "micromark-util-decode-string": "^2.0.0",
2223 + "micromark-util-normalize-identifier": "^2.0.0",
2224 + "micromark-util-symbol": "^2.0.0",
2225 + "micromark-util-types": "^2.0.0",
2226 + "unist-util-stringify-position": "^4.0.0"
2227 + },
2228 + "funding": {
2229 + "type": "opencollective",
2230 + "url": "https://opencollective.com/unified"
2231 + }
2232 + },
2233 + "node_modules/mdast-util-mdx": {
2234 + "version": "3.0.0",
2235 + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz",
2236 + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==",
2237 + "dev": true,
2238 + "license": "MIT",
2239 + "dependencies": {
2240 + "mdast-util-from-markdown": "^2.0.0",
2241 + "mdast-util-mdx-expression": "^2.0.0",
2242 + "mdast-util-mdx-jsx": "^3.0.0",
2243 + "mdast-util-mdxjs-esm": "^2.0.0",
2244 + "mdast-util-to-markdown": "^2.0.0"
2245 + },
2246 + "funding": {
2247 + "type": "opencollective",
2248 + "url": "https://opencollective.com/unified"
2249 + }
2250 + },
2251 + "node_modules/mdast-util-mdx-expression": {
2252 + "version": "2.0.1",
2253 + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
2254 + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
2255 + "dev": true,
2256 + "license": "MIT",
2257 + "dependencies": {
2258 + "@types/estree-jsx": "^1.0.0",
2259 + "@types/hast": "^3.0.0",
2260 + "@types/mdast": "^4.0.0",
2261 + "devlop": "^1.0.0",
2262 + "mdast-util-from-markdown": "^2.0.0",
2263 + "mdast-util-to-markdown": "^2.0.0"
2264 + },
2265 + "funding": {
2266 + "type": "opencollective",
2267 + "url": "https://opencollective.com/unified"
2268 + }
2269 + },
2270 + "node_modules/mdast-util-mdx-jsx": {
2271 + "version": "3.2.0",
2272 + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
2273 + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
2274 + "dev": true,
2275 + "license": "MIT",
2276 + "dependencies": {
2277 + "@types/estree-jsx": "^1.0.0",
2278 + "@types/hast": "^3.0.0",
2279 + "@types/mdast": "^4.0.0",
2280 + "@types/unist": "^3.0.0",
2281 + "ccount": "^2.0.0",
2282 + "devlop": "^1.1.0",
2283 + "mdast-util-from-markdown": "^2.0.0",
2284 + "mdast-util-to-markdown": "^2.0.0",
2285 + "parse-entities": "^4.0.0",
2286 + "stringify-entities": "^4.0.0",
2287 + "unist-util-stringify-position": "^4.0.0",
2288 + "vfile-message": "^4.0.0"
2289 + },
2290 + "funding": {
2291 + "type": "opencollective",
2292 + "url": "https://opencollective.com/unified"
2293 + }
2294 + },
2295 + "node_modules/mdast-util-mdxjs-esm": {
2296 + "version": "2.0.1",
2297 + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
2298 + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
2299 + "dev": true,
2300 + "license": "MIT",
2301 + "dependencies": {
2302 + "@types/estree-jsx": "^1.0.0",
2303 + "@types/hast": "^3.0.0",
2304 + "@types/mdast": "^4.0.0",
2305 + "devlop": "^1.0.0",
2306 + "mdast-util-from-markdown": "^2.0.0",
2307 + "mdast-util-to-markdown": "^2.0.0"
2308 + },
2309 + "funding": {
2310 + "type": "opencollective",
2311 + "url": "https://opencollective.com/unified"
2312 + }
2313 + },
2314 + "node_modules/mdast-util-phrasing": {
2315 + "version": "4.1.0",
2316 + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
2317 + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
2318 + "dev": true,
2319 + "license": "MIT",
2320 + "dependencies": {
2321 + "@types/mdast": "^4.0.0",
2322 + "unist-util-is": "^6.0.0"
2323 + },
2324 + "funding": {
2325 + "type": "opencollective",
2326 + "url": "https://opencollective.com/unified"
2327 + }
2328 + },
2329 + "node_modules/mdast-util-to-hast": {
2330 + "version": "13.2.1",
2331 + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
2332 + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
2333 + "dev": true,
2334 + "license": "MIT",
2335 + "dependencies": {
2336 + "@types/hast": "^3.0.0",
2337 + "@types/mdast": "^4.0.0",
2338 + "@ungap/structured-clone": "^1.0.0",
2339 + "devlop": "^1.0.0",
2340 + "micromark-util-sanitize-uri": "^2.0.0",
2341 + "trim-lines": "^3.0.0",
2342 + "unist-util-position": "^5.0.0",
2343 + "unist-util-visit": "^5.0.0",
2344 + "vfile": "^6.0.0"
2345 + },
2346 + "funding": {
2347 + "type": "opencollective",
2348 + "url": "https://opencollective.com/unified"
2349 + }
2350 + },
2351 + "node_modules/mdast-util-to-markdown": {
2352 + "version": "2.1.2",
2353 + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
2354 + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
2355 + "dev": true,
2356 + "license": "MIT",
2357 + "dependencies": {
2358 + "@types/mdast": "^4.0.0",
2359 + "@types/unist": "^3.0.0",
2360 + "longest-streak": "^3.0.0",
2361 + "mdast-util-phrasing": "^4.0.0",
2362 + "mdast-util-to-string": "^4.0.0",
2363 + "micromark-util-classify-character": "^2.0.0",
2364 + "micromark-util-decode-string": "^2.0.0",
2365 + "unist-util-visit": "^5.0.0",
2366 + "zwitch": "^2.0.0"
2367 + },
2368 + "funding": {
2369 + "type": "opencollective",
2370 + "url": "https://opencollective.com/unified"
2371 + }
2372 + },
2373 + "node_modules/mdast-util-to-string": {
2374 + "version": "4.0.0",
2375 + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
2376 + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
2377 + "dev": true,
2378 + "license": "MIT",
2379 + "dependencies": {
2380 + "@types/mdast": "^4.0.0"
2381 + },
2382 + "funding": {
2383 + "type": "opencollective",
2384 + "url": "https://opencollective.com/unified"
2385 + }
2386 + },
2387 + "node_modules/micromark": {
2388 + "version": "4.0.2",
2389 + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
2390 + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
2391 + "dev": true,
2392 + "funding": [
2393 + {
2394 + "type": "GitHub Sponsors",
2395 + "url": "https://github.com/sponsors/unifiedjs"
2396 + },
2397 + {
2398 + "type": "OpenCollective",
2399 + "url": "https://opencollective.com/unified"
2400 + }
2401 + ],
2402 + "license": "MIT",
2403 + "dependencies": {
2404 + "@types/debug": "^4.0.0",
2405 + "debug": "^4.0.0",
2406 + "decode-named-character-reference": "^1.0.0",
2407 + "devlop": "^1.0.0",
2408 + "micromark-core-commonmark": "^2.0.0",
2409 + "micromark-factory-space": "^2.0.0",
2410 + "micromark-util-character": "^2.0.0",
2411 + "micromark-util-chunked": "^2.0.0",
2412 + "micromark-util-combine-extensions": "^2.0.0",
2413 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
2414 + "micromark-util-encode": "^2.0.0",
2415 + "micromark-util-normalize-identifier": "^2.0.0",
2416 + "micromark-util-resolve-all": "^2.0.0",
2417 + "micromark-util-sanitize-uri": "^2.0.0",
2418 + "micromark-util-subtokenize": "^2.0.0",
2419 + "micromark-util-symbol": "^2.0.0",
2420 + "micromark-util-types": "^2.0.0"
2421 + }
2422 + },
2423 + "node_modules/micromark-core-commonmark": {
2424 + "version": "2.0.3",
2425 + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
2426 + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
2427 + "dev": true,
2428 + "funding": [
2429 + {
2430 + "type": "GitHub Sponsors",
2431 + "url": "https://github.com/sponsors/unifiedjs"
2432 + },
2433 + {
2434 + "type": "OpenCollective",
2435 + "url": "https://opencollective.com/unified"
2436 + }
2437 + ],
2438 + "license": "MIT",
2439 + "dependencies": {
2440 + "decode-named-character-reference": "^1.0.0",
2441 + "devlop": "^1.0.0",
2442 + "micromark-factory-destination": "^2.0.0",
2443 + "micromark-factory-label": "^2.0.0",
2444 + "micromark-factory-space": "^2.0.0",
2445 + "micromark-factory-title": "^2.0.0",
2446 + "micromark-factory-whitespace": "^2.0.0",
2447 + "micromark-util-character": "^2.0.0",
2448 + "micromark-util-chunked": "^2.0.0",
2449 + "micromark-util-classify-character": "^2.0.0",
2450 + "micromark-util-html-tag-name": "^2.0.0",
2451 + "micromark-util-normalize-identifier": "^2.0.0",
2452 + "micromark-util-resolve-all": "^2.0.0",
2453 + "micromark-util-subtokenize": "^2.0.0",
2454 + "micromark-util-symbol": "^2.0.0",
2455 + "micromark-util-types": "^2.0.0"
2456 + }
2457 + },
2458 + "node_modules/micromark-extension-mdx-expression": {
2459 + "version": "3.0.1",
2460 + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz",
2461 + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==",
2462 + "dev": true,
2463 + "funding": [
2464 + {
2465 + "type": "GitHub Sponsors",
2466 + "url": "https://github.com/sponsors/unifiedjs"
2467 + },
2468 + {
2469 + "type": "OpenCollective",
2470 + "url": "https://opencollective.com/unified"
2471 + }
2472 + ],
2473 + "license": "MIT",
2474 + "dependencies": {
2475 + "@types/estree": "^1.0.0",
2476 + "devlop": "^1.0.0",
2477 + "micromark-factory-mdx-expression": "^2.0.0",
2478 + "micromark-factory-space": "^2.0.0",
2479 + "micromark-util-character": "^2.0.0",
2480 + "micromark-util-events-to-acorn": "^2.0.0",
2481 + "micromark-util-symbol": "^2.0.0",
2482 + "micromark-util-types": "^2.0.0"
2483 + }
2484 + },
2485 + "node_modules/micromark-extension-mdx-jsx": {
2486 + "version": "3.0.2",
2487 + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz",
2488 + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==",
2489 + "dev": true,
2490 + "license": "MIT",
2491 + "dependencies": {
2492 + "@types/estree": "^1.0.0",
2493 + "devlop": "^1.0.0",
2494 + "estree-util-is-identifier-name": "^3.0.0",
2495 + "micromark-factory-mdx-expression": "^2.0.0",
2496 + "micromark-factory-space": "^2.0.0",
2497 + "micromark-util-character": "^2.0.0",
2498 + "micromark-util-events-to-acorn": "^2.0.0",
2499 + "micromark-util-symbol": "^2.0.0",
2500 + "micromark-util-types": "^2.0.0",
2501 + "vfile-message": "^4.0.0"
2502 + },
2503 + "funding": {
2504 + "type": "opencollective",
2505 + "url": "https://opencollective.com/unified"
2506 + }
2507 + },
2508 + "node_modules/micromark-extension-mdx-md": {
2509 + "version": "2.0.0",
2510 + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz",
2511 + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==",
2512 + "dev": true,
2513 + "license": "MIT",
2514 + "dependencies": {
2515 + "micromark-util-types": "^2.0.0"
2516 + },
2517 + "funding": {
2518 + "type": "opencollective",
2519 + "url": "https://opencollective.com/unified"
2520 + }
2521 + },
2522 + "node_modules/micromark-extension-mdxjs": {
2523 + "version": "3.0.0",
2524 + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz",
2525 + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==",
2526 + "dev": true,
2527 + "license": "MIT",
2528 + "dependencies": {
2529 + "acorn": "^8.0.0",
2530 + "acorn-jsx": "^5.0.0",
2531 + "micromark-extension-mdx-expression": "^3.0.0",
2532 + "micromark-extension-mdx-jsx": "^3.0.0",
2533 + "micromark-extension-mdx-md": "^2.0.0",
2534 + "micromark-extension-mdxjs-esm": "^3.0.0",
2535 + "micromark-util-combine-extensions": "^2.0.0",
2536 + "micromark-util-types": "^2.0.0"
2537 + },
2538 + "funding": {
2539 + "type": "opencollective",
2540 + "url": "https://opencollective.com/unified"
2541 + }
2542 + },
2543 + "node_modules/micromark-extension-mdxjs-esm": {
2544 + "version": "3.0.0",
2545 + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz",
2546 + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==",
2547 + "dev": true,
2548 + "license": "MIT",
2549 + "dependencies": {
2550 + "@types/estree": "^1.0.0",
2551 + "devlop": "^1.0.0",
2552 + "micromark-core-commonmark": "^2.0.0",
2553 + "micromark-util-character": "^2.0.0",
2554 + "micromark-util-events-to-acorn": "^2.0.0",
2555 + "micromark-util-symbol": "^2.0.0",
2556 + "micromark-util-types": "^2.0.0",
2557 + "unist-util-position-from-estree": "^2.0.0",
2558 + "vfile-message": "^4.0.0"
2559 + },
2560 + "funding": {
2561 + "type": "opencollective",
2562 + "url": "https://opencollective.com/unified"
2563 + }
2564 + },
2565 + "node_modules/micromark-factory-destination": {
2566 + "version": "2.0.1",
2567 + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
2568 + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
2569 + "dev": true,
2570 + "funding": [
2571 + {
2572 + "type": "GitHub Sponsors",
2573 + "url": "https://github.com/sponsors/unifiedjs"
2574 + },
2575 + {
2576 + "type": "OpenCollective",
2577 + "url": "https://opencollective.com/unified"
2578 + }
2579 + ],
2580 + "license": "MIT",
2581 + "dependencies": {
2582 + "micromark-util-character": "^2.0.0",
2583 + "micromark-util-symbol": "^2.0.0",
2584 + "micromark-util-types": "^2.0.0"
2585 + }
2586 + },
2587 + "node_modules/micromark-factory-label": {
2588 + "version": "2.0.1",
2589 + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
2590 + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
2591 + "dev": true,
2592 + "funding": [
2593 + {
2594 + "type": "GitHub Sponsors",
2595 + "url": "https://github.com/sponsors/unifiedjs"
2596 + },
2597 + {
2598 + "type": "OpenCollective",
2599 + "url": "https://opencollective.com/unified"
2600 + }
2601 + ],
2602 + "license": "MIT",
2603 + "dependencies": {
2604 + "devlop": "^1.0.0",
2605 + "micromark-util-character": "^2.0.0",
2606 + "micromark-util-symbol": "^2.0.0",
2607 + "micromark-util-types": "^2.0.0"
2608 + }
2609 + },
2610 + "node_modules/micromark-factory-mdx-expression": {
2611 + "version": "2.0.3",
2612 + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz",
2613 + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==",
2614 + "dev": true,
2615 + "funding": [
2616 + {
2617 + "type": "GitHub Sponsors",
2618 + "url": "https://github.com/sponsors/unifiedjs"
2619 + },
2620 + {
2621 + "type": "OpenCollective",
2622 + "url": "https://opencollective.com/unified"
2623 + }
2624 + ],
2625 + "license": "MIT",
2626 + "dependencies": {
2627 + "@types/estree": "^1.0.0",
2628 + "devlop": "^1.0.0",
2629 + "micromark-factory-space": "^2.0.0",
2630 + "micromark-util-character": "^2.0.0",
2631 + "micromark-util-events-to-acorn": "^2.0.0",
2632 + "micromark-util-symbol": "^2.0.0",
2633 + "micromark-util-types": "^2.0.0",
2634 + "unist-util-position-from-estree": "^2.0.0",
2635 + "vfile-message": "^4.0.0"
2636 + }
2637 + },
2638 + "node_modules/micromark-factory-space": {
2639 + "version": "2.0.1",
2640 + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
2641 + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
2642 + "dev": true,
2643 + "funding": [
2644 + {
2645 + "type": "GitHub Sponsors",
2646 + "url": "https://github.com/sponsors/unifiedjs"
2647 + },
2648 + {
2649 + "type": "OpenCollective",
2650 + "url": "https://opencollective.com/unified"
2651 + }
2652 + ],
2653 + "license": "MIT",
2654 + "dependencies": {
2655 + "micromark-util-character": "^2.0.0",
2656 + "micromark-util-types": "^2.0.0"
2657 + }
2658 + },
2659 + "node_modules/micromark-factory-title": {
2660 + "version": "2.0.1",
2661 + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
2662 + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
2663 + "dev": true,
2664 + "funding": [
2665 + {
2666 + "type": "GitHub Sponsors",
2667 + "url": "https://github.com/sponsors/unifiedjs"
2668 + },
2669 + {
2670 + "type": "OpenCollective",
2671 + "url": "https://opencollective.com/unified"
2672 + }
2673 + ],
2674 + "license": "MIT",
2675 + "dependencies": {
2676 + "micromark-factory-space": "^2.0.0",
2677 + "micromark-util-character": "^2.0.0",
2678 + "micromark-util-symbol": "^2.0.0",
2679 + "micromark-util-types": "^2.0.0"
2680 + }
2681 + },
2682 + "node_modules/micromark-factory-whitespace": {
2683 + "version": "2.0.1",
2684 + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
2685 + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
2686 + "dev": true,
2687 + "funding": [
2688 + {
2689 + "type": "GitHub Sponsors",
2690 + "url": "https://github.com/sponsors/unifiedjs"
2691 + },
2692 + {
2693 + "type": "OpenCollective",
2694 + "url": "https://opencollective.com/unified"
2695 + }
2696 + ],
2697 + "license": "MIT",
2698 + "dependencies": {
2699 + "micromark-factory-space": "^2.0.0",
2700 + "micromark-util-character": "^2.0.0",
2701 + "micromark-util-symbol": "^2.0.0",
2702 + "micromark-util-types": "^2.0.0"
2703 + }
2704 + },
2705 + "node_modules/micromark-util-character": {
2706 + "version": "2.1.1",
2707 + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
2708 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
2709 + "dev": true,
2710 + "funding": [
2711 + {
2712 + "type": "GitHub Sponsors",
2713 + "url": "https://github.com/sponsors/unifiedjs"
2714 + },
2715 + {
2716 + "type": "OpenCollective",
2717 + "url": "https://opencollective.com/unified"
2718 + }
2719 + ],
2720 + "license": "MIT",
2721 + "dependencies": {
2722 + "micromark-util-symbol": "^2.0.0",
2723 + "micromark-util-types": "^2.0.0"
2724 + }
2725 + },
2726 + "node_modules/micromark-util-chunked": {
2727 + "version": "2.0.1",
2728 + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
2729 + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
2730 + "dev": true,
2731 + "funding": [
2732 + {
2733 + "type": "GitHub Sponsors",
2734 + "url": "https://github.com/sponsors/unifiedjs"
2735 + },
2736 + {
2737 + "type": "OpenCollective",
2738 + "url": "https://opencollective.com/unified"
2739 + }
2740 + ],
2741 + "license": "MIT",
2742 + "dependencies": {
2743 + "micromark-util-symbol": "^2.0.0"
2744 + }
2745 + },
2746 + "node_modules/micromark-util-classify-character": {
2747 + "version": "2.0.1",
2748 + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
2749 + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
2750 + "dev": true,
2751 + "funding": [
2752 + {
2753 + "type": "GitHub Sponsors",
2754 + "url": "https://github.com/sponsors/unifiedjs"
2755 + },
2756 + {
2757 + "type": "OpenCollective",
2758 + "url": "https://opencollective.com/unified"
2759 + }
2760 + ],
2761 + "license": "MIT",
2762 + "dependencies": {
2763 + "micromark-util-character": "^2.0.0",
2764 + "micromark-util-symbol": "^2.0.0",
2765 + "micromark-util-types": "^2.0.0"
2766 + }
2767 + },
2768 + "node_modules/micromark-util-combine-extensions": {
2769 + "version": "2.0.1",
2770 + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
2771 + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
2772 + "dev": true,
2773 + "funding": [
2774 + {
2775 + "type": "GitHub Sponsors",
2776 + "url": "https://github.com/sponsors/unifiedjs"
2777 + },
2778 + {
2779 + "type": "OpenCollective",
2780 + "url": "https://opencollective.com/unified"
2781 + }
2782 + ],
2783 + "license": "MIT",
2784 + "dependencies": {
2785 + "micromark-util-chunked": "^2.0.0",
2786 + "micromark-util-types": "^2.0.0"
2787 + }
2788 + },
2789 + "node_modules/micromark-util-decode-numeric-character-reference": {
2790 + "version": "2.0.2",
2791 + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
2792 + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
2793 + "dev": true,
2794 + "funding": [
2795 + {
2796 + "type": "GitHub Sponsors",
2797 + "url": "https://github.com/sponsors/unifiedjs"
2798 + },
2799 + {
2800 + "type": "OpenCollective",
2801 + "url": "https://opencollective.com/unified"
2802 + }
2803 + ],
2804 + "license": "MIT",
2805 + "dependencies": {
2806 + "micromark-util-symbol": "^2.0.0"
2807 + }
2808 + },
2809 + "node_modules/micromark-util-decode-string": {
2810 + "version": "2.0.1",
2811 + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
2812 + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
2813 + "dev": true,
2814 + "funding": [
2815 + {
2816 + "type": "GitHub Sponsors",
2817 + "url": "https://github.com/sponsors/unifiedjs"
2818 + },
2819 + {
2820 + "type": "OpenCollective",
2821 + "url": "https://opencollective.com/unified"
2822 + }
2823 + ],
2824 + "license": "MIT",
2825 + "dependencies": {
2826 + "decode-named-character-reference": "^1.0.0",
2827 + "micromark-util-character": "^2.0.0",
2828 + "micromark-util-decode-numeric-character-reference": "^2.0.0",
2829 + "micromark-util-symbol": "^2.0.0"
2830 + }
2831 + },
2832 + "node_modules/micromark-util-encode": {
2833 + "version": "2.0.1",
2834 + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
2835 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
2836 + "dev": true,
2837 + "funding": [
2838 + {
2839 + "type": "GitHub Sponsors",
2840 + "url": "https://github.com/sponsors/unifiedjs"
2841 + },
2842 + {
2843 + "type": "OpenCollective",
2844 + "url": "https://opencollective.com/unified"
2845 + }
2846 + ],
2847 + "license": "MIT"
2848 + },
2849 + "node_modules/micromark-util-events-to-acorn": {
2850 + "version": "2.0.3",
2851 + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz",
2852 + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==",
2853 + "dev": true,
2854 + "funding": [
2855 + {
2856 + "type": "GitHub Sponsors",
2857 + "url": "https://github.com/sponsors/unifiedjs"
2858 + },
2859 + {
2860 + "type": "OpenCollective",
2861 + "url": "https://opencollective.com/unified"
2862 + }
2863 + ],
2864 + "license": "MIT",
2865 + "dependencies": {
2866 + "@types/estree": "^1.0.0",
2867 + "@types/unist": "^3.0.0",
2868 + "devlop": "^1.0.0",
2869 + "estree-util-visit": "^2.0.0",
2870 + "micromark-util-symbol": "^2.0.0",
2871 + "micromark-util-types": "^2.0.0",
2872 + "vfile-message": "^4.0.0"
2873 + }
2874 + },
2875 + "node_modules/micromark-util-html-tag-name": {
2876 + "version": "2.0.1",
2877 + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
2878 + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
2879 + "dev": true,
2880 + "funding": [
2881 + {
2882 + "type": "GitHub Sponsors",
2883 + "url": "https://github.com/sponsors/unifiedjs"
2884 + },
2885 + {
2886 + "type": "OpenCollective",
2887 + "url": "https://opencollective.com/unified"
2888 + }
2889 + ],
2890 + "license": "MIT"
2891 + },
2892 + "node_modules/micromark-util-normalize-identifier": {
2893 + "version": "2.0.1",
2894 + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
2895 + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
2896 + "dev": true,
2897 + "funding": [
2898 + {
2899 + "type": "GitHub Sponsors",
2900 + "url": "https://github.com/sponsors/unifiedjs"
2901 + },
2902 + {
2903 + "type": "OpenCollective",
2904 + "url": "https://opencollective.com/unified"
2905 + }
2906 + ],
2907 + "license": "MIT",
2908 + "dependencies": {
2909 + "micromark-util-symbol": "^2.0.0"
2910 + }
2911 + },
2912 + "node_modules/micromark-util-resolve-all": {
2913 + "version": "2.0.1",
2914 + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
2915 + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
2916 + "dev": true,
2917 + "funding": [
2918 + {
2919 + "type": "GitHub Sponsors",
2920 + "url": "https://github.com/sponsors/unifiedjs"
2921 + },
2922 + {
2923 + "type": "OpenCollective",
2924 + "url": "https://opencollective.com/unified"
2925 + }
2926 + ],
2927 + "license": "MIT",
2928 + "dependencies": {
2929 + "micromark-util-types": "^2.0.0"
2930 + }
2931 + },
2932 + "node_modules/micromark-util-sanitize-uri": {
2933 + "version": "2.0.1",
2934 + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
2935 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
2936 + "dev": true,
2937 + "funding": [
2938 + {
2939 + "type": "GitHub Sponsors",
2940 + "url": "https://github.com/sponsors/unifiedjs"
2941 + },
2942 + {
2943 + "type": "OpenCollective",
2944 + "url": "https://opencollective.com/unified"
2945 + }
2946 + ],
2947 + "license": "MIT",
2948 + "dependencies": {
2949 + "micromark-util-character": "^2.0.0",
2950 + "micromark-util-encode": "^2.0.0",
2951 + "micromark-util-symbol": "^2.0.0"
2952 + }
2953 + },
2954 + "node_modules/micromark-util-subtokenize": {
2955 + "version": "2.1.0",
2956 + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
2957 + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
2958 + "dev": true,
2959 + "funding": [
2960 + {
2961 + "type": "GitHub Sponsors",
2962 + "url": "https://github.com/sponsors/unifiedjs"
2963 + },
2964 + {
2965 + "type": "OpenCollective",
2966 + "url": "https://opencollective.com/unified"
2967 + }
2968 + ],
2969 + "license": "MIT",
2970 + "dependencies": {
2971 + "devlop": "^1.0.0",
2972 + "micromark-util-chunked": "^2.0.0",
2973 + "micromark-util-symbol": "^2.0.0",
2974 + "micromark-util-types": "^2.0.0"
2975 + }
2976 + },
2977 + "node_modules/micromark-util-symbol": {
2978 + "version": "2.0.1",
2979 + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
2980 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
2981 + "dev": true,
2982 + "funding": [
2983 + {
2984 + "type": "GitHub Sponsors",
2985 + "url": "https://github.com/sponsors/unifiedjs"
2986 + },
2987 + {
2988 + "type": "OpenCollective",
2989 + "url": "https://opencollective.com/unified"
2990 + }
2991 + ],
2992 + "license": "MIT"
2993 + },
2994 + "node_modules/micromark-util-types": {
2995 + "version": "2.0.2",
2996 + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
2997 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
1525 2998 "dev": true,
1526 − "license": "ISC",
1527 − "dependencies": {
1528 − "yallist": "^3.0.2"
1529 − }
2999 + "funding": [
3000 + {
3001 + "type": "GitHub Sponsors",
3002 + "url": "https://github.com/sponsors/unifiedjs"
3003 + },
3004 + {
3005 + "type": "OpenCollective",
3006 + "url": "https://opencollective.com/unified"
3007 + }
3008 + ],
3009 + "license": "MIT"
1530 3010 },
1531 3011 "node_modules/ms": {
1532 3012 "version": "2.1.3",
@@ -1564,6 +3044,33 @@
1564 3044 "node": ">=18"
1565 3045 }
1566 3046 },
3047 + "node_modules/parse-entities": {
3048 + "version": "4.0.2",
3049 + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
3050 + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
3051 + "dev": true,
3052 + "license": "MIT",
3053 + "dependencies": {
3054 + "@types/unist": "^2.0.0",
3055 + "character-entities-legacy": "^3.0.0",
3056 + "character-reference-invalid": "^2.0.0",
3057 + "decode-named-character-reference": "^1.0.0",
3058 + "is-alphanumerical": "^2.0.0",
3059 + "is-decimal": "^2.0.0",
3060 + "is-hexadecimal": "^2.0.0"
3061 + },
3062 + "funding": {
3063 + "type": "github",
3064 + "url": "https://github.com/sponsors/wooorm"
3065 + }
3066 + },
3067 + "node_modules/parse-entities/node_modules/@types/unist": {
3068 + "version": "2.0.11",
3069 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
3070 + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
3071 + "dev": true,
3072 + "license": "MIT"
3073 + },
1567 3074 "node_modules/picocolors": {
1568 3075 "version": "1.1.1",
1569 3076 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1584,6 +3091,35 @@
1584 3091 "url": "https://github.com/sponsors/jonschlinkert"
1585 3092 }
1586 3093 },
3094 + "node_modules/playwright": {
3095 + "version": "1.63.0",
3096 + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
3097 + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
3098 + "dev": true,
3099 + "license": "Apache-2.0",
3100 + "dependencies": {
3101 + "playwright-core": "1.63.0"
3102 + },
3103 + "bin": {
3104 + "playwright": "cli.js"
3105 + },
3106 + "engines": {
3107 + "node": ">=20"
3108 + }
3109 + },
3110 + "node_modules/playwright-core": {
3111 + "version": "1.63.0",
3112 + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
3113 + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
3114 + "dev": true,
3115 + "license": "Apache-2.0",
3116 + "bin": {
3117 + "playwright-core": "cli.js"
3118 + },
3119 + "engines": {
3120 + "node": ">=20"
3121 + }
3122 + },
1587 3123 "node_modules/postcss": {
1588 3124 "version": "8.5.26",
1589 3125 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
@@ -1613,6 +3149,30 @@
1613 3149 "node": "^10 || ^12 || >=14"
1614 3150 }
1615 3151 },
3152 + "node_modules/prism-react-renderer": {
3153 + "version": "2.4.1",
3154 + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
3155 + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
3156 + "license": "MIT",
3157 + "dependencies": {
3158 + "@types/prismjs": "^1.26.0",
3159 + "clsx": "^2.0.0"
3160 + },
3161 + "peerDependencies": {
3162 + "react": ">=16.0.0"
3163 + }
3164 + },
3165 + "node_modules/property-information": {
3166 + "version": "7.2.0",
3167 + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
3168 + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
3169 + "dev": true,
3170 + "license": "MIT",
3171 + "funding": {
3172 + "type": "github",
3173 + "url": "https://github.com/sponsors/wooorm"
3174 + }
3175 + },
1616 3176 "node_modules/react": {
1617 3177 "version": "18.3.1",
1618 3178 "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -1648,6 +3208,175 @@
1648 3208 "node": ">=0.10.0"
1649 3209 }
1650 3210 },
3211 + "node_modules/react-router": {
3212 + "version": "6.30.6",
3213 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.6.tgz",
3214 + "integrity": "sha512-5HfK7k5im7LTOB0EqCQmfvy4C13G92Ssj1VTmouTK3AJvyjKTnFuCV0vcMAD/JS+JC4DvDIBRrlAeJIFjh5VWg==",
3215 + "license": "MIT",
3216 + "dependencies": {
3217 + "@remix-run/router": "1.23.4"
3218 + },
3219 + "engines": {
3220 + "node": ">=14.0.0"
3221 + },
3222 + "peerDependencies": {
3223 + "react": ">=16.8"
3224 + }
3225 + },
3226 + "node_modules/react-router-dom": {
3227 + "version": "6.30.6",
3228 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.6.tgz",
3229 + "integrity": "sha512-0RHKZz7wwffvkU+2MFVT2NnjK44ssLEV+m0CAJaS2Ksmorrwj7WxH00jO0SOCW26/tINUnJHToXblDs33I38YQ==",
3230 + "license": "MIT",
3231 + "dependencies": {
3232 + "@remix-run/router": "1.23.4",
3233 + "react-router": "6.30.6"
3234 + },
3235 + "engines": {
3236 + "node": ">=14.0.0"
3237 + },
3238 + "peerDependencies": {
3239 + "react": ">=16.8",
3240 + "react-dom": ">=16.8"
3241 + }
3242 + },
3243 + "node_modules/recma-build-jsx": {
3244 + "version": "1.0.0",
3245 + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz",
3246 + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==",
3247 + "dev": true,
3248 + "license": "MIT",
3249 + "dependencies": {
3250 + "@types/estree": "^1.0.0",
3251 + "estree-util-build-jsx": "^3.0.0",
3252 + "vfile": "^6.0.0"
3253 + },
3254 + "funding": {
3255 + "type": "opencollective",
3256 + "url": "https://opencollective.com/unified"
3257 + }
3258 + },
3259 + "node_modules/recma-jsx": {
3260 + "version": "1.0.1",
3261 + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz",
3262 + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==",
3263 + "dev": true,
3264 + "license": "MIT",
3265 + "dependencies": {
3266 + "acorn-jsx": "^5.0.0",
3267 + "estree-util-to-js": "^2.0.0",
3268 + "recma-parse": "^1.0.0",
3269 + "recma-stringify": "^1.0.0",
3270 + "unified": "^11.0.0"
3271 + },
3272 + "funding": {
3273 + "type": "opencollective",
3274 + "url": "https://opencollective.com/unified"
3275 + },
3276 + "peerDependencies": {
3277 + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
3278 + }
3279 + },
3280 + "node_modules/recma-parse": {
3281 + "version": "1.0.0",
3282 + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz",
3283 + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==",
3284 + "dev": true,
3285 + "license": "MIT",
3286 + "dependencies": {
3287 + "@types/estree": "^1.0.0",
3288 + "esast-util-from-js": "^2.0.0",
3289 + "unified": "^11.0.0",
3290 + "vfile": "^6.0.0"
3291 + },
3292 + "funding": {
3293 + "type": "opencollective",
3294 + "url": "https://opencollective.com/unified"
3295 + }
3296 + },
3297 + "node_modules/recma-stringify": {
3298 + "version": "1.0.0",
3299 + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz",
3300 + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==",
3301 + "dev": true,
3302 + "license": "MIT",
3303 + "dependencies": {
3304 + "@types/estree": "^1.0.0",
3305 + "estree-util-to-js": "^2.0.0",
3306 + "unified": "^11.0.0",
3307 + "vfile": "^6.0.0"
3308 + },
3309 + "funding": {
3310 + "type": "opencollective",
3311 + "url": "https://opencollective.com/unified"
3312 + }
3313 + },
3314 + "node_modules/rehype-recma": {
3315 + "version": "1.0.0",
3316 + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz",
3317 + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==",
3318 + "dev": true,
3319 + "license": "MIT",
3320 + "dependencies": {
3321 + "@types/estree": "^1.0.0",
3322 + "@types/hast": "^3.0.0",
3323 + "hast-util-to-estree": "^3.0.0"
3324 + },
3325 + "funding": {
3326 + "type": "opencollective",
3327 + "url": "https://opencollective.com/unified"
3328 + }
3329 + },
3330 + "node_modules/remark-mdx": {
3331 + "version": "3.1.1",
3332 + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz",
3333 + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==",
3334 + "dev": true,
3335 + "license": "MIT",
3336 + "dependencies": {
3337 + "mdast-util-mdx": "^3.0.0",
3338 + "micromark-extension-mdxjs": "^3.0.0"
3339 + },
3340 + "funding": {
3341 + "type": "opencollective",
3342 + "url": "https://opencollective.com/unified"
3343 + }
3344 + },
3345 + "node_modules/remark-parse": {
3346 + "version": "11.0.0",
3347 + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
3348 + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
3349 + "dev": true,
3350 + "license": "MIT",
3351 + "dependencies": {
3352 + "@types/mdast": "^4.0.0",
3353 + "mdast-util-from-markdown": "^2.0.0",
3354 + "micromark-util-types": "^2.0.0",
3355 + "unified": "^11.0.0"
3356 + },
3357 + "funding": {
3358 + "type": "opencollective",
3359 + "url": "https://opencollective.com/unified"
3360 + }
3361 + },
3362 + "node_modules/remark-rehype": {
3363 + "version": "11.1.2",
3364 + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
3365 + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
3366 + "dev": true,
3367 + "license": "MIT",
3368 + "dependencies": {
3369 + "@types/hast": "^3.0.0",
3370 + "@types/mdast": "^4.0.0",
3371 + "mdast-util-to-hast": "^13.0.0",
3372 + "unified": "^11.0.0",
3373 + "vfile": "^6.0.0"
3374 + },
3375 + "funding": {
3376 + "type": "opencollective",
3377 + "url": "https://opencollective.com/unified"
3378 + }
3379 + },
1651 3380 "node_modules/rollup": {
1652 3381 "version": "4.62.4",
1653 3382 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
@@ -1713,6 +3442,16 @@
1713 3442 "semver": "bin/semver.js"
1714 3443 }
1715 3444 },
3445 + "node_modules/source-map": {
3446 + "version": "0.7.6",
3447 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
3448 + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
3449 + "dev": true,
3450 + "license": "BSD-3-Clause",
3451 + "engines": {
3452 + "node": ">= 12"
3453 + }
3454 + },
1716 3455 "node_modules/source-map-js": {
1717 3456 "version": "1.2.1",
1718 3457 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1723,6 +3462,52 @@
1723 3462 "node": ">=0.10.0"
1724 3463 }
1725 3464 },
3465 + "node_modules/space-separated-tokens": {
3466 + "version": "2.0.2",
3467 + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
3468 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
3469 + "dev": true,
3470 + "license": "MIT",
3471 + "funding": {
3472 + "type": "github",
3473 + "url": "https://github.com/sponsors/wooorm"
3474 + }
3475 + },
3476 + "node_modules/stringify-entities": {
3477 + "version": "4.0.4",
3478 + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
3479 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
3480 + "dev": true,
3481 + "license": "MIT",
3482 + "dependencies": {
3483 + "character-entities-html4": "^2.0.0",
3484 + "character-entities-legacy": "^3.0.0"
3485 + },
3486 + "funding": {
3487 + "type": "github",
3488 + "url": "https://github.com/sponsors/wooorm"
3489 + }
3490 + },
3491 + "node_modules/style-to-js": {
3492 + "version": "1.1.21",
3493 + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz",
3494 + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==",
3495 + "dev": true,
3496 + "license": "MIT",
3497 + "dependencies": {
3498 + "style-to-object": "1.0.14"
3499 + }
3500 + },
3501 + "node_modules/style-to-object": {
3502 + "version": "1.0.14",
3503 + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
3504 + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
3505 + "dev": true,
3506 + "license": "MIT",
3507 + "dependencies": {
3508 + "inline-style-parser": "0.2.7"
3509 + }
3510 + },
1726 3511 "node_modules/tinyglobby": {
1727 3512 "version": "0.2.17",
1728 3513 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
@@ -1740,6 +3525,135 @@
1740 3525 "url": "https://github.com/sponsors/SuperchupuDev"
1741 3526 }
1742 3527 },
3528 + "node_modules/trim-lines": {
3529 + "version": "3.0.1",
3530 + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
3531 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
3532 + "dev": true,
3533 + "license": "MIT",
3534 + "funding": {
3535 + "type": "github",
3536 + "url": "https://github.com/sponsors/wooorm"
3537 + }
3538 + },
3539 + "node_modules/trough": {
3540 + "version": "2.2.0",
3541 + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
3542 + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
3543 + "dev": true,
3544 + "license": "MIT",
3545 + "funding": {
3546 + "type": "github",
3547 + "url": "https://github.com/sponsors/wooorm"
3548 + }
3549 + },
3550 + "node_modules/unified": {
3551 + "version": "11.0.5",
3552 + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
3553 + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
3554 + "dev": true,
3555 + "license": "MIT",
3556 + "dependencies": {
3557 + "@types/unist": "^3.0.0",
3558 + "bail": "^2.0.0",
3559 + "devlop": "^1.0.0",
3560 + "extend": "^3.0.0",
3561 + "is-plain-obj": "^4.0.0",
3562 + "trough": "^2.0.0",
3563 + "vfile": "^6.0.0"
3564 + },
3565 + "funding": {
3566 + "type": "opencollective",
3567 + "url": "https://opencollective.com/unified"
3568 + }
3569 + },
3570 + "node_modules/unist-util-is": {
3571 + "version": "6.0.1",
3572 + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
3573 + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
3574 + "dev": true,
3575 + "license": "MIT",
3576 + "dependencies": {
3577 + "@types/unist": "^3.0.0"
3578 + },
3579 + "funding": {
3580 + "type": "opencollective",
3581 + "url": "https://opencollective.com/unified"
3582 + }
3583 + },
3584 + "node_modules/unist-util-position": {
3585 + "version": "5.0.0",
3586 + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
3587 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
3588 + "dev": true,
3589 + "license": "MIT",
3590 + "dependencies": {
3591 + "@types/unist": "^3.0.0"
3592 + },
3593 + "funding": {
3594 + "type": "opencollective",
3595 + "url": "https://opencollective.com/unified"
3596 + }
3597 + },
3598 + "node_modules/unist-util-position-from-estree": {
3599 + "version": "2.0.0",
3600 + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz",
3601 + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==",
3602 + "dev": true,
3603 + "license": "MIT",
3604 + "dependencies": {
3605 + "@types/unist": "^3.0.0"
3606 + },
3607 + "funding": {
3608 + "type": "opencollective",
3609 + "url": "https://opencollective.com/unified"
3610 + }
3611 + },
3612 + "node_modules/unist-util-stringify-position": {
3613 + "version": "4.0.0",
3614 + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
3615 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
3616 + "dev": true,
3617 + "license": "MIT",
3618 + "dependencies": {
3619 + "@types/unist": "^3.0.0"
3620 + },
3621 + "funding": {
3622 + "type": "opencollective",
3623 + "url": "https://opencollective.com/unified"
3624 + }
3625 + },
3626 + "node_modules/unist-util-visit": {
3627 + "version": "5.1.0",
3628 + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz",
3629 + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==",
3630 + "dev": true,
3631 + "license": "MIT",
3632 + "dependencies": {
3633 + "@types/unist": "^3.0.0",
3634 + "unist-util-is": "^6.0.0",
3635 + "unist-util-visit-parents": "^6.0.0"
3636 + },
3637 + "funding": {
3638 + "type": "opencollective",
3639 + "url": "https://opencollective.com/unified"
3640 + }
3641 + },
3642 + "node_modules/unist-util-visit-parents": {
3643 + "version": "6.0.2",
3644 + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
3645 + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
3646 + "dev": true,
3647 + "license": "MIT",
3648 + "dependencies": {
3649 + "@types/unist": "^3.0.0",
3650 + "unist-util-is": "^6.0.0"
3651 + },
3652 + "funding": {
3653 + "type": "opencollective",
3654 + "url": "https://opencollective.com/unified"
3655 + }
3656 + },
1743 3657 "node_modules/update-browserslist-db": {
1744 3658 "version": "1.3.0",
1745 3659 "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz",
@@ -1771,6 +3685,36 @@
1771 3685 "browserslist": ">= 4.21.0"
1772 3686 }
1773 3687 },
3688 + "node_modules/vfile": {
3689 + "version": "6.0.3",
3690 + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
3691 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
3692 + "dev": true,
3693 + "license": "MIT",
3694 + "dependencies": {
3695 + "@types/unist": "^3.0.0",
3696 + "vfile-message": "^4.0.0"
3697 + },
3698 + "funding": {
3699 + "type": "opencollective",
3700 + "url": "https://opencollective.com/unified"
3701 + }
3702 + },
3703 + "node_modules/vfile-message": {
3704 + "version": "4.0.3",
3705 + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
3706 + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
3707 + "dev": true,
3708 + "license": "MIT",
3709 + "dependencies": {
3710 + "@types/unist": "^3.0.0",
3711 + "unist-util-stringify-position": "^4.0.0"
3712 + },
3713 + "funding": {
3714 + "type": "opencollective",
3715 + "url": "https://opencollective.com/unified"
3716 + }
3717 + },
1774 3718 "node_modules/vite": {
1775 3719 "version": "6.4.3",
1776 3720 "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
@@ -1852,6 +3796,17 @@
1852 3796 "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1853 3797 "dev": true,
1854 3798 "license": "ISC"
3799 + },
3800 + "node_modules/zwitch": {
3801 + "version": "2.0.4",
3802 + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
3803 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
3804 + "dev": true,
3805 + "license": "MIT",
3806 + "funding": {
3807 + "type": "github",
3808 + "url": "https://github.com/sponsors/wooorm"
3809 + }
1855 3810 }
1856 3811 }
1857 3812 }
modified hfmarketdata/web/package.json +10 −4
@@ -1,21 +1,27 @@
1 1 {
2 2 "name": "hfmarketdata-web",
3 3 "private": true,
4 − "version": "1.0.0",
5 − "description": "HF Market Data — open high-frequency market data API platform",
4 + "version": "2.0.0",
5 + "description": "HF Market Data — open high-frequency market data API platform (site, docs, playground, dashboard)",
6 6 "author": "Simon-Pierre Boucher <contact@spboucher.ai>",
7 7 "type": "module",
8 8 "scripts": {
9 9 "dev": "vite",
10 10 "build": "vite build",
11 − "preview": "vite preview"
11 + "preview": "vite preview",
12 + "test:e2e": "playwright test"
12 13 },
13 14 "dependencies": {
14 15 "react": "^18.3.1",
15 − "react-dom": "^18.3.1"
16 + "react-dom": "^18.3.1",
17 + "react-router-dom": "^6.28.0",
18 + "lightweight-charts": "^4.2.0",
19 + "prism-react-renderer": "^2.4.0"
16 20 },
17 21 "devDependencies": {
18 22 "@vitejs/plugin-react": "^4.3.4",
23 + "@mdx-js/rollup": "^3.1.0",
24 + "@playwright/test": "^1.49.0",
19 25 "vite": "^6.0.0"
20 26 }
21 27 }
modified hfmarketdata/web/src/App.jsx +36 −481
@@ -1,486 +1,41 @@
1 −import React, { useEffect, useMemo, useState } from 'react'
2 −import {
3 − ASSET_TYPES, BAR_FIELDS, BASE_URL, CAPABILITIES, ENDPOINTS, ERRORS,
4 − OPTION_FIELDS, TIMEFRAMES,
5 − buildUrl, curlSnippet, defaultValues, jsSnippet, paramOptions, pythonSnippet,
6 −} from './spec.js'
7 −
8 −// ---------------------------------------------------------------- atoms
9 −
10 −function Kicker({ children }) {
11 − return <div className="kicker">{children}</div>
12 −}
13 −
14 −function MethodChip({ method }) {
15 − return <span className="method">{method}</span>
16 −}
17 −
18 −function CopyButton({ text, label = 'Copy' }) {
19 − const [copied, setCopied] = useState(false)
20 − const copy = () => {
21 − navigator.clipboard.writeText(text).then(() => {
22 − setCopied(true)
23 − setTimeout(() => setCopied(false), 1200)
24 − })
25 − }
26 − return <button className="tab" onClick={copy}>{copied ? 'Copied ✓' : label}</button>
27 −}
28 −
29 −// ---------------------------------------------------------------- playground
30 −
31 −function ParamInput({ p, values, onChange }) {
32 − const id = `pg-${p.name}-${Math.random().toString(36).slice(2, 6)}`
33 − const common = { id, value: values[p.name] ?? '' }
34 − return (
35 − <label className="pg-field" htmlFor={id}>
36 − <span className="pg-label">
37 − {p.name}
38 − {p.req ? <em className="pg-req">required</em> : null}
39 − <span className="pg-in">{p.in}</span>
40 − </span>
41 − {p.t === 'select' ? (
42 − <select {...common} onChange={e => onChange(p.name, e.target.value)}>
43 − {paramOptions(p, values).map(o => (
44 − <option key={o} value={o}>{o === '' ? '(default)' : o}</option>
45 − ))}
46 − </select>
47 − ) : (
48 − <input {...common} type="text" placeholder={p.ph || ''}
49 − onChange={e => onChange(p.name, e.target.value)} />
50 − )}
51 − </label>
52 − )
53 −}
54 −
55 −function Playground({ ep }) {
56 − const [values, setValues] = useState(() => defaultValues(ep))
57 − const [resp, setResp] = useState(null) // {status, ms, body, ok}
58 − const [busy, setBusy] = useState(false)
59 − const [lang, setLang] = useState('curl')
60 −
61 − const onChange = (name, v) => {
62 − setValues(prev => {
63 − const next = { ...prev, [name]: v }
64 − // keep adjustment valid when the asset changes
65 − if (name === 'asset') next.adjustment = ''
66 − return next
67 − })
68 − }
69 −
70 − const url = buildUrl(ep, values)
71 − const snippets = { curl: curlSnippet(url), Python: pythonSnippet(url), JavaScript: jsSnippet(url) }
72 −
73 − const run = async () => {
74 − setBusy(true)
75 − const t0 = performance.now()
76 − try {
77 − const r = await fetch(url)
78 − const ms = Math.round(performance.now() - t0)
79 − const text = await r.text()
80 − let body = text
81 − try { body = JSON.stringify(JSON.parse(text), null, 2) } catch { /* csv */ }
82 − if (body.length > 6000) body = body.slice(0, 6000) + '\n… (truncated)'
83 − setResp({ status: r.status, ok: r.ok, ms, body })
84 − } catch (e) {
85 − setResp({ status: '—', ok: false, ms: Math.round(performance.now() - t0), body: `Request failed: ${e.message}` })
86 − } finally {
87 − setBusy(false)
88 − }
89 − }
90 −
91 − return (
92 − <div className="playground">
93 − <div className="pg-head">
94 − <span className="pg-title">Playground</span>
95 − <span className="pg-hint">edit the parameters, the request updates live</span>
96 − </div>
97 −
98 − {ep.params.length > 0 && (
99 − <div className="pg-grid">
100 − {ep.params.map(p => (
101 − <ParamInput key={p.name} p={p} values={values} onChange={onChange} />
102 − ))}
103 − </div>
104 − )}
105 −
106 − <div className="pg-url">
107 − <span className="pg-url-method">GET</span>
108 − <code>{BASE_URL}{url}</code>
109 − </div>
110 −
111 − <div className="code-tabs-bar">
112 − <button className="tab run" onClick={run} disabled={busy}>
113 − {busy ? 'Running…' : '▶ Send request'}
114 − </button>
115 − {Object.keys(snippets).map(t => (
116 − <button key={t} className={t === lang ? 'tab active' : 'tab'}
117 − onClick={() => setLang(t)}>{t}</button>
118 − ))}
119 − <span className="pg-spacer" />
120 − <CopyButton text={`${BASE_URL}${url}`} label="Copy URL" />
121 − <CopyButton text={snippets[lang]} label="Copy code" />
122 − </div>
123 − <pre><code>{snippets[lang]}</code></pre>
124 −
125 − {resp ? (
126 − <div className="pg-response">
127 − <div className="pg-resp-bar">
128 − <span className={resp.ok ? 'pg-status ok' : 'pg-status err'}>
129 − {resp.status}
130 − </span>
131 − <span className="pg-ms">{resp.ms} ms</span>
132 − <span className="pg-resp-label">live response</span>
133 − </div>
134 − <pre className="response"><code>{resp.body}</code></pre>
135 − </div>
136 − ) : (
137 − <div className="pg-response">
138 − <div className="pg-resp-bar">
139 − <span className="pg-resp-label">example response</span>
140 − </div>
141 − <pre className="response"><code>{ep.response}</code></pre>
142 − </div>
143 − )}
144 − </div>
145 − )
146 −}
147 −
148 −// ---------------------------------------------------------------- endpoint card
149 −
150 −function FieldsTable({ kind }) {
151 − const fields = kind === 'options' ? OPTION_FIELDS : BAR_FIELDS
152 − const [open, setOpen] = useState(false)
153 − return (
154 − <div className="fields">
155 − <button className="fields-toggle" onClick={() => setOpen(!open)}>
156 − {open ? '−' : '+'} Response fields <span className="fields-n">{fields.length}</span>
157 − </button>
158 − {open && (
159 − <table className="params">
160 − <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
161 − <tbody>
162 − {fields.map(f => (
163 − <tr key={f.name}>
164 − <td><code>{f.name}</code></td>
165 − <td className="type">{f.type}</td>
166 − <td>{f.desc}</td>
167 − </tr>
168 − ))}
169 − </tbody>
170 − </table>
171 − )}
172 − </div>
173 − )
174 −}
175 −
176 −function Endpoint({ ep, index }) {
177 − return (
178 − <article className="endpoint" id={ep.id}>
179 − <div className="endpoint-index">{String(index + 1).padStart(2, '0')}</div>
180 − <div className="endpoint-body">
181 − <div className="endpoint-head">
182 − <MethodChip method={ep.method} />
183 − <code className="path">{ep.path}</code>
184 − </div>
185 − <h3>{ep.title}</h3>
186 − <p className="endpoint-desc">{ep.desc}</p>
187 − {ep.params.length > 0 && (
188 − <table className="params">
189 − <thead>
190 − <tr><th>Parameter</th><th>In</th><th></th><th>Description</th></tr>
191 − </thead>
192 − <tbody>
193 − {ep.params.map(p => (
194 − <tr key={p.name}>
195 − <td><code>{p.name}</code></td>
196 − <td className="type">{p.in}</td>
197 − <td>{p.req ? <span className="req">required</span> : <span className="opt">optional</span>}</td>
198 − <td>{p.desc}</td>
199 − </tr>
200 − ))}
201 − </tbody>
202 − </table>
203 − )}
204 − <Playground ep={ep} />
205 − {ep.fields && <FieldsTable kind={ep.fields} />}
206 − </div>
207 − </article>
208 − )
209 −}
210 −
211 −// ---------------------------------------------------------------- live status
212 −
213 −function LiveStatus() {
214 − const [status, setStatus] = useState(null)
215 − const [err, setErr] = useState(null)
216 − useEffect(() => {
217 − fetch('/v1/status')
218 − .then(r => r.json())
219 − .then(d => setStatus(d.datasets))
220 − .catch(e => setErr(e.message))
221 − }, [])
222 − if (err) return <p className="muted">Live status unavailable ({err}).</p>
223 − if (!status) return <p className="muted">Loading live dataset status…</p>
224 − const rows = []
225 − for (const [asset, tfs] of Object.entries(status)) {
226 − if (asset === 'options') continue
227 − for (const [tf, adjs] of Object.entries(tfs)) {
228 − for (const [adj, n] of Object.entries(adjs)) {
229 − rows.push({ asset, tf, adj, n })
230 − }
231 − }
232 − }
233 − const total = rows.reduce((s, r) => s + r.n, 0)
234 − return (
235 − <div>
236 − <div className="stat-chips">
237 − <div className="chip"><b>{total.toLocaleString('en-US')}</b><span>instrument files live</span></div>
238 − {status.options && (
239 − <div className="chip"><b>{status.options.quarters.length}</b><span>options quarters ({status.options.quarters[0]} → {status.options.quarters.at(-1)})</span></div>
240 − )}
241 − </div>
242 − <div className="table-scroll">
243 − <table className="params">
244 − <thead>
245 − <tr><th>Asset</th><th>Timeframe</th><th>Adjustment</th><th>Instruments</th></tr>
246 − </thead>
247 − <tbody>
248 − {rows.map((r, i) => (
249 − <tr key={i}>
250 − <td>{r.asset}</td><td>{r.tf}</td><td><code>{r.adj}</code></td>
251 − <td className="num">{r.n.toLocaleString('en-US')}</td>
252 − </tr>
253 − ))}
254 − </tbody>
255 − </table>
256 − </div>
257 − </div>
258 − )
259 −}
260 −
261 −// ---------------------------------------------------------------- app
1 +// Route table — one lazy chunk per area. Owners: web-core (home, pricing, docs, status, layout),
2 +// web-app (playground, auth, dashboard, admin), mcp-skills (integrations).
3 +import React, { Suspense, lazy } from 'react'
4 +import { Route, Routes } from 'react-router-dom'
5 +import Layout from './app/Layout.jsx'
6 +
7 +const Home = lazy(() => import('./pages/home/Home.jsx'))
8 +const Docs = lazy(() => import('./pages/docs/Docs.jsx'))
9 +const Playground = lazy(() => import('./pages/playground/Playground.jsx'))
10 +const Integrations = lazy(() => import('./pages/integrations/Integrations.jsx'))
11 +const Pricing = lazy(() => import('./pages/pricing/Pricing.jsx'))
12 +const Status = lazy(() => import('./pages/status/Status.jsx'))
13 +const Auth = lazy(() => import('./pages/auth/Auth.jsx'))
14 +const Dashboard = lazy(() => import('./pages/dashboard/Dashboard.jsx'))
15 +const Admin = lazy(() => import('./pages/admin/Admin.jsx'))
16 +const NotFound = () => <main className="page narrow"><h1>404</h1><p className="muted">This page does not exist.</p></main>
262 17
263 18 export default function App() {
264 − const grouped = useMemo(() => {
265 − const g = {}
266 − for (const e of ENDPOINTS) (g[e.tag] ||= []).push(e)
267 − return g
268 − }, [])
269 −
270 19 return (
271 − <>
272 − <div className="layout">
273 − <aside className="sidebar">
274 − <a className="brand" href="#top">
275 − <span className="logo">HF</span>
276 − <span className="brand-text">
277 − <span className="brand-name">HF Market Data</span>
278 − <span className="brand-sub">Open API · v1</span>
279 − </span>
280 − </a>
281 − <nav>
282 − <Kicker>Getting started</Kicker>
283 − <a href="#overview">Overview</a>
284 − <a href="#capabilities">What you can do</a>
285 − <a href="#quickstart">Quick start</a>
286 − <a href="#conventions">Conventions</a>
287 − <a href="#coverage">Data coverage</a>
288 − <Kicker>Endpoints — live playground</Kicker>
289 − {ENDPOINTS.map(e => (
290 − <a key={e.id} href={`#${e.id}`} className="ep-link">
291 − <span className="ep-method">{e.method}</span>{e.title}
292 − </a>
293 − ))}
294 − <Kicker>Resources</Kicker>
295 − <a href="#errors">Errors</a>
296 − <a href="#live">Live status</a>
297 − <a href="#about">About</a>
298 − <a href="/docs" target="_blank" rel="noreferrer">OpenAPI / Swagger ↗</a>
299 − </nav>
300 − </aside>
301 −
302 − <main id="top">
303 − <section className="hero" id="overview">
304 − <Kicker>Open high-frequency market data</Kicker>
305 − <h1>
306 − Market data,<br />
307 − <em className="accent-word">wide open.</em>
308 − </h1>
309 − <p className="lede">
310 − A free, keyless REST API over a <strong>26.5-billion-row</strong> historical
311 − archive: <strong>1-minute to daily bars</strong> for stocks, ETFs, futures,
312 − crypto, indices and FX — and <strong>complete end-of-day options
313 − chains</strong> with quotes, implied volatility and Greeks back to 2010.
314 − Every endpoint below is a live playground.
315 − </p>
316 − <div className="hero-actions">
317 − <a className="btn" href="#bars">Try the playground</a>
318 − <a className="btn ghost" href="/docs" target="_blank" rel="noreferrer">OpenAPI spec ↗</a>
319 − </div>
320 − <div className="stat-chips hero-chips">
321 − <div className="chip"><b>26.5B</b><span>rows in the lake</span></div>
322 − <div className="chip"><b>7</b><span>asset classes</span></div>
323 − <div className="chip"><b>5</b><span>timeframes — 1min → 1day</span></div>
324 − <div className="chip"><b>5,800+</b><span>options underlyings</span></div>
325 − <div className="chip"><b>66</b><span>options quarters since 2010</span></div>
326 − </div>
327 − </section>
328 −
329 − <section id="capabilities">
330 − <Kicker>01 — Getting started</Kicker>
331 − <h2>What you can do</h2>
332 − <div className="conv-grid">
333 − {CAPABILITIES.map(c => (
334 − <div className="conv" key={c.title}>
335 − <h4>{c.title}</h4>
336 − <p>{c.desc}</p>
337 − </div>
338 − ))}
339 − </div>
340 − </section>
341 −
342 − <section id="quickstart">
343 − <Kicker>02 — Getting started</Kicker>
344 − <h2>Quick start</h2>
345 − <p>
346 − No key, no registration, no SDK required. Every endpoint is a plain
347 − GET on <code>{BASE_URL}</code> returning JSON — or CSV when you ask for it.
348 − </p>
349 − <pre><code>{`# Daily AAPL bars, split+dividend adjusted
350 −curl "${BASE_URL}/v1/bars/stock/AAPL?timeframe=1day&start=2024-01-01"
351 −
352 −# A precise 4-hour window of 1-minute bars, across three tickers at once
353 −curl "${BASE_URL}/v1/bars/stock?tickers=AAPL,MSFT,NVDA&timeframe=1min\\
354 − &start=2024-06-03%2009:30:00&end=2024-06-03%2013:30:00"
355 −
356 −# The whole watchlist at one exact moment
357 −curl "${BASE_URL}/v1/snapshot/stock?tickers=AAPL,MSFT,NVDA&at=2024-06-03%2010:35:00"
358 −
359 −# Full AAPL option chain — quotes, IV, Greeks
360 −curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
361 − <div className="callout">
362 − <b>Response envelope.</b>&nbsp; Every JSON data endpoint returns
363 − <code>{'{ "count": n, "data": [ … ] }'}</code>. CSV responses stream a
364 − header row followed by the records — ideal for <code>pandas.read_csv</code> straight from the URL.
365 − </div>
366 − </section>
367 −
368 − <section id="conventions">
369 − <Kicker>03 — Getting started</Kicker>
370 − <h2>Conventions</h2>
371 − <div className="conv-grid">
372 − <div className="conv">
373 − <h4>Timeframes</h4>
374 − <p>{TIMEFRAMES.join(' · ')}. Intraday timestamps are <b>US Eastern Time</b>; daily bars carry the session date. Zero-volume bars are excluded.</p>
375 − </div>
376 − <div className="conv">
377 − <h4>Adjustments</h4>
378 − <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>
379 − </div>
380 − <div className="conv">
381 − <h4>Formats & limits</h4>
382 − <p><code>format=json</code> (default, 50k-row cap) or <code>format=csv</code> (2M-row cap) on every data endpoint. Filter with <code>start</code>/<code>end</code>, page with <code>limit</code> + <code>order</code>.</p>
383 − </div>
384 − <div className="conv">
385 − <h4>Options snapshots</h4>
386 − <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>
387 − </div>
388 − </div>
389 − </section>
390 −
391 − <section id="coverage">
392 − <Kicker>04 — Getting started</Kicker>
393 − <h2>Data coverage</h2>
394 − <div className="cards">
395 − {ASSET_TYPES.map(a => (
396 − <div className="card" key={a.id}>
397 − <div className="card-head">
398 − <h3>{a.name}</h3>
399 − <code className="card-ex">{a.example}</code>
400 − </div>
401 − <p>{a.desc}</p>
402 − <p className="muted small">{a.adjustments.map(x => <code key={x}>{x}</code>)}</p>
403 − </div>
404 − ))}
405 − <div className="card accent">
406 − <div className="card-head">
407 − <h3>Options</h3>
408 − <code className="card-ex">AAPL 200c</code>
409 − </div>
410 − <p>End-of-day chains for 5,800+ US equities & indices since 2010 —
411 − last price, bid/ask, bid/ask IV, open interest, volume,
412 − delta, gamma, vega, theta, rho.</p>
413 − </div>
414 − </div>
415 − </section>
416 −
417 − <section id="reference">
418 − {Object.entries(grouped).map(([tag, eps]) => (
419 − <div key={tag} className="ep-group">
420 − <Kicker>API reference — live playground</Kicker>
421 − <h2>{tag}</h2>
422 − {eps.map(e => (
423 − <Endpoint key={e.id} ep={e}
424 − index={ENDPOINTS.findIndex(x => x.id === e.id)} />
425 − ))}
426 − </div>
427 − ))}
428 − </section>
429 −
430 − <section id="errors">
431 − <Kicker>Resources</Kicker>
432 − <h2>Errors</h2>
433 − <p>
434 − Errors are plain HTTP status codes with a JSON body:&nbsp;
435 − <code>{'{ "detail": "…" }'}</code>. The detail message always names the
436 − invalid parameter and lists the valid values.
437 − </p>
438 − <table className="params">
439 − <thead><tr><th>Code</th><th>Meaning</th><th>When</th></tr></thead>
440 − <tbody>
441 − {ERRORS.map(e => (
442 − <tr key={e.code}>
443 − <td><code>{e.code}</code></td>
444 − <td>{e.meaning}</td>
445 − <td>{e.desc}</td>
446 − </tr>
447 − ))}
448 − </tbody>
449 − </table>
450 − </section>
451 −
452 − <section id="live">
453 − <Kicker>Resources</Kicker>
454 − <h2>Live dataset status</h2>
455 − <LiveStatus />
456 − </section>
457 −
458 − <section id="about" className="about">
459 − <Kicker>Resources</Kicker>
460 − <h2>About</h2>
461 − <p>
462 − HF Market Data is built and maintained by <strong>Simon-Pierre
463 − Boucher</strong>. Under the hood it is a DuckDB-over-Parquet data
464 − lake: every instrument is a zstd-compressed Parquet file queried
465 − in place with predicate pushdown — no database server between you
466 − and the data, which is what keeps responses fast across 26.5
467 − billion rows.
468 − </p>
469 − <p>
470 − Contact — <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>
471 − </p>
472 − </section>
473 −
474 − <footer>
475 − <div className="foot-brand">HF Market Data</div>
476 − <div>
477 − © {new Date().getFullYear()} Simon-Pierre Boucher ·{' '}
478 − <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a> ·{' '}
479 − <a href="/docs" target="_blank" rel="noreferrer">OpenAPI</a>
480 − </div>
481 − </footer>
482 − </main>
483 − </div>
484 − </>
20 + <Layout>
21 + <Suspense fallback={<div className="page-loading" aria-busy="true" />}>
22 + <Routes>
23 + <Route path="/" element={<Home />} />
24 + <Route path="/docs/*" element={<Docs />} />
25 + <Route path="/playground" element={<Playground />} />
26 + <Route path="/integrations/*" element={<Integrations />} />
27 + <Route path="/pricing" element={<Pricing />} />
28 + <Route path="/status" element={<Status />} />
29 + <Route path="/signin" element={<Auth mode="signin" />} />
30 + <Route path="/signup" element={<Auth mode="signup" />} />
31 + <Route path="/verify" element={<Auth mode="verify" />} />
32 + <Route path="/reset" element={<Auth mode="reset" />} />
33 + <Route path="/invite" element={<Auth mode="invite" />} />
34 + <Route path="/dashboard/*" element={<Dashboard />} />
35 + <Route path="/admin/*" element={<Admin />} />
36 + <Route path="*" element={<NotFound />} />
37 + </Routes>
38 + </Suspense>
39 + </Layout>
485 40 )
486 41 }
added hfmarketdata/web/src/app/Layout.jsx +36 −0
@@ -0,0 +1,36 @@
1 +import React from 'react'
2 +import { Link, NavLink } from 'react-router-dom'
3 +import { useAuth } from './auth.jsx'
4 +import { CONTACT_EMAIL } from './api.js'
5 +
6 +const NAV = [
7 + ['/docs', 'Docs'], ['/playground', 'Playground'], ['/integrations', 'Integrations'],
8 + ['/pricing', 'Pricing / Limits'], ['/status', 'Status'],
9 +]
10 +
11 +export default function Layout({ children }) {
12 + const { user } = useAuth()
13 + return (
14 + <div className="shell">
15 + <header className="topbar">
16 + <Link to="/" className="brand" aria-label="HF Market Data home">
17 + <span className="brand-mark">HF</span><span className="brand-name">Market Data</span>
18 + </Link>
19 + <nav className="topnav" aria-label="Main">
20 + {NAV.map(([to, label]) => (
21 + <NavLink key={to} to={to} className={({ isActive }) => (isActive ? 'active' : '')}>{label}</NavLink>
22 + ))}
23 + </nav>
24 + <div className="topbar-right">
25 + {user ? <Link to="/dashboard" className="btn btn-ghost">Dashboard</Link>
26 + : <Link to="/signin" className="btn btn-ghost">Sign in</Link>}
27 + </div>
28 + </header>
29 + {children}
30 + <footer className="footer">
31 + <div>© {new Date().getFullYear()} HF Market Data · Simon-Pierre Boucher · <a href={`mailto:${CONTACT_EMAIL}`}>{CONTACT_EMAIL}</a></div>
32 + <div className="muted">Data: FirstRate Data · SEC EDGAR · UTC / ISO 8601 everywhere</div>
33 + </footer>
34 + </div>
35 + )
36 +}
added hfmarketdata/web/src/app/api.js +53 −0
@@ -0,0 +1,53 @@
1 +// Tiny API client shared by every page. Injects the session key (dashboard playground) when present,
2 +// exposes rate-limit headers on every result, never logs keys.
3 +export const BASE_URL = import.meta.env.VITE_API_BASE || ''
4 +export const PUBLIC_BASE = 'https://www.hfmarketdata.io'
5 +export const CONTACT_EMAIL = 'contact@spboucher.ai'
6 +
7 +export const TIERS = [
8 + { id: 'keyless', name: 'Keyless (per IP)', window: 'hour', requests: 30, rows: 100_000, maxRows: 5_000 },
9 + { id: 'free', name: 'Free account + API key', window: 'minute', requests: 120, rows: 1_000_000, maxRows: 50_000 },
10 + { id: 'high_usage', name: 'High usage (on request)', window: 'minute', requests: 600, rows: 10_000_000, maxRows: 200_000 },
11 +]
12 +
13 +export function rateHeaders(res) {
14 + const h = res.headers
15 + const num = k => (h.get(k) == null ? null : Number(h.get(k)))
16 + return {
17 + limitRequests: num('x-ratelimit-limit-requests'),
18 + remainingRequests: num('x-ratelimit-remaining-requests'),
19 + limitRows: num('x-ratelimit-limit-rows'),
20 + remainingRows: num('x-ratelimit-remaining-rows'),
21 + reset: num('x-ratelimit-reset'),
22 + rowCount: num('x-row-count'),
23 + retryAfter: num('retry-after'),
24 + }
25 +}
26 +
27 +export async function api(path, { method = 'GET', body, apiKey, headers = {}, raw = false, signal } = {}) {
28 + const h = { Accept: 'application/json', ...headers }
29 + if (body !== undefined) h['Content-Type'] = 'application/json'
30 + if (apiKey) h.Authorization = `Bearer ${apiKey}`
31 + const t0 = performance.now()
32 + const res = await fetch(BASE_URL + path, { method, headers: h, body: body === undefined ? undefined : JSON.stringify(body), credentials: 'include', signal })
33 + const ms = Math.round(performance.now() - t0)
34 + const rate = rateHeaders(res)
35 + if (raw) return { res, ms, rate }
36 + const ct = res.headers.get('content-type') || ''
37 + const data = ct.includes('json') ? await res.json() : await res.text()
38 + if (!res.ok) {
39 + const err = new Error(data?.error?.message || data?.detail || `HTTP ${res.status}`)
40 + err.status = res.status
41 + err.code = data?.error?.code
42 + err.body = data
43 + err.rate = rate
44 + throw err
45 + }
46 + return { data, ms, rate, status: res.status }
47 +}
48 +
49 +export function buildUrl(path, params = {}) {
50 + const u = new URL(path, PUBLIC_BASE)
51 + Object.entries(params).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== '') u.searchParams.set(k, v) })
52 + return u.toString()
53 +}
added hfmarketdata/web/src/app/auth.jsx +29 −0
@@ -0,0 +1,29 @@
1 +// Session context: who is signed in, their tier, and the API key to inject in the authenticated playground.
2 +// Backed by cookie session endpoints under /v1/auth and /v1/me (owned by the accounts module).
3 +import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
4 +import { api } from './api.js'
5 +
6 +const Ctx = createContext({ user: null, loading: true, refresh: () => {}, signout: () => {} })
7 +
8 +export function AuthProvider({ children }) {
9 + const [user, setUser] = useState(null)
10 + const [loading, setLoading] = useState(true)
11 + const refresh = useCallback(async () => {
12 + try {
13 + const { data } = await api('/v1/me')
14 + setUser(data.data || data)
15 + } catch {
16 + setUser(null)
17 + } finally {
18 + setLoading(false)
19 + }
20 + }, [])
21 + const signout = useCallback(async () => {
22 + try { await api('/v1/auth/logout', { method: 'POST' }) } catch { /* ignore */ }
23 + setUser(null)
24 + }, [])
25 + useEffect(() => { refresh() }, [refresh])
26 + return <Ctx.Provider value={{ user, loading, refresh, signout }}>{children}</Ctx.Provider>
27 +}
28 +
29 +export const useAuth = () => useContext(Ctx)
added hfmarketdata/web/src/app/theme.css +41 −0
@@ -0,0 +1,41 @@
1 +/* HF Market Data design tokens — dark by default, one accent, mono for code. Owner: web-core. */
2 +:root {
3 + color-scheme: dark;
4 + --bg: #0b0d10; --bg-1: #12151a; --bg-2: #181c23; --line: #262b34; --line-2: #333a46;
5 + --fg: #e6e9ef; --fg-1: #aeb6c2; --fg-2: #7c8594;
6 + --accent: #5ee7a5; --accent-2: #37b3ff; --danger: #ff6b6b; --warn: #ffc857;
7 + --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
8 + --sans: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
9 + --radius: 8px; --max: 1240px;
10 +}
11 +[data-theme="light"] { color-scheme: light; --bg: #ffffff; --bg-1: #f6f7f9; --bg-2: #eef0f4; --line: #dfe3ea; --line-2: #c9cfd9; --fg: #14181f; --fg-1: #3d4654; --fg-2: #6b7482; --accent: #0f8f5a; --accent-2: #0b6fb8; }
12 +* { box-sizing: border-box; }
13 +html { font-family: var(--sans); background: var(--bg); color: var(--fg); -webkit-font-smoothing: antialiased; font-size: 15px; }
14 +body { margin: 0; }
15 +a { color: var(--accent-2); text-decoration: none; }
16 +a:hover { text-decoration: underline; }
17 +code, pre, kbd, .mono { font-family: var(--mono); font-size: 0.92em; }
18 +pre { background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; overflow: auto; }
19 +.muted { color: var(--fg-2); }
20 +.shell { min-height: 100dvh; display: flex; flex-direction: column; }
21 +.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; gap: 24px; padding: 0 20px; height: 56px; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); }
22 +.brand { display: inline-flex; align-items: center; gap: 8px; color: var(--fg); font-weight: 600; }
23 +.brand-mark { font-family: var(--mono); background: var(--accent); color: #05140c; border-radius: 6px; padding: 2px 6px; font-size: 13px; }
24 +.topnav { display: flex; gap: 4px; flex: 1; }
25 +.topnav a { color: var(--fg-1); padding: 6px 10px; border-radius: 6px; }
26 +.topnav a.active, .topnav a:hover { color: var(--fg); background: var(--bg-2); text-decoration: none; }
27 +.btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: var(--radius); border: 1px solid var(--line-2); background: var(--bg-1); color: var(--fg); cursor: pointer; font: inherit; }
28 +.btn:hover { border-color: var(--fg-2); text-decoration: none; }
29 +.btn-primary { background: var(--accent); color: #05140c; border-color: var(--accent); font-weight: 600; }
30 +.btn-ghost { background: transparent; }
31 +.page { width: 100%; max-width: var(--max); margin: 0 auto; padding: 40px 20px 80px; flex: 1; }
32 +.page.narrow { max-width: 760px; }
33 +.page-loading { flex: 1; min-height: 40vh; }
34 +.footer { border-top: 1px solid var(--line); padding: 24px 20px; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; color: var(--fg-1); font-size: 13px; }
35 +.card { background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px; }
36 +table { border-collapse: collapse; width: 100%; }
37 +th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
38 +th { color: var(--fg-1); font-weight: 600; font-size: 13px; }
39 +input, select, textarea { font: inherit; color: var(--fg); background: var(--bg-1); border: 1px solid var(--line-2); border-radius: 6px; padding: 8px 10px; }
40 +input:focus, select:focus, textarea:focus { outline: 2px solid color-mix(in srgb, var(--accent) 50%, transparent); border-color: var(--accent); }
41 +@media (max-width: 800px) { .topnav { display: none; } .topbar { gap: 12px; } }
renamed hfmarketdata/web/src/spec.js → hfmarketdata/web/src/legacy-spec.js +0 −0
modified hfmarketdata/web/src/main.jsx +10 −4
@@ -1,10 +1,16 @@
1 1 import React from 'react'
2 −import ReactDOM from 'react-dom/client'
2 +import { createRoot } from 'react-dom/client'
3 +import { BrowserRouter } from 'react-router-dom'
3 4 import App from './App.jsx'
4 −import './styles.css'
5 +import { AuthProvider } from './app/auth.jsx'
6 +import './app/theme.css'
5 7
6 −ReactDOM.createRoot(document.getElementById('root')).render(
8 +createRoot(document.getElementById('root')).render(
7 9 <React.StrictMode>
8 − <App />
10 + <BrowserRouter>
11 + <AuthProvider>
12 + <App />
13 + </AuthProvider>
14 + </BrowserRouter>
9 15 </React.StrictMode>,
10 16 )
added hfmarketdata/web/src/pages/admin/Admin.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Admin() {
4 + return <main className="page"><h1>Admin</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/auth/Auth.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by web-app. Props: mode = signin | signup | verify | reset | invite
3 +export default function Auth({ mode }) {
4 + return <main className="page narrow"><h1>{mode}</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/dashboard/Dashboard.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Dashboard() {
4 + return <main className="page"><h1>Dashboard</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/docs/Docs.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Docs() {
4 + return <main className="page"><h1>Docs</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/home/Home.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Home() {
4 + return <main className="page"><h1>Home</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/integrations/Integrations.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Integrations() {
4 + return <main className="page"><h1>Integrations</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/playground/Playground.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Playground() {
4 + return <main className="page"><h1>Playground</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/pricing/Pricing.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Pricing() {
4 + return <main className="page"><h1>Pricing</h1><p className="muted">Coming soon.</p></main>
5 +}
added hfmarketdata/web/src/pages/status/Status.jsx +5 −0
@@ -0,0 +1,5 @@
1 +import React from 'react'
2 +// Placeholder — implemented by the owning agent (see App.jsx header). Keep the default export name.
3 +export default function Status() {
4 + return <main className="page"><h1>Status</h1><p className="muted">Coming soon.</p></main>
5 +}
modified hfmarketdata/web/vite.config.js +17 −2
@@ -1,12 +1,27 @@
1 1 import { defineConfig } from 'vite'
2 2 import react from '@vitejs/plugin-react'
3 +import mdx from '@mdx-js/rollup'
3 4
5 +// Dev proxy → local API (uvicorn on :8090). In production the API serves dist/ itself (SPA fallback in main.py).
4 6 export default defineConfig({
5 − plugins: [react()],
7 + plugins: [{ enforce: 'pre', ...mdx({ providerImportSource: '@mdx-js/react' }) }, react()],
8 + build: {
9 + target: 'es2020',
10 + rollupOptions: {
11 + output: {
12 + manualChunks: {
13 + vendor: ['react', 'react-dom', 'react-router-dom'],
14 + charts: ['lightweight-charts'],
15 + prism: ['prism-react-renderer'],
16 + },
17 + },
18 + },
19 + },
6 20 server: {
7 21 proxy: {
8 − '/v1': 'http://localhost:8090',
22 + '/v1': { target: 'http://localhost:8090', ws: true },
9 23 '/health': 'http://localhost:8090',
24 + '/openapi.json': 'http://localhost:8090',
10 25 },
11 26 },
12 27 })
added pytest.ini +7 −0
@@ -0,0 +1,7 @@
1 +[pytest]
2 +testpaths = tests
3 +pythonpath = hfmarketdata/api
4 +asyncio_mode = auto
5 +filterwarnings =
6 + ignore::DeprecationWarning
7 +addopts = -q
added requirements-dev.txt +7 −0
@@ -0,0 +1,7 @@
1 +-r hfmarketdata/requirements.txt
2 +pytest>=8
3 +pytest-asyncio>=0.23
4 +fakeredis>=2.23
5 +freezegun>=1.5
6 +respx>=0.21
7 +ruff>=0.6
added tests/__init__.py +0 −0
added tests/conftest.py +49 −0
@@ -0,0 +1,49 @@
1 +"""Shared pytest fixtures.
2 +
3 +The tests never touch the real 350 GB lake: `tests/fixtures/make_fixtures.py` builds a tiny synthetic
4 +Parquet lake (same layout as frd_downloader.py) in a temp directory, and the API is imported with
5 +HFMD_DATA_ROOT pointing at it. Redis is replaced by fakeredis; the SQLite state DB lives in the temp dir.
6 +"""
7 +from __future__ import annotations
8 +
9 +import importlib
10 +import os
11 +import sys
12 +from pathlib import Path
13 +
14 +import pytest
15 +
16 +ROOT = Path(__file__).resolve().parents[1]
17 +API_DIR = ROOT / "hfmarketdata" / "api"
18 +if str(API_DIR) not in sys.path:
19 + sys.path.insert(0, str(API_DIR))
20 +
21 +
22 +@pytest.fixture(scope="session")
23 +def lake(tmp_path_factory) -> Path:
24 + from tests.fixtures.make_fixtures import build_lake
25 + root = tmp_path_factory.mktemp("lake")
26 + build_lake(root)
27 + return root
28 +
29 +
30 +@pytest.fixture(scope="session")
31 +def app(lake):
32 + os.environ["HFMD_DATA_ROOT"] = str(lake)
33 + os.environ["HFMD_STATE_DB"] = str(lake / "state" / "hfmd.db")
34 + os.environ["HFMD_WEB_DIST"] = str(lake / "no-web")
35 + os.environ["HFMD_ENV"] = "test"
36 + os.environ["HFMD_REDIS_URL"] = "fakeredis://"
37 + os.environ["HFMD_SECRET_KEY"] = "test-secret"
38 + os.environ["HFMD_KEY_SALT"] = "test-salt"
39 + for m in [m for m in list(sys.modules) if m.split(".")[0] in ("core", "main", "futures", "accounts", "ratelimit", "fundamentals", "bulk", "stream", "openapi")]:
40 + del sys.modules[m]
41 + main = importlib.import_module("main")
42 + return main.app
43 +
44 +
45 +@pytest.fixture(scope="session")
46 +def client(app):
47 + from fastapi.testclient import TestClient
48 + with TestClient(app) as c:
49 + yield c
added tests/fixtures/__init__.py +0 −0
added tests/fixtures/make_fixtures.py +118 −0
@@ -0,0 +1,118 @@
1 +"""Build a tiny synthetic Parquet lake with the exact layout produced by frd_downloader.py.
2 +
3 + parquet/{stock|etf|crypto|index|fx}/{timeframe}/{adjustment}/{TICKER}_{timeframe}.parquet
4 + parquet/futures/{timeframe}/{contin_UNadj|contin_adj_ratio|contin_adj_absolute}/{ROOT}_{timeframe}.parquet
5 + parquet/futures_contracts/{timeframe}/{archive|update}/{ROOT}_{MonthCode}{YY}_{timeframe}.parquet
6 + parquet/options/{year}_{quarter}/{TICKER}_month_option_chain.parquet
7 + meta/futures/futures.csv
8 +
9 +Every bar file carries a `ticker` column (root only for futures contracts — the contract identity is
10 +in the file name, exactly like the real lake).
11 +"""
12 +from __future__ import annotations
13 +
14 +import csv
15 +from datetime import date, datetime, timedelta
16 +from pathlib import Path
17 +
18 +import numpy as np
19 +import pandas as pd
20 +
21 +TIMEFRAMES = ["1min", "5min", "30min", "1hour", "1day"]
22 +TICKERS = {"stock": ["AAPL", "MSFT", "SMCP"], "etf": ["SPY"], "crypto": ["BTCUSD"], "index": ["SPX"], "fx": ["EURUSD"]}
23 +ADJ = {"stock": ["adj_split", "adj_splitdiv", "UNADJUSTED"], "etf": ["adj_split", "adj_splitdiv", "UNADJUSTED"],
24 + "crypto": ["none"], "index": ["none"], "fx": ["none"]}
25 +ROOTS = ["ES", "CL", "NG"]
26 +MONTHS = {"F": 1, "G": 2, "H": 3, "J": 4, "K": 5, "M": 6, "N": 7, "Q": 8, "U": 9, "V": 10, "X": 11, "Z": 12}
27 +CYCLE = {"ES": "HMUZ", "CL": "FGHJKMNQUVXZ", "NG": "FGHJKMNQUVXZ"}
28 +
29 +
30 +def _bars(ticker: str, start: date, end: date, tf: str, seed: int, oi: bool = False) -> pd.DataFrame:
31 + rng = np.random.default_rng(seed)
32 + if tf == "1day":
33 + idx = pd.bdate_range(start, end)
34 + else:
35 + step = {"1min": 1, "5min": 5, "30min": 30, "1hour": 60}[tf]
36 + days = pd.bdate_range(start, end)[-5:] # intraday: last 5 sessions only, RTH 09:30-16:00 ET
37 + idx = pd.DatetimeIndex([d + timedelta(hours=9, minutes=30) + timedelta(minutes=step * i)
38 + for d in days for i in range(int(390 / step))])
39 + n = len(idx)
40 + close = 100 + np.cumsum(rng.normal(0, 1, n))
41 + df = pd.DataFrame({"ticker": ticker, "datetime": idx, "open": close + rng.normal(0, .2, n),
42 + "high": close + abs(rng.normal(0, .5, n)), "low": close - abs(rng.normal(0, .5, n)),
43 + "close": close, "volume": rng.integers(100, 10_000, n).astype(float)})
44 + if oi:
45 + df["open_interest"] = rng.integers(1_000, 100_000, n).astype(float)
46 + return df
47 +
48 +
49 +def build_lake(root: Path, start: date = date(2023, 1, 2), end: date = date(2025, 6, 30)) -> None:
50 + pq = root / "parquet"
51 + seed = 1
52 + for asset, tickers in TICKERS.items():
53 + for tf in TIMEFRAMES:
54 + for adj in ADJ[asset]:
55 + if tf not in ("1min", "1day") and adj == "UNADJUSTED":
56 + continue
57 + d = pq / asset / tf / adj
58 + d.mkdir(parents=True, exist_ok=True)
59 + for t in tickers:
60 + seed += 1
61 + _bars(t, start, end, tf, seed).to_parquet(d / f"{t}_{tf}.parquet", index=False)
62 + for tf in TIMEFRAMES:
63 + for adj in ("contin_UNadj", "contin_adj_ratio", "contin_adj_absolute"):
64 + d = pq / "futures" / tf / adj
65 + d.mkdir(parents=True, exist_ok=True)
66 + for r in ROOTS:
67 + seed += 1
68 + _bars(r, start, end, tf, seed, oi=(tf == "1day")).to_parquet(d / f"{r}_{tf}.parquet", index=False)
69 + # individual contracts: archive = up to 2024, update = 2025+ (with overlap for 2025 contracts)
70 + for tf in TIMEFRAMES:
71 + for r in ROOTS:
72 + for yy in (23, 24, 25, 26):
73 + for mc in CYCLE[r][:: (1 if r == "ES" else 3)]:
74 + exp_month = MONTHS[mc]
75 + exp = date(2000 + yy, exp_month, 15)
76 + first = exp - timedelta(days=400)
77 + last = min(exp, end)
78 + if first > end:
79 + continue
80 + bucket = "update" if yy >= 25 else "archive"
81 + d = pq / "futures_contracts" / tf / bucket
82 + d.mkdir(parents=True, exist_ok=True)
83 + seed += 1
84 + _bars(r, max(first, date(2022, 1, 3)), last, tf, seed, oi=(tf == "1day")).to_parquet(
85 + d / f"{r}_{mc}{yy}_{tf}.parquet", index=False)
86 + if bucket == "update" and yy == 25: # archive also holds the first half of 2025 contracts
87 + d2 = pq / "futures_contracts" / tf / "archive"
88 + d2.mkdir(parents=True, exist_ok=True)
89 + _bars(r, max(first, date(2022, 1, 3)), min(last, date(2024, 12, 31)), tf, seed, oi=(tf == "1day")).to_parquet(
90 + d2 / f"{r}_{mc}{yy}_{tf}.parquet", index=False)
91 + # options: one quarter, one ticker
92 + d = pq / "options" / "2025_q2"
93 + d.mkdir(parents=True, exist_ok=True)
94 + rows = []
95 + for td in pd.bdate_range("2025-04-01", "2025-04-10"):
96 + for k in (180, 190, 200, 210):
97 + for cp in ("c", "p"):
98 + rows.append({"ticker": "AAPL", "trade_date": td.date(), "strike": float(k), "expiry": date(2025, 6, 20),
99 + "call_put": cp, "bid": 1.0, "ask": 1.2, "last": 1.1, "volume": 10.0, "open_interest": 100.0,
100 + "iv": 0.25, "delta": 0.5 if cp == "c" else -0.5, "gamma": 0.01, "theta": -0.02, "vega": 0.1,
101 + "rho": 0.01, "underlying_price": 195.0})
102 + pd.DataFrame(rows).to_parquet(d / "AAPL_month_option_chain.parquet", index=False)
103 + # metadata
104 + m = root / "meta" / "futures"
105 + m.mkdir(parents=True, exist_ok=True)
106 + with open(m / "futures.csv", "w", newline="") as f:
107 + w = csv.writer(f)
108 + w.writerow(["Ticker", "Name", "First Date", "Last Date"])
109 + w.writerow(["ES", "E-mini S&P 500 (CME) ", "2008-01-02", str(end)])
110 + w.writerow(["CL", "Crude Oil WTI (NYMEX) ", "2008-01-02", str(end)])
111 + w.writerow(["NG", "Natural Gas (NYMEX) ", "2008-01-02", str(end)])
112 + (root / "state").mkdir(exist_ok=True)
113 +
114 +
115 +if __name__ == "__main__":
116 + import sys
117 + build_lake(Path(sys.argv[1] if len(sys.argv) > 1 else "/tmp/hfmd-lake"))
118 + print("fixture lake built")
added tests/test_v1_regression.py +52 −0
@@ -0,0 +1,52 @@
1 +"""Non-regression of the historical v1 surface: same paths, same shapes, same status codes."""
2 +
3 +
4 +def test_health(client):
5 + r = client.get("/health")
6 + assert r.status_code == 200
7 + assert r.json()["status"] == "ok"
8 +
9 +
10 +def test_status_lists_assets(client):
11 + r = client.get("/v1/status")
12 + assert r.status_code == 200
13 + body = r.json()
14 + assert "datasets" in body and "stock" in body["datasets"]
15 +
16 +
17 +def test_tickers_and_bars_shape(client):
18 + r = client.get("/v1/stock/tickers?timeframe=1day")
19 + assert r.status_code == 200
20 + assert "AAPL" in r.json()["tickers"]
21 + r = client.get("/v1/bars/stock/AAPL?timeframe=1day&limit=5")
22 + assert r.status_code == 200
23 + body = r.json()
24 + assert body["count"] == 5 and set(body["data"][0]) >= {"ticker", "datetime", "open", "high", "low", "close", "volume"}
25 + assert r.headers["X-Row-Count"] == "5"
26 +
27 +
28 +def test_csv_format(client):
29 + r = client.get("/v1/bars/stock/AAPL?timeframe=1day&limit=3&format=csv")
30 + assert r.status_code == 200 and r.headers["content-type"].startswith("text/csv")
31 + assert r.text.splitlines()[0].startswith("ticker,datetime")
32 +
33 +
34 +def test_uniform_error_envelope_keeps_legacy_detail(client):
35 + r = client.get("/v1/bars/stock/NOPE?timeframe=1day")
36 + assert r.status_code == 404
37 + body = r.json()
38 + assert body["error"]["code"] == "TICKER_NOT_FOUND"
39 + assert body["error"]["docs"].endswith("#ticker_not_found")
40 + assert body["detail"] # legacy field still present
41 +
42 +
43 +def test_validation_error_is_uniform(client):
44 + r = client.get("/v1/bars/stock/AAPL?timeframe=1day&limit=0")
45 + assert r.status_code == 422
46 + assert r.json()["error"]["code"] == "VALIDATION_ERROR"
47 +
48 +
49 +def test_options_chain(client):
50 + r = client.get("/v1/options/chain/AAPL?limit=4")
51 + assert r.status_code == 200
52 + assert r.json()["count"] == 4
53