SPB Git

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)

Python 46.7% JavaScript 37.4% CSS 14.9% HTML 0.9%

Docs platform: interactive playground on every endpoint

- Per-parameter form controls (selects for enums, asset-aware adjustment
  options, required/path/query chips), live URL preview
- Send request: real call with status + latency chips, pretty response
- curl/Python/JS snippets regenerated from the chosen parameters,
  Copy URL / Copy code
- New 'What you can do' capabilities section; hero updated with final
  dataset metrics (26.5B rows, 66 options quarters)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon-pierre boucher committed 18 h ago (Aug 10, 2026) parent e082f14

Showing 3 changed files with +381 and −137

modified hfmarketdata/web/src/App.jsx +140 −57
@@ -1,7 +1,8 @@
1 1 import React, { useEffect, useMemo, useState } from 'react'
2 2 import {
3 ASSET_TYPES, BAR_FIELDS, BASE_URL, ENDPOINTS, ERRORS, OPTION_FIELDS,
4 TIMEFRAMES, curlSnippet, jsSnippet, pythonSnippet,
3 + ASSET_TYPES, BAR_FIELDS, BASE_URL, CAPABILITIES, ENDPOINTS, ERRORS,
4 + OPTION_FIELDS, TIMEFRAMES,
5 + buildUrl, curlSnippet, defaultValues, jsSnippet, paramOptions, pythonSnippet,
5 6 } from './spec.js'
6 7
7 8 // ---------------------------------------------------------------- atoms
@@ -14,70 +15,138 @@ function MethodChip({ method }) {
14 15 return <span className="method">{method}</span>
15 16 }
16 17
17 function CodeTabs({ ep }) {
18 const tabs = {
19 curl: curlSnippet(ep),
20 Python: pythonSnippet(ep),
21 JavaScript: jsSnippet(ep),
22 }
23 const [active, setActive] = useState('curl')
18 +function CopyButton({ text, label = 'Copy' }) {
24 19 const [copied, setCopied] = useState(false)
25 20 const copy = () => {
26 navigator.clipboard.writeText(tabs[active]).then(() => {
21 + navigator.clipboard.writeText(text).then(() => {
27 22 setCopied(true)
28 23 setTimeout(() => setCopied(false), 1200)
29 24 })
30 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] ?? '' }
31 34 return (
32 <div className="code-tabs">
33 <div className="code-tabs-bar">
34 <span className="code-tabs-label">Request</span>
35 {Object.keys(tabs).map(t => (
36 <button key={t} className={t === active ? 'tab active' : 'tab'}
37 onClick={() => setActive(t)}>{t}</button>
38 ))}
39 <button className="tab copy" onClick={copy}>{copied ? 'Copied ✓' : 'Copy'}</button>
40 </div>
41 <pre className="ink"><code>{tabs[active]}</code></pre>
42 </div>
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>
43 52 )
44 53 }
45 54
46 function ResponseBlock({ ep }) {
47 const [open, setOpen] = useState(false)
48 const [live, setLive] = useState(null)
55 +function Playground({ ep }) {
56 + const [values, setValues] = useState(() => defaultValues(ep))
57 + const [resp, setResp] = useState(null) // {status, ms, body, ok}
49 58 const [busy, setBusy] = useState(false)
50 const runLive = async () => {
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 () => {
51 74 setBusy(true)
75 + const t0 = performance.now()
52 76 try {
53 const r = await fetch(ep.example)
77 + const r = await fetch(url)
78 + const ms = Math.round(performance.now() - t0)
54 79 const text = await r.text()
55 let pretty = text
56 try { pretty = JSON.stringify(JSON.parse(text), null, 2) } catch { /* csv */ }
57 setLive(pretty.length > 5000 ? pretty.slice(0, 5000) + '\n… (truncated)' : pretty)
58 setOpen(true)
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 })
59 84 } catch (e) {
60 setLive(`Request failed: ${e.message}`)
61 setOpen(true)
85 + setResp({ status: '—', ok: false, ms: Math.round(performance.now() - t0), body: `Request failed: ${e.message}` })
62 86 } finally {
63 87 setBusy(false)
64 88 }
65 89 }
90 +
66 91 return (
67 <div className="response-block">
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 +
68 111 <div className="code-tabs-bar">
69 <span className="code-tabs-label">Response</span>
70 <button className={!open ? 'tab active' : 'tab'} onClick={() => setOpen(false)}>Example</button>
71 {live && <button className={open ? 'tab active' : 'tab'} onClick={() => setOpen(true)}>Live</button>}
72 <button className="tab live" onClick={runLive} disabled={busy}>
73 {busy ? 'Running…' : '▶ Run live'}
112 + <button className="tab run" onClick={run} disabled={busy}>
113 + {busy ? 'Running…' : '▶ Send request'}
74 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" />
75 122 </div>
76 <pre className="ink response"><code>{open && live ? live : ep.response}</code></pre>
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 + )}
77 144 </div>
78 145 )
79 146 }
80 147
148 +// ---------------------------------------------------------------- endpoint card
149 +
81 150 function FieldsTable({ kind }) {
82 151 const fields = kind === 'options' ? OPTION_FIELDS : BAR_FIELDS
83 152 const [open, setOpen] = useState(false)
@@ -132,8 +201,7 @@ function Endpoint({ ep, index }) {
132 201 </tbody>
133 202 </table>
134 203 )}
135 <CodeTabs ep={ep} />
136 <ResponseBlock ep={ep} />
204 + <Playground ep={ep} />
137 205 {ep.fields && <FieldsTable kind={ep.fields} />}
138 206 </div>
139 207 </article>
@@ -186,7 +254,6 @@ function LiveStatus() {
186 254 </tbody>
187 255 </table>
188 256 </div>
189 <p className="muted small">Ingestion is continuous — this inventory refreshes as new segments land in the lake.</p>
190 257 </div>
191 258 )
192 259 }
@@ -214,10 +281,11 @@ export default function App() {
214 281 <nav>
215 282 <Kicker>Getting started</Kicker>
216 283 <a href="#overview">Overview</a>
284 + <a href="#capabilities">What you can do</a>
217 285 <a href="#quickstart">Quick start</a>
218 286 <a href="#conventions">Conventions</a>
219 287 <a href="#coverage">Data coverage</a>
220 <Kicker>Endpoints</Kicker>
288 + <Kicker>Endpoints — live playground</Kicker>
221 289 {ENDPOINTS.map(e => (
222 290 <a key={e.id} href={`#${e.id}`} className="ep-link">
223 291 <span className="ep-method">{e.method}</span>{e.title}
@@ -239,31 +307,46 @@ export default function App() {
239 307 <em className="accent-word">wide open.</em>
240 308 </h1>
241 309 <p className="lede">
242 A free, keyless REST API over a deep historical archive:
243 <strong> 1-minute to daily bars</strong> for stocks, ETFs, futures,
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,
244 312 crypto, indices and FX — and <strong>complete end-of-day options
245 313 chains</strong> with quotes, implied volatility and Greeks back to 2010.
314 + Every endpoint below is a live playground.
246 315 </p>
247 316 <div className="hero-actions">
248 <a className="btn" href="#quickstart">Get started</a>
317 + <a className="btn" href="#bars">Try the playground</a>
249 318 <a className="btn ghost" href="/docs" target="_blank" rel="noreferrer">OpenAPI spec ↗</a>
250 319 </div>
251 320 <div className="stat-chips hero-chips">
321 + <div className="chip"><b>26.5B</b><span>rows in the lake</span></div>
252 322 <div className="chip"><b>7</b><span>asset classes</span></div>
253 323 <div className="chip"><b>5</b><span>timeframes — 1min → 1day</span></div>
254 324 <div className="chip"><b>5,800+</b><span>options underlyings</span></div>
255 <div className="chip"><b>2010</b><span>options history since</span></div>
325 + <div className="chip"><b>66</b><span>options quarters since 2010</span></div>
256 326 </div>
257 327 </section>
258 328
259 <section id="quickstart">
329 + <section id="capabilities">
260 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>
261 344 <h2>Quick start</h2>
262 345 <p>
263 346 No key, no registration, no SDK required. Every endpoint is a plain
264 347 GET on <code>{BASE_URL}</code> returning JSON — or CSV when you ask for it.
265 348 </p>
266 <pre className="ink"><code>{`# Daily AAPL bars, split+dividend adjusted
349 + <pre><code>{`# Daily AAPL bars, split+dividend adjusted
267 350 curl "${BASE_URL}/v1/bars/stock/AAPL?timeframe=1day&start=2024-01-01"
268 351
269 352 # A precise 4-hour window of 1-minute bars, across three tickers at once
@@ -283,7 +366,7 @@ curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
283 366 </section>
284 367
285 368 <section id="conventions">
286 <Kicker>02 — Getting started</Kicker>
369 + <Kicker>03 — Getting started</Kicker>
287 370 <h2>Conventions</h2>
288 371 <div className="conv-grid">
289 372 <div className="conv">
@@ -295,8 +378,8 @@ curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
295 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>
296 379 </div>
297 380 <div className="conv">
298 <h4>Formats</h4>
299 <p><code>format=json</code> (default, capped at 50k rows) or <code>format=csv</code> (capped at 2M rows) on every data endpoint. Filter with <code>start</code>/<code>end</code>, page with <code>limit</code> + <code>order</code>.</p>
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>
300 383 </div>
301 384 <div className="conv">
302 385 <h4>Options snapshots</h4>
@@ -306,7 +389,7 @@ curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
306 389 </section>
307 390
308 391 <section id="coverage">
309 <Kicker>03 — Getting started</Kicker>
392 + <Kicker>04 — Getting started</Kicker>
310 393 <h2>Data coverage</h2>
311 394 <div className="cards">
312 395 {ASSET_TYPES.map(a => (
@@ -334,7 +417,7 @@ curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
334 417 <section id="reference">
335 418 {Object.entries(grouped).map(([tag, eps]) => (
336 419 <div key={tag} className="ep-group">
337 <Kicker>API reference</Kicker>
420 + <Kicker>API reference — live playground</Kicker>
338 421 <h2>{tag}</h2>
339 422 {eps.map(e => (
340 423 <Endpoint key={e.id} ep={e}
@@ -380,8 +463,8 @@ curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
380 463 Boucher</strong>. Under the hood it is a DuckDB-over-Parquet data
381 464 lake: every instrument is a zstd-compressed Parquet file queried
382 465 in place with predicate pushdown — no database server between you
383 and the data, which is what keeps responses fast across billions
384 of rows.
466 + and the data, which is what keeps responses fast across 26.5
467 + billion rows.
385 468 </p>
386 469 <p>
387 470 Contact — <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>
@@ -389,7 +472,7 @@ curl "${BASE_URL}/v1/options/chain/AAPL?trade_date=2024-06-21"`}</code></pre>
389 472 </section>
390 473
391 474 <footer>
392 <div className="foot-brand">HF MARKET DATA</div>
475 + <div className="foot-brand">HF Market Data</div>
393 476 <div>
394 477 © {new Date().getFullYear()} Simon-Pierre Boucher ·{' '}
395 478 <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a> ·{' '}
modified hfmarketdata/web/src/spec.js +117 −80
@@ -13,6 +13,12 @@ export const ASSET_TYPES = [
13 13 { id: 'fx', name: 'FX', desc: 'Foreign-exchange pairs.', adjustments: ['none'], example: 'EURUSD' },
14 14 ]
15 15
16 +export const ASSET_IDS = ASSET_TYPES.map(a => a.id)
17 +export const ADJUSTMENTS_BY_ASSET = Object.fromEntries(
18 + ASSET_TYPES.map(a => [a.id, a.adjustments]))
19 +export const EXAMPLE_TICKER = Object.fromEntries(
20 + ASSET_TYPES.map(a => [a.id, a.example]))
21 +
16 22 export const TIMEFRAMES = ['1min', '5min', '30min', '1hour', '1day']
17 23
18 24 export const BAR_FIELDS = [
@@ -54,6 +60,17 @@ export const ERRORS = [
54 60 { code: 503, meaning: 'Unavailable', desc: 'The requested dataset segment has not been ingested yet.' },
55 61 ]
56 62
63 +export const CAPABILITIES = [
64 + { title: 'Any window, to the minute', desc: 'start / end accept full timestamps — pull exactly 4 hours of 1-minute bars, one session, or 20 years of dailies.' },
65 + { title: 'Whole watchlists in one call', desc: 'Up to 50 tickers per request on /v1/bars/{asset} — per-ticker limits, rows grouped by symbol.' },
66 + { title: 'Point-in-time snapshots', desc: 'The state of every ticker you follow at 10:35:00 on any given day — one request.' },
67 + { title: 'Options with the full Greek set', desc: 'Chains, expirations and single-contract life stories — quotes, bid/ask IV, OI, delta gamma vega theta rho.' },
68 + { title: 'Bulk CSV extraction', desc: 'format=csv streams up to 2,000,000 rows per request — pandas.read_csv straight from the URL.' },
69 + { title: 'Every adjustment variant', desc: 'Split-only, split+dividend or raw unadjusted equities; ratio / absolute / unadjusted futures rolls.' },
70 +]
71 +
72 +// t: input type ('select' | 'text'), opts: fixed list or dynamic key,
73 +// def: playground default, ph: placeholder
57 74 export const ENDPOINTS = [
58 75 {
59 76 id: 'health',
@@ -63,7 +80,6 @@ export const ENDPOINTS = [
63 80 tag: 'Meta',
64 81 desc: 'Service liveness probe. Returns service name, version and whether the data lake is mounted.',
65 82 params: [],
66 example: '/health',
67 83 response: `{
68 84 "status": "ok",
69 85 "service": "hfmarketdata-api",
@@ -77,16 +93,15 @@ export const ENDPOINTS = [
77 93 path: '/v1/status',
78 94 title: 'Dataset inventory',
79 95 tag: 'Meta',
80 desc: 'Full inventory of the data lake: every asset type, timeframe and adjustment with the number of instruments available, plus the list of options quarters. Coverage grows continuously as ingestion progresses.',
96 + desc: 'Full inventory of the data lake: every asset type, timeframe and adjustment with the number of instruments available, plus the list of options quarters.',
81 97 params: [],
82 example: '/v1/status',
83 98 response: `{
84 99 "datasets": {
85 100 "stock": {
86 "1day": { "adj_split": 7664, "adj_splitdiv": 7664, "UNADJUSTED": 7665 },
87 "1min": { "adj_split": 7659, "adj_splitdiv": 7659, "UNADJUSTED": 7660 }
101 + "1min": { "adj_split": 7669, "adj_splitdiv": 7669, "UNADJUSTED": 7670 },
102 + "1day": { "adj_split": 7664, "adj_splitdiv": 7664, "UNADJUSTED": 7665 }
88 103 },
89 "fx": { "1day": { "none": 79 } },
104 + "fx": { "1min": { "none": 79 } },
90 105 "options": {
91 106 "quarters": ["2010_q1", "2010_q2", "…", "2026_q2"],
92 107 "files_latest_quarter": 5972
@@ -102,13 +117,12 @@ export const ENDPOINTS = [
102 117 tag: 'Bars',
103 118 desc: 'Every instrument available for an asset type / timeframe / adjustment combination. Use it to discover coverage before requesting bars.',
104 119 params: [
105 { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
106 { name: 'timeframe', in: 'query', req: false, desc: '1min · 5min · 30min · 1hour · 1day — default 1day' },
107 { name: 'adjustment', in: 'query', req: false, desc: 'Adjustment variant; a sensible default is applied per asset type' },
108 { name: 'search', in: 'query', req: false, desc: 'Case-insensitive substring filter' },
109 { name: 'limit', in: 'query', req: false, desc: 'Max results — default 10 000' },
120 + { name: 'asset', in: 'path', req: true, desc: 'Asset class', t: 'select', opts: 'ASSETS', def: 'stock' },
121 + { name: 'timeframe', in: 'query', req: false, desc: 'Bar resolution — default 1day', t: 'select', opts: TIMEFRAMES, def: '1day' },
122 + { name: 'adjustment', in: 'query', req: false, desc: 'Adjustment variant; sensible default per asset type', t: 'select', opts: 'ADJ_BY_ASSET', def: '' },
123 + { name: 'search', in: 'query', req: false, desc: 'Case-insensitive substring filter', t: 'text', def: 'AAP', ph: 'AAP' },
124 + { name: 'limit', in: 'query', req: false, desc: 'Max results — default 10 000', t: 'text', def: '', ph: '10000' },
110 125 ],
111 example: '/v1/stock/tickers?timeframe=1day&search=AAP',
112 126 response: `{
113 127 "asset": "stock",
114 128 "timeframe": "1day",
@@ -123,19 +137,18 @@ export const ENDPOINTS = [
123 137 path: '/v1/bars/{asset}/{ticker}',
124 138 title: 'OHLCV bars',
125 139 tag: 'Bars',
126 desc: 'Historical bars for one instrument, 1-minute to daily. Daily futures bars also carry open interest. Switch to format=csv for bulk extraction — up to 2 million rows per request.',
140 + desc: 'Historical bars for one instrument, 1-minute to daily. start and end accept full timestamps, so any precise window works — one trading hour or twenty years. Daily futures bars also carry open interest.',
127 141 params: [
128 { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
129 { name: 'ticker', in: 'path', req: true, desc: 'Instrument symbol — AAPL, SPY, ES, BTCUSD, EURUSD…' },
130 { name: 'timeframe', in: 'query', req: false, desc: '1min · 5min · 30min · 1hour · 1day — default 1day' },
131 { name: 'adjustment', in: 'query', req: false, desc: 'stock/etf: adj_split · adj_splitdiv · UNADJUSTED — futures: contin_UNadj · contin_adj_ratio · contin_adj_absolute' },
132 { name: 'start', in: 'query', req: false, desc: 'ISO lower bound — 2024-01-01 or 2024-01-01 09:30:00' },
133 { name: 'end', in: 'query', req: false, desc: 'ISO upper bound' },
134 { name: 'order', in: 'query', req: false, desc: 'asc (default) · desc' },
135 { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 5 000 · JSON cap 50 000 · CSV cap 2 000 000' },
136 { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
142 + { name: 'asset', in: 'path', req: true, desc: 'Asset class', t: 'select', opts: 'ASSETS', def: 'stock' },
143 + { name: 'ticker', in: 'path', req: true, desc: 'Instrument symbol — AAPL, SPY, ES, BTCUSD, EURUSD…', t: 'text', def: 'AAPL', ph: 'AAPL' },
144 + { name: 'timeframe', in: 'query', req: false, desc: 'Bar resolution — default 1day', t: 'select', opts: TIMEFRAMES, def: '1day' },
145 + { name: 'adjustment', in: 'query', req: false, desc: 'stock/etf: adj_split · adj_splitdiv · UNADJUSTED — futures: contin_UNadj · contin_adj_ratio · contin_adj_absolute', t: 'select', opts: 'ADJ_BY_ASSET', def: '' },
146 + { name: 'start', in: 'query', req: false, desc: 'ISO lower bound — 2024-01-01 or 2024-06-03 09:30:00', t: 'text', def: '2024-06-03', ph: '2024-06-03 09:30:00' },
147 + { name: 'end', in: 'query', req: false, desc: 'ISO upper bound', t: 'text', def: '', ph: '2024-06-07' },
148 + { name: 'order', in: 'query', req: false, desc: 'Sort direction', t: 'select', opts: ['asc', 'desc'], def: 'asc' },
149 + { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 5 000 · JSON cap 50 000 · CSV cap 2 000 000', t: 'text', def: '5', ph: '5000' },
150 + { name: 'format', in: 'query', req: false, desc: 'Response format', t: 'select', opts: ['json', 'csv'], def: 'json' },
137 151 ],
138 example: '/v1/bars/stock/AAPL?timeframe=1day&start=2024-06-03&limit=2',
139 152 response: `{
140 153 "count": 2,
141 154 "data": [
@@ -155,17 +168,16 @@ export const ENDPOINTS = [
155 168 tag: 'Bars',
156 169 desc: 'Bars for several instruments in ONE call. Combine tickers with start/end to slice any precise window — e.g. 4 hours of 1-minute bars across a whole watchlist. The row limit applies per ticker, and rows come back grouped by ticker.',
157 170 params: [
158 { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
159 { name: 'tickers', in: 'query', req: true, desc: 'Comma-separated list — AAPL,MSFT,TSLA (max 50)' },
160 { name: 'timeframe', in: 'query', req: false, desc: '1min · 5min · 30min · 1hour · 1day — default 1day' },
161 { name: 'adjustment', in: 'query', req: false, desc: 'Same variants as single-ticker bars' },
162 { name: 'start', in: 'query', req: false, desc: 'ISO lower bound — precise to the minute' },
163 { name: 'end', in: 'query', req: false, desc: 'ISO upper bound' },
164 { name: 'order', in: 'query', req: false, desc: 'asc (default) · desc' },
165 { name: 'limit', in: 'query', req: false, desc: 'Max rows PER TICKER — default 5 000' },
166 { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
171 + { name: 'asset', in: 'path', req: true, desc: 'Asset class', t: 'select', opts: 'ASSETS', def: 'stock' },
172 + { name: 'tickers', in: 'query', req: true, desc: 'Comma-separated list — max 50', t: 'text', def: 'AAPL,MSFT,NVDA', ph: 'AAPL,MSFT,NVDA' },
173 + { name: 'timeframe', in: 'query', req: false, desc: 'Bar resolution — default 1day', t: 'select', opts: TIMEFRAMES, def: '1min' },
174 + { name: 'adjustment', in: 'query', req: false, desc: 'Same variants as single-ticker bars', t: 'select', opts: 'ADJ_BY_ASSET', def: '' },
175 + { name: 'start', in: 'query', req: false, desc: 'ISO lower bound — precise to the minute', t: 'text', def: '2024-06-03 09:30:00', ph: '2024-06-03 09:30:00' },
176 + { name: 'end', in: 'query', req: false, desc: 'ISO upper bound', t: 'text', def: '2024-06-03 13:30:00', ph: '2024-06-03 13:30:00' },
177 + { name: 'order', in: 'query', req: false, desc: 'Sort direction', t: 'select', opts: ['asc', 'desc'], def: 'asc' },
178 + { name: 'limit', in: 'query', req: false, desc: 'Max rows PER TICKER — default 5 000', t: 'text', def: '3', ph: '5000' },
179 + { name: 'format', in: 'query', req: false, desc: 'Response format', t: 'select', opts: ['json', 'csv'], def: 'json' },
167 180 ],
168 example: '/v1/bars/stock?tickers=AAPL,MSFT,NVDA&timeframe=1min&start=2024-06-03%2009:30:00&end=2024-06-03%2013:30:00',
169 181 response: `{
170 182 "count": 720,
171 183 "data": [
@@ -184,14 +196,13 @@ export const ENDPOINTS = [
184 196 tag: 'Bars',
185 197 desc: 'The state of a whole watchlist at one precise moment: for each requested instrument, the last bar at or before the given timestamp. Perfect for reconstructing a cross-section of the market at 10:35:00 on any given day.',
186 198 params: [
187 { name: 'asset', in: 'path', req: true, desc: 'stock · etf · futures · futures_contracts · crypto · index · fx' },
188 { name: 'tickers', in: 'query', req: true, desc: 'Comma-separated list — AAPL,MSFT,TSLA (max 50)' },
189 { name: 'at', in: 'query', req: true, desc: 'The precise moment — 2024-06-03 10:35:00' },
190 { name: 'timeframe', in: 'query', req: false, desc: '1min (default) · 5min · 30min · 1hour · 1day' },
191 { name: 'adjustment', in: 'query', req: false, desc: 'Same variants as bars' },
192 { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
199 + { name: 'asset', in: 'path', req: true, desc: 'Asset class', t: 'select', opts: 'ASSETS', def: 'stock' },
200 + { name: 'tickers', in: 'query', req: true, desc: 'Comma-separated list — max 50', t: 'text', def: 'AAPL,MSFT,NVDA', ph: 'AAPL,MSFT,NVDA' },
201 + { name: 'at', in: 'query', req: true, desc: 'The precise moment', t: 'text', def: '2024-06-03 10:35:00', ph: '2024-06-03 10:35:00' },
202 + { name: 'timeframe', in: 'query', req: false, desc: 'Bar resolution — default 1min', t: 'select', opts: TIMEFRAMES, def: '1min' },
203 + { name: 'adjustment', in: 'query', req: false, desc: 'Same variants as bars', t: 'select', opts: 'ADJ_BY_ASSET', def: '' },
204 + { name: 'format', in: 'query', req: false, desc: 'Response format', t: 'select', opts: ['json', 'csv'], def: 'json' },
193 205 ],
194 example: '/v1/snapshot/stock?tickers=AAPL,MSFT,NVDA&at=2024-06-03%2010:35:00',
195 206 response: `{
196 207 "count": 3,
197 208 "data": [
@@ -210,7 +221,6 @@ export const ENDPOINTS = [
210 221 tag: 'Options',
211 222 desc: 'The quarterly archives available, from 2010_q1 through the current quarter. Each quarter holds one end-of-day chain file per underlying.',
212 223 params: [],
213 example: '/v1/options/quarters',
214 224 response: `{
215 225 "quarters": ["2010_q1", "2010_q2", "2010_q3", "…", "2026_q1", "2026_q2"]
216 226 }`,
@@ -223,11 +233,10 @@ export const ENDPOINTS = [
223 233 tag: 'Options',
224 234 desc: 'Underlyings with options data in a given quarter — 5,800+ US equities and indices.',
225 235 params: [
226 { name: 'quarter', in: 'query', req: false, desc: 'e.g. 2024_q4 — default: latest quarter' },
227 { name: 'search', in: 'query', req: false, desc: 'Substring filter' },
228 { name: 'limit', in: 'query', req: false, desc: 'Max results — default 10 000' },
236 + { name: 'quarter', in: 'query', req: false, desc: 'e.g. 2024_q4 — default: latest quarter', t: 'text', def: '', ph: '2024_q4' },
237 + { name: 'search', in: 'query', req: false, desc: 'Substring filter', t: 'text', def: 'TSL', ph: 'TSL' },
238 + { name: 'limit', in: 'query', req: false, desc: 'Max results — default 10 000', t: 'text', def: '', ph: '10000' },
229 239 ],
230 example: '/v1/options/tickers?search=TSL',
231 240 response: `{
232 241 "quarter": "2026_q2",
233 242 "count": 14,
@@ -242,25 +251,24 @@ export const ENDPOINTS = [
242 251 tag: 'Options',
243 252 desc: 'The complete end-of-day chain for an underlying on one trade date: last price, bid/ask quotes, bid/ask implied volatility, open interest, volume and the full set of Greeks. Snapshots are sampled 30 seconds before the close to avoid rebalancing-order noise.',
244 253 params: [
245 { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol — AAPL, SPX…' },
246 { name: 'trade_date', in: 'query', req: false, desc: 'yyyy-mm-dd — default: latest available date' },
247 { name: 'expiry', in: 'query', req: false, desc: 'Restrict to a single expiry date' },
248 { name: 'call_put', in: 'query', req: false, desc: 'c · p' },
249 { name: 'strike_min', in: 'query', req: false, desc: 'Minimum strike' },
250 { name: 'strike_max', in: 'query', req: false, desc: 'Maximum strike' },
251 { name: 'min_volume', in: 'query', req: false, desc: 'Minimum traded volume' },
252 { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 20 000' },
253 { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
254 + { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol — AAPL, SPX…', t: 'text', def: 'AAPL', ph: 'AAPL' },
255 + { name: 'trade_date', in: 'query', req: false, desc: 'yyyy-mm-dd — default: latest available date', t: 'text', def: '2024-06-21', ph: '2024-06-21' },
256 + { name: 'expiry', in: 'query', req: false, desc: 'Restrict to a single expiry date', t: 'text', def: '', ph: '2024-12-20' },
257 + { name: 'call_put', in: 'query', req: false, desc: 'Side', t: 'select', opts: ['', 'c', 'p'], def: 'c' },
258 + { name: 'strike_min', in: 'query', req: false, desc: 'Minimum strike', t: 'text', def: '200', ph: '150' },
259 + { name: 'strike_max', in: 'query', req: false, desc: 'Maximum strike', t: 'text', def: '210', ph: '250' },
260 + { name: 'min_volume', in: 'query', req: false, desc: 'Minimum traded volume', t: 'text', def: '', ph: '10' },
261 + { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 20 000', t: 'text', def: '5', ph: '20000' },
262 + { name: 'format', in: 'query', req: false, desc: 'Response format', t: 'select', opts: ['json', 'csv'], def: 'json' },
254 263 ],
255 example: '/v1/options/chain/AAPL?call_put=c&strike_min=200&strike_max=210&limit=1',
256 264 response: `{
257 265 "count": 1,
258 266 "data": [
259 { "ticker": "AAPL", "trade_date": "2026-08-07", "strike": 200.0,
260 "expiry": "2026-08-21", "call_put": "c", "last_price": 113.25,
261 "bid": 111.25, "ask": 115.0, "bid_iv": 0.412, "ask_iv": 0.487,
262 "open_interest": 5.0, "volume": 12.0, "delta": 0.998,
263 "gamma": 0.0001, "vega": 0.0012, "theta": -0.0034, "rho": 0.0274 }
267 + { "ticker": "AAPL", "trade_date": "2024-06-21", "strike": 200.0,
268 + "expiry": "2024-06-28", "call_put": "c", "last_price": 8.0,
269 + "bid": 7.85, "ask": 8.15, "bid_iv": 0.264, "ask_iv": 0.271,
270 + "open_interest": 5231.0, "volume": 1930.0, "delta": 0.694,
271 + "gamma": 0.041, "vega": 0.104, "theta": -0.213, "rho": 0.026 }
264 272 ]
265 273 }`,
266 274 fields: 'options',
@@ -273,13 +281,12 @@ export const ENDPOINTS = [
273 281 tag: 'Options',
274 282 desc: 'Available expiry dates for an underlying, optionally as of one trade date — the natural first call before requesting a filtered chain.',
275 283 params: [
276 { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol' },
277 { name: 'trade_date', in: 'query', req: false, desc: 'yyyy-mm-dd' },
284 + { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol', t: 'text', def: 'AAPL', ph: 'AAPL' },
285 + { name: 'trade_date', in: 'query', req: false, desc: 'yyyy-mm-dd', t: 'text', def: '2024-06-21', ph: '2024-06-21' },
278 286 ],
279 example: '/v1/options/expirations/AAPL',
280 287 response: `{
281 288 "ticker": "AAPL",
282 "expirations": ["2026-08-21", "2026-08-28", "2026-09-18", "…"]
289 + "expirations": ["2024-06-28", "2024-07-05", "2024-07-19", "…"]
283 290 }`,
284 291 },
285 292 {
@@ -290,40 +297,70 @@ export const ENDPOINTS = [
290 297 tag: 'Options',
291 298 desc: 'The daily time series of one specific contract — strike + expiry + side — across its entire life: price, quotes, implied volatility, open interest and Greeks, day by day.',
292 299 params: [
293 { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol' },
294 { name: 'strike', in: 'query', req: true, desc: 'Contract strike' },
295 { name: 'expiry', in: 'query', req: true, desc: 'Contract expiry — yyyy-mm-dd' },
296 { name: 'call_put', in: 'query', req: true, desc: 'c · p' },
297 { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 5 000' },
298 { name: 'format', in: 'query', req: false, desc: 'json (default) · csv' },
300 + { name: 'ticker', in: 'path', req: true, desc: 'Underlying symbol', t: 'text', def: 'AAPL', ph: 'AAPL' },
301 + { name: 'strike', in: 'query', req: true, desc: 'Contract strike', t: 'text', def: '200', ph: '200' },
302 + { name: 'expiry', in: 'query', req: true, desc: 'Contract expiry — yyyy-mm-dd', t: 'text', def: '2024-12-20', ph: '2024-12-20' },
303 + { name: 'call_put', in: 'query', req: true, desc: 'Side', t: 'select', opts: ['c', 'p'], def: 'c' },
304 + { name: 'limit', in: 'query', req: false, desc: 'Max rows — default 5 000', t: 'text', def: '5', ph: '5000' },
305 + { name: 'format', in: 'query', req: false, desc: 'Response format', t: 'select', opts: ['json', 'csv'], def: 'json' },
299 306 ],
300 example: '/v1/options/history/AAPL?strike=200&expiry=2026-12-18&call_put=c',
301 307 response: `{
302 308 "count": 214,
303 309 "data": [
304 { "ticker": "AAPL", "trade_date": "2026-01-02", "strike": 200.0,
305 "expiry": "2026-12-18", "call_put": "c", "last_price": 41.3, "…": "…" }
310 + { "ticker": "AAPL", "trade_date": "2024-01-02", "strike": 200.0,
311 + "expiry": "2024-12-20", "call_put": "c", "last_price": 11.2, "…": "…" }
306 312 ]
307 313 }`,
308 314 fields: 'options',
309 315 },
310 316 ]
311 317
312 export function curlSnippet(ep) {
313 return `curl "${BASE_URL}${ep.example}"`
318 +// ---- URL building + code snippets (shared by docs & playground) -------------
319 +
320 +export function buildUrl(ep, values) {
321 + let path = ep.path
322 + const query = []
323 + for (const p of ep.params) {
324 + const v = (values[p.name] ?? '').toString().trim()
325 + if (p.in === 'path') {
326 + path = path.replace(`{${p.name}}`, encodeURIComponent(v || `{${p.name}}`))
327 + } else if (v !== '') {
328 + query.push(`${p.name}=${encodeURIComponent(v).replace(/%2C/g, ',')}`)
329 + }
330 + }
331 + return path + (query.length ? '?' + query.join('&') : '')
332 +}
333 +
334 +export function defaultValues(ep) {
335 + const v = {}
336 + for (const p of ep.params) v[p.name] = p.def ?? ''
337 + return v
338 +}
339 +
340 +export function paramOptions(p, values) {
341 + if (p.opts === 'ASSETS') return ASSET_IDS
342 + if (p.opts === 'ADJ_BY_ASSET') {
343 + const asset = values.asset || 'stock'
344 + return ['', ...(ADJUSTMENTS_BY_ASSET[asset] || [])]
345 + }
346 + return p.opts || []
347 +}
348 +
349 +export function curlSnippet(url) {
350 + return `curl "${BASE_URL}${url}"`
314 351 }
315 352
316 export function pythonSnippet(ep) {
353 +export function pythonSnippet(url) {
317 354 return `import requests
318 355
319 r = requests.get("${BASE_URL}${ep.example}")
356 +r = requests.get("${BASE_URL}${url}")
320 357 r.raise_for_status()
321 358 data = r.json()
322 print(f"{data.get('count', len(data))} rows")`
359 +print(f"{data.get('count', '')} rows")`
323 360 }
324 361
325 export function jsSnippet(ep) {
326 return `const res = await fetch("${BASE_URL}${ep.example}");
362 +export function jsSnippet(url) {
363 + return `const res = await fetch("${BASE_URL}${url}");
327 364 const data = await res.json();
328 365 console.log(data);`
329 366 }
modified hfmarketdata/web/src/styles.css +124 −0
@@ -384,6 +384,130 @@ td.num { font-family: var(--mono); font-size: 0.78rem; text-align: right; }
384 384 .tab:disabled { opacity: 0.6; cursor: default; }
385 385 pre.response { max-height: 340px; overflow: auto; }
386 386
387 +/* ---------------------------------------------------------------- playground */
388 +
389 +.playground {
390 + margin-top: 18px;
391 + border: 1px solid var(--line);
392 + border-radius: 12px;
393 + background: linear-gradient(180deg, var(--accent-wash) 0, var(--card) 90px);
394 + padding: 16px 18px 18px;
395 +}
396 +.pg-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
397 +.pg-title {
398 + font-family: var(--serif);
399 + font-style: italic;
400 + font-size: 1.05rem;
401 + color: var(--accent-deep);
402 +}
403 +.pg-hint { font-size: 0.72rem; color: var(--muted); }
404 +
405 +.pg-grid {
406 + display: grid;
407 + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
408 + gap: 10px 12px;
409 + margin-bottom: 14px;
410 +}
411 +.pg-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
412 +.pg-label {
413 + font-family: var(--mono);
414 + font-size: 0.68rem;
415 + font-weight: 600;
416 + color: var(--ink-soft);
417 + display: flex;
418 + align-items: baseline;
419 + gap: 6px;
420 +}
421 +.pg-req {
422 + font-style: normal;
423 + font-size: 0.56rem;
424 + color: var(--accent-deep);
425 + background: var(--accent-wash);
426 + border: 1px solid #d9e2fa;
427 + border-radius: 999px;
428 + padding: 0 6px;
429 +}
430 +.pg-in { margin-left: auto; font-size: 0.58rem; color: var(--muted); font-weight: 400; }
431 +.pg-field input, .pg-field select {
432 + font-family: var(--mono);
433 + font-size: 0.78rem;
434 + color: var(--ink);
435 + background: var(--card);
436 + border: 1px solid var(--line-strong);
437 + border-radius: 8px;
438 + padding: 7px 10px;
439 + outline: none;
440 + width: 100%;
441 + transition: border-color 0.12s, box-shadow 0.12s;
442 + -webkit-appearance: none;
443 + appearance: none;
444 +}
445 +.pg-field select {
446 + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a8f98' fill='none' stroke-width='1.5'/%3E%3C/svg%3E");
447 + background-repeat: no-repeat;
448 + background-position: right 10px center;
449 + padding-right: 26px;
450 +}
451 +.pg-field input:focus, .pg-field select:focus {
452 + border-color: var(--accent);
453 + box-shadow: 0 0 0 3px var(--accent-wash);
454 +}
455 +
456 +.pg-url {
457 + display: flex;
458 + align-items: center;
459 + gap: 10px;
460 + background: var(--card);
461 + border: 1px solid var(--line);
462 + border-radius: 8px;
463 + padding: 8px 12px;
464 + margin-bottom: 12px;
465 + overflow-x: auto;
466 +}
467 +.pg-url-method {
468 + font-family: var(--mono);
469 + font-size: 0.62rem;
470 + font-weight: 600;
471 + color: var(--accent-deep);
472 + letter-spacing: 0.06em;
473 + flex-shrink: 0;
474 +}
475 +.pg-url code {
476 + background: none; border: none; padding: 0; margin: 0;
477 + font-size: 0.74rem;
478 + white-space: nowrap;
479 + color: var(--ink-soft);
480 +}
481 +
482 +.tab.run {
483 + background: var(--accent);
484 + border-color: var(--accent);
485 + color: white;
486 + font-weight: 600;
487 +}
488 +.tab.run:hover { background: var(--accent-deep); }
489 +.pg-spacer { margin-left: auto; }
490 +
491 +.pg-response { margin-top: 12px; }
492 +.pg-resp-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
493 +.pg-status {
494 + font-family: var(--mono);
495 + font-size: 0.68rem;
496 + font-weight: 600;
497 + border-radius: 999px;
498 + padding: 2px 10px;
499 +}
500 +.pg-status.ok { color: #116632; background: #e3f5ea; border: 1px solid #bfe6cd; }
501 +.pg-status.err { color: #9e2a25; background: #fdeae9; border: 1px solid #f5c9c6; }
502 +.pg-ms { font-family: var(--mono); font-size: 0.68rem; color: var(--muted); }
503 +.pg-resp-label {
504 + font-size: 0.62rem;
505 + font-weight: 700;
506 + letter-spacing: 0.14em;
507 + text-transform: uppercase;
508 + color: var(--muted);
509 +}
510 +
387 511 /* fields accordion */
388 512 .fields { margin-top: 16px; }
389 513 .fields-toggle {
390 514