# HF Market Data — Upgrade majeur (v2) : plan d'exécution Date : 2026-09-04 · Auteur : Simon-Pierre Boucher · Statut : livrable 1 (schéma · plan de doc · maquettes), puis implémentation chantier par chantier. 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. ## 0. Architecture cible ``` hfmarketdata/ api/ FastAPI (Python 3.14, DuckDB sur le lac Parquet, SQLite métadonnées, Redis quotas) main.py app + endpoints historiques (inchangés) + montage des routeurs v2 core/config.py env (HFMD_*) · core/errors.py (format uniforme) · core/db.py (SQLite/SQLAlchemy) futures/ chantier 1 : symbols.py (parsing), specs.py (référentiel racines), rolls.py, backfill.py, routes.py accounts/ chantier 2 : models.py, security.py (hash/PAT), routes_auth.py, routes_me.py, routes_admin.py, mailer.py, cli.py ratelimit/ chantier 2 : redis_limiter.py (Lua), middleware.py, tiers.py openapi.py spec 3.1 enrichie (tags, exemples, codes d'erreur) = source unique de la doc web/ Vite + React 18 + react-router · dark par défaut · SPA code-splittée (pré-rendu des pages statiques au build) src/app/ shell, nav, thème, auth context src/pages/{home,docs,playground,integrations,pricing,status,auth,dashboard,admin} src/docs/ générateur 3 colonnes depuis /openapi.json + guides MDX (content/guides/*.mdx) + changelog src/playground/ constructeur de requêtes, exécution, tableau, mini-graphique, export de code, headers quota mcp/ chantier 5 : serveur MCP `hfmarketdata-mcp` (TypeScript, stdio) + README vitrine skills/ chantier 5 : pack de skills (SKILL.md + scripts) → web/public/downloads/hfmarketdata-skills.zip tests/ pytest (unit + intégration TestClient + Redis) · web/e2e (Playwright) scripts/backfill_contracts.py chantier 1 : backfill idempotent des échéances (2010 →) frd_downloader.py inchangé ``` 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. ## 1. Schéma de base de données ### 1.1 `futures_roots` (référentiel des produits) | colonne | type | note | |---|---|---| | root | TEXT PK | ex. ES, CL, 6E (FirstRate : `E6` → alias `6E` accepté) | | name | TEXT | « E-mini S&P 500 » | | exchange | TEXT | CME, CBOT, NYMEX, COMEX, ICE, EUREX, … | | asset_class | TEXT | equity_index, energy, metals, rates, ags, fx, crypto, volatility, softs, livestock | | currency | TEXT | USD, EUR, … | | contract_size | REAL / TEXT | « 50 × index », 1 000 bbl… (valeur numérique + `contract_size_unit`) | | tick_size | REAL | | | tick_value | REAL | | | settlement_type | TEXT | cash / physical | | expiry_rule | TEXT | clé de règle (ex. `third_friday`, `cl_rule`, `last_business_day`, `data`) | | month_cycle | TEXT | ex. `HMUZ`, `FGHJKMNQUVXZ` | | first_data_date, last_data_date | DATE | dérivés du lac (min/max sur tous les contrats) | | contracts_count | INT | | | source | TEXT | `reference` (table statique) / `derived` | ### 1.2 `futures_contracts` | colonne | type | note | |---|---|---| | symbol | TEXT PK | forme courte `ESZ25` ; forme longue `ESZ2025` acceptée en entrée | | root | TEXT FK | | | month_code | CHAR(1) | F G H J K M N Q U V X Z | | contract_month | INT (1-12) · contract_year | INT | | | expiration_date | DATE | règle par racine, sinon `last_data_date` ; `expiration_source` = rule / data | | last_trading_date | DATE | | | first_notice_date | DATE | NULL si cash-settled | | settlement_type, contract_size, tick_size, tick_value, currency, exchange | | copiés de la racine | | first_data_date, last_data_date | DATE | plage réelle (1day) | | status | TEXT | active / expired (expired si `last_data_date` < aujourd'hui − 7 j ET mois d'échéance passé) | | volume_avg_daily | REAL | 20 dernières séances | | open_interest_last | REAL | dernier OI non nul | | timeframes | TEXT | JSON des granularités disponibles | | files | TEXT | JSON `{tf: [archive_path, update_path]}` | | updated_at | TIMESTAMP | | Index : (root, expiration_date), (status), (root, contract_year, contract_month). ### 1.3 `futures_contract_gaps` — `symbol, timeframe, gap_start, gap_end, bars_missing` (gaps > 3 jours ouvrés, calculés au backfill). ### 1.4 Comptes / clés / quotas | table | colonnes | |---|---| | 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 | | email_tokens | id, user_id FK, kind (`verify`/`reset`/`invite`), token_hash, expires_at, used_at | | 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 | | usage_daily | day, principal (`key:` ou `ip:`), requests, rows, rows_parquet, bytes, status_2xx, status_429 — PK (day, principal) — agrégé depuis Redis toutes les minutes | | usage_minute | minute, principal, requests, rows — 7 j glissants (graphes 24 h / 7 j) | | audit_log | ts, actor, action, target, meta | 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. ### 1.5 Tiers (constantes `ratelimit/tiers.py`) | tier | fenêtre | requêtes | lignes | lignes max / requête | |---|---|---|---|---| | keyless (IP) | 1 h | 30 | 100 000 | 5 000 | | free (compte + clé) | 1 min | 120 | 1 000 000 | 50 000 | | high_usage (sur demande à contact@spboucher.ai) | 1 min | 600 | 10 000 000 | 200 000 | Redis : clé `rl:{principal}:{req|rows}` — fenêtre glissante par buckets de 1 s (hash) évaluée en Lua : `EVALSHA limiter ` → `{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`. ## 2. Contrat d'API — nouveautés (toutes additives) 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=`. 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":[...]}}`. Erreurs : 400 `INVALID_CONTRACT_SYMBOL`, 404 `CONTRACT_NOT_FOUND`, 404 `ROOT_NOT_FOUND`, 400 `INVALID_PARAMETER`, 429 `RATE_LIMIT_EXCEEDED`, 401 `INVALID_API_KEY`. 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). ## 3. Plan de la documentation (`/docs`) 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=…¶ms…`, lien « Edit / Report issue » (mailto contact@spboucher.ai + lien repo), dark par défaut, mobile : nav en tiroir, colonne code repliable. ``` Getting started Guides Reference (OpenAPI) More Quickstart (30 s) Futures individual contracts Meta & status Changelog Authentication Options chains & Greeks Bars (stocks/ETF/crypto/…) Versioning & deprecation Rate limits Time zones & sessions (RTH/ETH) Futures v2 (7 endpoints) Limits & pricing Data formats Bulk downloads Options Integrations (MCP, skills) Recipes: pandas backtest · Accounts & keys · Usage Status custom continuous · CL term structure Errors ``` ## 4. Maquettes ### 4.1 Playground (`/playground`, aussi intégré au dashboard avec clé injectée) ``` ┌ HF Market Data ─ Docs · Playground · Integrations · Pricing · Status ─────────────── [Sign in] ┐ │ ⓘ Keyless mode: 30 req/h. Create a free account for 120 req/min → [Create free account] │ ├──────────────────────────────┬─────────────────────────────────────────────────────────────────┤ │ Request type ▾ │ GET https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=… │ [Copy] │ ○ Stock/ETF bars │ ──────────────────────────────────────────────────────────────── │ │ ○ Crypto · FX │ [Run ▶] 200 OK · 184 ms · 2 512 rows │ │ ● Futures continuous │ ┌ Table ─┬ JSON ─┬ Chart ──────────────────────────────────────┐ │ │ ○ Individual contract │ │ ▂▃▅▆▇█▇▆▅ candlesticks (lightweight-charts) roll markers ▲ │ │ │ ○ Contract chain │ └─────────────────────────────────────────────────────────────┘ │ │ ○ Term structure │ Rate limit: requests 29/30 · rows 97 488/100 000 · reset 41 min │ │ ○ Options chain (Greeks) │ ┌ curl ─┬ Python (requests+pandas) ─┬ JavaScript (fetch) ─────┐ │ │ ○ Symbols / roots │ │ import requests, pandas as pd … │ │ │ ─ Form (dynamic) ─ │ └─────────────────────────────────────────────────────────────┘ │ │ root [ES ▾ autocomplete] │ Examples: AAPL 1-min today · CL chain · ES continuous back-adj │ │ roll [volume ▾] adjust [back_adjusted ▾] depth [1 ▾] 2015-2025 · NG term structure │ │ from [2015-01-01] to [2025-12-31] interval [1d ▾] format [json ▾] session [all ▾] │ └──────────────────────────────┴─────────────────────────────────────────────────────────────────┘ ``` ### 4.2 Dashboard (`/dashboard`) ``` ┌ Sidebar: Overview · API keys · Usage · Playground · Account Tier: Free · Need more? contact@spboucher.ai ┐ │ Overview ─ Requests today 1 240 / (120/min) · Rows today 3.1 M · Last request 2 min ago │ │ API keys ─ ┌ name ─ prefix ─ created ─ last used ─ status ─ actions ┐ [+ Create key] │ │ │ default hfmd_live_ab12cd34… 2026-09-04 just now active [Rotate] [Revoke] │ (clé montrée 1 fois) │ │ Usage ─ [24h] [7d] [30d] ▁▂▃▅▆▇█ requests / min ▁▁▂▃▅▆ rows / min · table par jour · export CSV │ │ Playground─ même composant qu'en public, clé injectée automatiquement, bandeau « authenticated as … » │ │ Admin (rôle admin) ─ users (créer · inviter · tier · désactiver) · consommation globale · top principals │ └────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ``` ### 4.3 Access & limits (`/limits`) — tout est gratuit 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). ### 4.4 Homepage 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`). ## 5. Ordre d'implémentation 1. Socle : config, erreurs uniformes, SQLite, OpenAPI 3.1 enrichie, tests de non-régression des endpoints v1. 2. Chantier 1 (futures) + backfill + tests unitaires symboles/rolls. 3. Chantier 2 (Redis limiter, clés, comptes, dashboard API, CLI `hfmd users add`, seed des 3 utilisateurs). 4. Chantier 6 + 3 + 4 (site, docs 3 colonnes, playground) — frontend. 5. Chantier 5 (MCP + skills + page Integrations). 6. Tests E2E Playwright, déploiement via `mld` sur M3U96b (Redis + backfill dans le manifeste).