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%

page 2

  1. Sun, Sep 6, 2026 26

    1. Simon-Pierre Boucher committed 18 days ago · 1 file +35 −1
    2. Simon-Pierre Boucher committed 18 days ago · 2 files +7 −1
    3. Simon-Pierre Boucher committed 18 days ago · 81 files +2,909 −2,351
    4. Simon-Pierre Boucher committed 18 days ago · 1 file +40 −12
    5. Simon-Pierre Boucher committed 18 days ago · 1 file +45 −15
    6. stream: registre de connexions ZSET et livraison par XREAD bloquant
      …
      * stream:conns:{principal} : ZSET (membre = id de connexion, score = dernier
        heartbeat) au lieu d'un compteur INCR/DECR qui restait gonflé 1 h quand un worker
        mourait sans DECR (4029 injustifiés). Les membres muets depuis 90 s sont élagués à
        chaque comptage ; le score est rafraîchi toutes les 30 s ; ZREM à la déconnexion.
      * Fan-out : un XREAD BLOCK (1 s) par socket abonnée sur filings:stream, via le client
        redis.asyncio, au lieu d'un XRANGE toutes les 250 ms par connexion. Le lecteur a sa
        propre connexion Redis fermée avec lui (une lecture bloquante annulée peut laisser une
        réponse tardive sur la connexion — doublons à la ré-abonnement) + événement d'arrêt.
      * Plus aucun appel Redis synchrone dans la coroutine : ZSET/hello via le client async,
        accounting.charge (compteur sync) via asyncio.to_thread.
      * Tests : élagage des connexions fantômes, rafraîchissement du score au heartbeat,
        livraison bloquante + unsubscribe/resume sans doublon. conftest.make_user : e-mail
        unique par uuid (id(counter) provoquait des collisions EMAIL_TAKEN).
      
      Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
      Simon-Pierre Boucher committed 18 days ago · 4 files +234 −47
    7. Simon-Pierre Boucher committed 18 days ago · 1 file +15 −4
    8. frd: row groups de 65 536 lignes pour l'intraday + script de réécriture reprenable du lac
      …
      - frd_downloader.csv_to_parquet : ROW_GROUP_SIZE 65536 pour 1min/5min/30min/1hour (déduit du chemin), 1 000 000 pour 1day/options
      - scripts/rewrite_row_groups.py : COPY (read_parquet) → tmp (ZSTD, ROW_GROUP_SIZE 65536) puis os.replace, fichier par
        fichier, manifeste JSON de reprise (state/row_groups.json), --dry-run, --asset, --timeframe, --workers, --max-files,
        --retry-failed ; ignore les fichiers déjà découpés ; vérifie le nombre de lignes avant le swap
      
      Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
      Simon-Pierre Boucher committed 18 days ago · 3 files +322 −1
    9. api: v1 robuste — 400 au lieu de 500, tickers partiels, limites de palier, gzip, X-Request-ID, /health étendu, /swagger
      …
      - bars/bars_multi/snapshot/options : dates parsées en Python et liées typées (start=bad → 400 INVALID_PARAMETER,
        end < start → 400), > 50 tickers → 400 TOO_MANY_TICKERS, ticker absent dans une requête multi → 200 + en-tête
        X-Missing-Tickers (404 seulement si aucun n'existe), `limit` borné par request.state.max_rows et les plafonds existants,
        LIMIT poussé dans la requête (plus de matérialisation intégrale puis .limit())
      - bars_multi/snapshot via core.duck.run_topn (un seul scan filtré) ; suppression des doublons _tls/_cache de main.py
        au profit de core.duck.con()/cached()
      - GZipMiddleware(minimum_size=1024) ; core.http.install (X-Request-ID, access log JSON, en-têtes de sécurité) ;
        Cache-Control: public, max-age=86400 sur les fenêtres de barres closes avant aujourd'hui
      - /openapi.json : route dédiée (ETag + Cache-Control, 304) au lieu du middleware BaseHTTP ; Swagger déplacé sur
        /swagger, ReDoc sur /redoc — /docs revient au site React ; fallback SPA via core.spa (404 pour chemins inconnus)
      - /health : {status, checks, version} 200/503, hors quota ; /v1/status hors quota lignes
      - lifespan branché (garde secrets prod, pool de threads borné) ; OpenAPI : X-Request-ID, X-Missing-Tickers, TOO_MANY_TICKERS
      
      Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
      Simon-Pierre Boucher committed 18 days ago · 4 files +685 −204
    10. futures: filtre à l'intérieur de la source fusionnée + CTE matérialisée (un seul scan par fichier)
      …
      `merged_sql(paths, where=…)` applique la condition dans l'union archive/update avant la fenêtre de dédup et matérialise
      le résultat (core.duck.topn_sql) ; contract_bars, continuous et root_daily n'émettent plus de QUALIFY nu suivi d'un
      LIMIT. Test de plan : EXPLAIN sur le lac synthétique = un READ_PARQUET filtré par fichier, sans HASH_JOIN.
      
      Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
      Simon-Pierre Boucher committed 18 days ago · 3 files +132 −22
    11. core: une seule base DuckDB par processus, cache TTL/LRU, parsing strict des paramètres, middleware requête, lifespan, santé, SPA
      …
      - duck.py : `duckdb.connect()` unique au niveau module (memory_limit HFMD_DUCK_MEMORY=4GB, threads HFMD_DUCK_THREADS=4,
        temp_directory HFMD_DUCK_TEMP, object cache) + `_db.cursor()` par thread ; helpers `topn_sql`/`topn_per_group`/`run_topn`
        (CTE MATERIALIZED → un seul scan filtré, plus de re-scan intégral par la late materialization) ; `explain`/`scan_count`
      - cache.py : TTLCache thread-safe avec maxsize (HFMD_CACHE_MAXSIZE=4096, HFMD_CACHE_TTL=300) — remplace les dict sans éviction
      - params.py : dates ISO 8601 (YYYY-MM-DD, espace, T, Z, décalage) → datetime typé, start <= end, listes de tickers
        (dédup, max 50 → TOO_MANY_TICKERS), `bound_limit` borné par `request.state.max_rows`
      - errors.py : code TOO_MANY_TICKERS ; handler `duckdb.Error` (Conversion/Binder/… → 400, IO/OOM → 503, sinon 500 propre)
      - http.py : middleware ASGI pur — X-Request-ID (réutilise l'entrant), une ligne JSON par requête (logger
        hfmarketdata.access), en-têtes de sécurité (HSTS, nosniff, Referrer-Policy, X-Frame-Options / CSP pour le HTML),
        Cache-Control immutable sur /assets/*
      - lifespan.py : garde secrets par défaut en production, pool anyio borné (HFMD_THREADPOOL=8), DuckDB pré-initialisé
      - health.py : Parquet + SQLite + Redis (timeout 0,3 s, fakeredis toléré) + read_parquet témoin (cache 60 s) → ok/degraded
      - spa.py : fallback React (routes connues → 200, inconnues → 404 avec index.html, fichiers réels servis, chemins API
        jamais avalés, /login → 301 /signin)
      
      Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
      Simon-Pierre Boucher committed 18 days ago · 9 files +712 −24
    12. stream: principal du middleware ratelimit, fin du repli permissif
      …
      stream/auth.py cherchait verify_api_key/resolve_api_key/authenticate_key dans
      accounts.security — aucune n'existe — puis acceptait toute chaîne hfmd_live_<32> avec
      un principal key:<sha256[:16]> : les lignes livrées étaient débitées à un compteur
      Redis orphelin et _tier_for_principal retombait sur free.
      
      * stream_ws lit ws.scope["state"] (principal, principal_kind, tier) posés par
        ratelimit.middleware._websocket ; keyless → JSON AUTH_REQUIRED + 4001 comme avant ;
        aucun contexte alors que HFMD_RATELIMIT est actif → refus 4401 sans handshake.
      * authenticate(key) = recherche api_keys.key_hash (sha256(sel+clé)) + statut compte,
        utilisée seulement quand le middleware est désactivé ; plus aucune acceptation par
        forme de clé.
      * Tests : principal = key:<id> (≠ hash), clé inconnue bien formée refusée,
        _resolve_principal sans contexte.
      
      Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
      Simon-Pierre Boucher committed 18 days ago · 3 files +106 −37
    13. fundamentals: co-déclarants (clé (cik, accn)), poller borné, dictionnaire de couverture
      …
      E1 — edgar_filings : clé primaire composite (cik, accn) + index sur accn. Un numéro
      d'accession est partagé par les co-déclarants (famille Entergy 65984/66901/…) : la PK
      sur accn seule faisait échouer tout _upsert_filings (IntegrityError, 18 entreprises
      jamais ingérées). Migration idempotente (fundamentals/migrations.py) : inline au
      démarrage si ≤ 300 k lignes, sinon avertissement + scripts/migrate_edgar_filings.py à
      lancer avant le déploiement ; l'upsert passe par INSERT … ON CONFLICT DO NOTHING et
      tolère l'ancien schéma pendant la transition. _known_pairs raisonne en paires (cik, accn)
      sans balayer la table ; publish_filing_event produit un événement par co-déclarant.
      
      E2 — poller incrémental : filtre des entrées Atom par TRACKED_FORMS (le flux type=8-K
      renvoie des 8-K/A : Genasys ré-ingéré à chaque cycle pendant 41 h) ; TRACKED_FORMS
      étendu aux amendements et rapports de transition ; garde-fou Redis edgar:seen_accn
      (TTL 7 j) + edgar:failed_accn (3 tentatives) ; companyfacts_404 enregistre quand même
      les dépôts et n'est plus compté comme échec (1 671 « failures » venaient de deux CIK
      sans facts retentés à chaque cycle) ; failure_samples désormais cumulés (plus écrasés
      par un cycle sain) ; index quotidien : jours ouvrables seulement, 403/404 = non publié
      (les master.idx de fin de semaine n'existent pas) ; bulk rebuild seulement si des
      lignes ont changé ; httpx en WARNING dans le script.
      
      E3 — fund_statements.coverage (1,17 Ko × 1,22 M lignes = 1,9 Go) → dictionnaire
      fund_coverage_blob(id, sha1, json) référencé par coverage_id ; lecture hybride
      (JSON inline hérité OU référence) dans service._rows et bulk.build ;
      _replace_statements ne réécrit que si l'empreinte des lignes a changé ;
      scripts/migrate_coverage.py convertit l'existant par lots avec reprise (--vacuum
      optionnel) ; auto_vacuum=INCREMENTAL sur une base jeune + incremental_vacuum dans
      edgar_reconcile.
      
      Tests : tests/test_fundamentals_ingest.py (co-déclarants, boucle du poller, retries,
      échantillons, 403 index, réécriture conditionnelle, dictionnaire, migration).
      
      Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
      Simon-Pierre Boucher committed 18 days ago · 13 files +1,114 −70
  2. Fri, Sep 4, 2026 14

    1. Simon-Pierre Boucher committed 20 days ago · 2 files +3 −1
    2. Simon-Pierre Boucher committed 20 days ago · 3 files +6 −6
    3. Simon-Pierre Boucher committed 20 days ago · 1 file +4 −1
    4. Simon-Pierre Boucher committed 20 days ago · 1 file +1
    5. Simon-Pierre Boucher committed 20 days ago · 1 file +23 −3
    6. Simon-Pierre Boucher committed 20 days ago · 1 file +1 −1
    7. Simon-Pierre Boucher committed 20 days ago · 1 file +6 −2