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%

correctifs prod : fond opaque de repli (Safari sans color-mix) pour topbar/tiroir/nav docs, thème sombre par défaut ; clés utilisables pour les comptes invités ; normalisation EDGAR tolère les dates manquantes (NaT) ; import robuste des modèles dans l'agrégation d'usage

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

8 changed files +40 −23

modified hfmarketdata/api/fundamentals/normalize.py +11 −1
@@ -222,8 +222,18 @@ class FiscalCalendar:
222 222 return fe - timedelta(days=round(91.3125 * (4 - fq)))
223 223
224 224
225 +def _is_missing(d) -> bool:
226 + """None or pandas NaT / NaN (companyfacts leaves `start` empty on instant facts and on some odd rows)."""
227 + if d is None:
228 + return True
229 + try:
230 + return bool(d != d) # NaT/NaN are not equal to themselves
231 + except Exception:
232 + return False
233 +
234 +
225 235 def span_type(start: date | None, end: date) -> str | None:
226 − if start is None:
236 + if _is_missing(start) or _is_missing(end):
227 237 return None
228 238 days = (end - start).days
229 239 if 75 <= days <= 105:
modified hfmarketdata/api/ratelimit/middleware.py +5 −1
@@ -158,7 +158,7 @@ def resolve_principal(scope: dict) -> Principal:
158 158 if info is None or info.key_status != "active":
159 159 raise ApiError(401, "INVALID_API_KEY", "The API key is unknown, malformed or revoked. Check it in your "
160 160 f"dashboard ({settings.public_url}/dashboard) or omit it to use keyless access.")
161 − if info.user_status != "active":
161 + if info.user_status == "disabled": # `invited` accounts (created by an admin) can use their key right away
162 162 raise ApiError(403, "ACCOUNT_DISABLED", "This API key belongs to a disabled account. "
163 163 f"Contact {settings.contact_email}.")
164 164 return Principal(f"key:{info.key_id}", "key", tier_for(info.tier), info.user_id, info.key_id)
@@ -410,6 +410,10 @@ def stream_charger(principal: str, rows: int) -> None:
410 410
411 411 def install(app: FastAPI) -> None:
412 412 """Mount the middleware INSIDE the CORS layer so 429/401 responses keep their CORS headers."""
413 + try: # import the account models once, on the main thread (avoids a duplicate-definition race in the usage folder thread)
414 + import accounts.models # noqa: F401
415 + except Exception:
416 + pass
413 417 try: # stream module (fundamentals filings WebSocket) → charge delivered messages to the rows quota
414 418 from stream import accounting as _stream_accounting
415 419 _stream_accounting.set_charger(stream_charger)
modified hfmarketdata/api/ratelimit/usage.py +5 −2
@@ -161,8 +161,11 @@ def fold(now_s: float | None = None) -> int:
161 161
162 162
163 163 def _models():
164 − from accounts import models
165 − return models
164 + import sys
165 + mod = sys.modules.get("accounts.models")
166 + if mod is None:
167 + from accounts import models as mod # type: ignore[no-redef]
168 + return mod
166 169
167 170
168 171 async def _folder_loop(interval_s: int = 60) -> None:
modified hfmarketdata/web/index.html +1 −1
@@ -20,7 +20,7 @@
20 20 // Theme is resolved before first paint (no flash): localStorage → system preference → dark.
21 21 try {
22 22 var t = localStorage.getItem('hfmd.theme');
23 − if (t !== 'light' && t !== 'dark') t = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
23 + if (t !== 'light' && t !== 'dark') t = 'dark';
24 24 document.documentElement.setAttribute('data-theme', t);
25 25 } catch (e) {}
26 26 </script>
modified hfmarketdata/web/src/app/theme.css +6 −6
@@ -60,7 +60,7 @@ button { font: inherit; }
60 60
61 61 /* ---- shell ---- */
62 62 .shell { min-height: 100dvh; display: flex; flex-direction: column; }
63 −.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; gap: 20px; padding: 0 20px; height: var(--topbar-h); background: color-mix(in srgb, var(--bg) 86%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); }
63 +.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; gap: 20px; padding: 0 20px; height: var(--topbar-h); background: var(--bg); background: color-mix(in srgb, var(--bg) 86%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--line); }
64 64 .brand { display: inline-flex; align-items: center; gap: 9px; color: var(--fg); font-weight: 650; letter-spacing: -0.01em; white-space: nowrap; }
65 65 .brand:hover { text-decoration: none; }
66 66 .brand-mark { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 7px; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 55%, var(--accent-2))); color: var(--accent-ink); font-family: var(--mono); font-size: 12px; font-weight: 700; }
@@ -80,7 +80,7 @@ button { font: inherit; }
80 80 .btn:hover { border-color: var(--line-3); text-decoration: none; background: var(--bg-2); }
81 81 .btn:active { transform: translateY(1px); }
82 82 .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 650; }
83 −.btn-primary:hover { background: color-mix(in srgb, var(--accent) 88%, white); border-color: color-mix(in srgb, var(--accent) 88%, white); }
83 +.btn-primary:hover { background: var(--accent); background: color-mix(in srgb, var(--accent) 88%, white); border-color: color-mix(in srgb, var(--accent) 88%, white); }
84 84 .btn-ghost { background: transparent; border-color: transparent; color: var(--fg-1); }
85 85 .btn-ghost:hover { background: var(--bg-2); color: var(--fg); border-color: transparent; }
86 86 .btn-lg { padding: 12px 20px; font-size: 15px; border-radius: 10px; }
@@ -109,7 +109,7 @@ button { font: inherit; }
109 109 table { border-collapse: collapse; width: 100%; font-size: 14px; }
110 110 th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
111 111 th { color: var(--fg-1); font-weight: 600; font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; }
112 −tbody tr:hover { background: color-mix(in srgb, var(--bg-2) 60%, transparent); }
112 +tbody tr:hover { background: var(--bg-2); background: color-mix(in srgb, var(--bg-2) 60%, transparent); }
113 113 .table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); }
114 114 .table-wrap table { min-width: 520px; }
115 115 .table-wrap th { background: var(--bg-1); position: sticky; top: 0; white-space: nowrap; overflow-wrap: normal; }
@@ -118,12 +118,12 @@ tbody tr:hover { background: color-mix(in srgb, var(--bg-2) 60%, transparent); }
118 118
119 119 input, select, textarea { font: inherit; color: var(--fg); background: var(--bg-1); border: 1px solid var(--line-2); border-radius: 6px; padding: 8px 10px; }
120 120 input:focus, select:focus, textarea:focus { outline: 2px solid color-mix(in srgb, var(--accent) 50%, transparent); border-color: var(--accent); }
121 −::selection { background: color-mix(in srgb, var(--accent) 35%, transparent); }
121 +::selection { background: var(--accent); background: color-mix(in srgb, var(--accent) 35%, transparent); }
122 122 @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
123 123
124 124 /* ---- mobile drawer ---- */
125 −.drawer-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, .55); }
126 −.drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 61; width: min(320px, 86vw); background: var(--bg-1); border-left: 1px solid var(--line); padding: 16px; display: flex; flex-direction: column; gap: 6px; box-shadow: var(--shadow); }
125 +.drawer-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(0, 0, 0, .62); }
126 +.drawer { position: fixed; top: 0; right: 0; bottom: 0; z-index: 61; width: min(320px, 86vw); background: var(--bg-1); background-color: var(--bg-1); isolation: isolate; overflow-y: auto; -webkit-overflow-scrolling: touch; border-left: 1px solid var(--line); padding: 16px; display: flex; flex-direction: column; gap: 6px; box-shadow: var(--shadow); }
127 127 .drawer-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
128 128 .drawer a.navlink { display: block; padding: 10px 12px; border-radius: 8px; color: var(--fg-1); font-weight: 500; }
129 129 .drawer a.navlink.active, .drawer a.navlink:hover { background: var(--bg-2); color: var(--fg); text-decoration: none; }
modified hfmarketdata/web/src/components/components.css +1 −1
@@ -2,7 +2,7 @@
2 2
3 3 /* Code */
4 4 .code { border: 1px solid var(--line); border-radius: var(--radius); background: var(--code-bg); overflow: hidden; margin: 0 0 16px; min-width: 0; max-width: 100%; }
5 −.code-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px 6px 12px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--bg-1) 70%, var(--code-bg)); min-height: 34px; flex-wrap: wrap; }
5 +.code-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 8px 6px 12px; border-bottom: 1px solid var(--line); background: var(--bg-1); background: color-mix(in srgb, var(--bg-1) 70%, var(--code-bg)); min-height: 34px; flex-wrap: wrap; }
6 6 .code-title { font-family: var(--mono); font-size: 12px; color: var(--fg-2); letter-spacing: .02em; }
7 7 /* contain: inline-size → the (scrolling) pre never inflates the intrinsic width of grid/flex ancestors on narrow screens */
8 8 .code-pre { margin: 0; border: 0; border-radius: 0; background: transparent; padding: 12px 14px; font-size: 13px; line-height: 1.55; tab-size: 2; contain: inline-size; }
modified hfmarketdata/web/src/pages/docs/docs.css +1 −1
@@ -126,7 +126,7 @@
126 126 @media (max-width: 900px) {
127 127 .docs { grid-template-columns: 1fr; }
128 128 .docs-nav { display: none; }
129 − .docs-nav.open { display: block; position: fixed; inset: var(--topbar-h) 0 0 0; z-index: 45; height: auto; background: var(--bg); border-right: 0; padding: 16px; }
129 + .docs-nav.open { display: block; position: fixed; inset: var(--topbar-h) 0 0 0; z-index: 45; height: auto; background: var(--bg); background-color: var(--bg); isolation: isolate; border-right: 0; padding: 16px; }
130 130 .docs-nav-toggle { display: flex; align-items: center; gap: 8px; padding: 10px 16px; border-bottom: 1px solid var(--line); background: var(--bg-1); position: sticky; top: var(--topbar-h); z-index: 40; font-size: 13.5px; color: var(--fg-1); }
131 131 .docs-nav-toggle button { display: inline-flex; align-items: center; gap: 6px; }
132 132 .docs-content { padding: 24px 16px 60px; }
modified hfmarketdata/web/src/playground/playground.css +10 −10
@@ -20,9 +20,9 @@
20 20 .pg-type.active { background: var(--bg-1); border-color: var(--line-2); color: var(--fg); }
21 21 .pg-type-title { font-weight: 500; }
22 22 .pg-type-path { grid-column: 2; font-size: 11px; color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
23 −.pg-method { font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; align-self: center; background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
24 −.pg-method.ws { background: color-mix(in srgb, var(--accent-2) 18%, transparent); color: var(--accent-2); }
25 −.pg-method.post, .pg-method.patch, .pg-method.delete { background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
23 +.pg-method { font-family: var(--mono); font-size: 10px; font-weight: 600; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; align-self: center; background: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
24 +.pg-method.ws { background: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 18%, transparent); color: var(--accent-2); }
25 +.pg-method.post, .pg-method.patch, .pg-method.delete { background: var(--warn); background: color-mix(in srgb, var(--warn) 18%, transparent); color: var(--warn); }
26 26
27 27 .pg-form-section { border-top: 1px solid var(--line); padding-top: 14px; }
28 28 .pg-desc { font-size: 13px; margin: 0 0 12px; }
@@ -55,8 +55,8 @@
55 55 .pg-kbd { font-size: 12px; }
56 56 .pg-status { display: inline-flex; align-items: center; gap: 10px; font-size: 13px; }
57 57 .pg-code-badge { padding: 2px 8px; border-radius: 4px; font-family: var(--mono); font-size: 12px; font-weight: 600; }
58 −.pg-code-badge.ok { background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
59 −.pg-code-badge.bad { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }
58 +.pg-code-badge.ok { background: var(--accent); background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent); }
59 +.pg-code-badge.bad { background: var(--danger); background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }
60 60 .pg-err-details { margin: 8px 0 0; font-size: 12px; }
61 61
62 62 .pg-results { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-1); overflow: hidden; }
@@ -111,7 +111,7 @@
111 111 .pg-rl-fill.warn { background: var(--warn); }
112 112 .pg-rl-fill.danger { background: var(--danger); }
113 113 .pg-rl-foot { font-size: 12px; margin: 0; }
114 −.pg-rl-429 { font-size: 13px; padding: 8px 10px; border-radius: 6px; background: color-mix(in srgb, var(--danger) 15%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent); }
114 +.pg-rl-429 { font-size: 13px; padding: 8px 10px; border-radius: 6px; background: var(--danger); background: color-mix(in srgb, var(--danger) 15%, transparent); border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent); }
115 115
116 116 .pg-code { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: #011627; }
117 117 .pg-code-head { display: flex; align-items: center; justify-content: space-between; padding: 6px 10px; background: var(--bg-2); border-bottom: 1px solid var(--line); font-size: 12px; color: var(--fg-1); }
@@ -124,10 +124,10 @@
124 124 .pg-preset:hover { color: var(--fg); border-color: var(--fg-2); }
125 125
126 126 .pg-callout { display: flex; justify-content: space-between; gap: 14px; align-items: center; padding: 12px 14px; border-radius: var(--radius); border: 1px solid var(--line); background: var(--bg-1); font-size: 13.5px; flex-wrap: wrap; }
127 −.pg-callout-info { border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); background: color-mix(in srgb, var(--accent-2) 8%, var(--bg-1)); }
128 −.pg-callout-warn { border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: color-mix(in srgb, var(--warn) 8%, var(--bg-1)); }
129 −.pg-callout-danger { border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: color-mix(in srgb, var(--danger) 8%, var(--bg-1)); }
130 −.pg-callout-success { border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: color-mix(in srgb, var(--accent) 8%, var(--bg-1)); }
127 +.pg-callout-info { border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); background: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 8%, var(--bg-1)); }
128 +.pg-callout-warn { border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: var(--warn); background: color-mix(in srgb, var(--warn) 8%, var(--bg-1)); }
129 +.pg-callout-danger { border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger); background: color-mix(in srgb, var(--danger) 8%, var(--bg-1)); }
130 +.pg-callout-success { border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--bg-1)); }
131 131 .pg-callout-body { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 220px; }
132 132 .pg-callout-title { font-size: 14px; }
133 133
134 134