docs: 18 guides MDX (quickstart, auth, rate limits, formats avec benchmark réel, futures contrats + schémas SVG, options, fuseaux, bulk, 3 recettes, 6 pages fundamentals autour du bilan Apple T2 FY2024, versioning) + changelog v2.0.0 ; exemples réels capturés (content/examples), échantillon P/E AAPL, scripts fetch-examples / bench ; remark-gfm pour les tableaux, manifest Vite, corrections de débordement mobile
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
41 changed files +3,314 −65
modified
hfmarketdata/web/content/changelog.mdx
+1 −1
@@ -17,7 +17,7 @@ The v2 platform release. Every `/v1/*` endpoint that existed before keeps its pa | ||
| 17 | 17 | - **Uniform error envelope** — `{"error": {"code", "message", "docs", "type?", "details?"}}` on every error; the legacy `detail` field is preserved. All codes are listed on the [Errors](/docs/errors) page. |
| 18 | 18 | - **Response formats** — `format=parquet` on tabular v2 endpoints (counts half against the rows quota), `X-Row-Count` header, ETag / `If-None-Match` (304 responses are free). |
| 19 | 19 | - **OpenAPI 3.1** at `/openapi.json` with tags, markdown descriptions, examples and per-operation error codes. This documentation is generated from it. |
| 20 | −- **Website** — new documentation (3-column reference generated from the spec, guides, instant search with ⌘K), playground with deep links (`/playground?ep=…`), pricing/limits page, live status page, dashboard, light theme, MCP server and skills pack under Integrations. | |
| 20 | +- **Website** — new documentation (3-column reference generated from the spec, guides, instant search with ⌘K), playground with deep links (`/playground?ep=…`), access & limits page, live status page, dashboard, light theme, MCP server and skills pack under Integrations. | |
| 21 | 21 | |
| 22 | 22 | ### Changed |
| 23 | 23 | |
added
hfmarketdata/web/content/examples/bars_multi_v1_bars__asset__get.json
+70 −0
@@ -0,0 +1,70 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "bars_multi_v1_bars__asset__get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/bars/{asset}", | |
| 5 | + "params": { | |
| 6 | + "asset": "stock", | |
| 7 | + "tickers": "AAPL,MSFT,NVDA", | |
| 8 | + "timeframe": "1min", | |
| 9 | + "start": "2024-06-03 09:30:00", | |
| 10 | + "end": "2024-06-03 09:35:00", | |
| 11 | + "limit": "3" | |
| 12 | + }, | |
| 13 | + "url": "https://www.hfmarketdata.io/v1/bars/stock?tickers=AAPL,MSFT,NVDA&timeframe=1min&start=2024-06-03%2009:30:00&end=2024-06-03%2009:35:00&limit=3", | |
| 14 | + "status": 200, | |
| 15 | + "headers": { | |
| 16 | + "content-type": "application/json" | |
| 17 | + }, | |
| 18 | + "captured_at": "2026-09-04T23:37:54.477Z", | |
| 19 | + "body": { | |
| 20 | + "count": 9, | |
| 21 | + "data": [ | |
| 22 | + { | |
| 23 | + "ticker": "AAPL", | |
| 24 | + "datetime": "2024-06-03 09:30:00", | |
| 25 | + "open": 191.3528, | |
| 26 | + "high": 191.6799, | |
| 27 | + "low": 191.1446, | |
| 28 | + "close": 191.6601, | |
| 29 | + "volume": 1521665 | |
| 30 | + }, | |
| 31 | + { | |
| 32 | + "ticker": "AAPL", | |
| 33 | + "datetime": "2024-06-03 09:31:00", | |
| 34 | + "open": 191.6403, | |
| 35 | + "high": 191.7295, | |
| 36 | + "low": 190.9265, | |
| 37 | + "close": 191.1248, | |
| 38 | + "volume": 385549 | |
| 39 | + }, | |
| 40 | + { | |
| 41 | + "ticker": "AAPL", | |
| 42 | + "datetime": "2024-06-03 09:32:00", | |
| 43 | + "open": 191.1198, | |
| 44 | + "high": 191.2338, | |
| 45 | + "low": 190.867, | |
| 46 | + "close": 191.1049, | |
| 47 | + "volume": 225777 | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + "ticker": "MSFT", | |
| 51 | + "datetime": "2024-06-03 09:30:00", | |
| 52 | + "open": 409.0976, | |
| 53 | + "high": 409.8361, | |
| 54 | + "low": 408.3888, | |
| 55 | + "close": 408.5069, | |
| 56 | + "volume": 489654 | |
| 57 | + }, | |
| 58 | + { | |
| 59 | + "ticker": "MSFT", | |
| 60 | + "datetime": "2024-06-03 09:31:00", | |
| 61 | + "open": 408.438, | |
| 62 | + "high": 409.4029, | |
| 63 | + "low": 408.3297, | |
| 64 | + "close": 408.8614, | |
| 65 | + "volume": 60686 | |
| 66 | + } | |
| 67 | + ], | |
| 68 | + "_truncated": "4 more rows omitted in this example" | |
| 69 | + } | |
| 70 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/bars_v1_bars__asset___ticker__get.json
+69 −0
@@ -0,0 +1,69 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "bars_v1_bars__asset___ticker__get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/bars/{asset}/{ticker}", | |
| 5 | + "params": { | |
| 6 | + "asset": "stock", | |
| 7 | + "ticker": "AAPL", | |
| 8 | + "timeframe": "1day", | |
| 9 | + "start": "2024-06-03", | |
| 10 | + "end": "2024-06-07", | |
| 11 | + "limit": "5" | |
| 12 | + }, | |
| 13 | + "url": "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day&start=2024-06-03&end=2024-06-07&limit=5", | |
| 14 | + "status": 200, | |
| 15 | + "headers": { | |
| 16 | + "content-type": "application/json" | |
| 17 | + }, | |
| 18 | + "captured_at": "2026-09-04T23:37:54.341Z", | |
| 19 | + "body": { | |
| 20 | + "count": 5, | |
| 21 | + "data": [ | |
| 22 | + { | |
| 23 | + "ticker": "AAPL", | |
| 24 | + "datetime": "2024-06-03", | |
| 25 | + "open": 191.2434, | |
| 26 | + "high": 193.3154, | |
| 27 | + "low": 190.8666, | |
| 28 | + "close": 192.3637, | |
| 29 | + "volume": 50080500 | |
| 30 | + }, | |
| 31 | + { | |
| 32 | + "ticker": "AAPL", | |
| 33 | + "datetime": "2024-06-04", | |
| 34 | + "open": 192.9684, | |
| 35 | + "high": 193.6426, | |
| 36 | + "low": 191.3722, | |
| 37 | + "close": 192.6809, | |
| 38 | + "volume": 47471400 | |
| 39 | + }, | |
| 40 | + { | |
| 41 | + "ticker": "AAPL", | |
| 42 | + "datetime": "2024-06-05", | |
| 43 | + "open": 193.7219, | |
| 44 | + "high": 195.209, | |
| 45 | + "low": 193.1964, | |
| 46 | + "close": 194.1879, | |
| 47 | + "volume": 54156800 | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + "ticker": "AAPL", | |
| 51 | + "datetime": "2024-06-06", | |
| 52 | + "open": 194.0094, | |
| 53 | + "high": 194.8124, | |
| 54 | + "low": 192.5025, | |
| 55 | + "close": 192.8098, | |
| 56 | + "volume": 41181800 | |
| 57 | + }, | |
| 58 | + { | |
| 59 | + "ticker": "AAPL", | |
| 60 | + "datetime": "2024-06-07", | |
| 61 | + "open": 192.9783, | |
| 62 | + "high": 195.2487, | |
| 63 | + "low": 192.4727, | |
| 64 | + "close": 195.1991, | |
| 65 | + "volume": 53103900 | |
| 66 | + } | |
| 67 | + ] | |
| 68 | + } | |
| 69 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/health_health_get.json
+18 −0
@@ -0,0 +1,18 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "health_health_get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/health", | |
| 5 | + "params": {}, | |
| 6 | + "url": "https://www.hfmarketdata.io/health", | |
| 7 | + "status": 200, | |
| 8 | + "headers": { | |
| 9 | + "content-type": "application/json" | |
| 10 | + }, | |
| 11 | + "captured_at": "2026-09-04T23:37:53.274Z", | |
| 12 | + "body": { | |
| 13 | + "status": "ok", | |
| 14 | + "service": "hfmarketdata-api", | |
| 15 | + "version": "1.0.0", | |
| 16 | + "data_root_present": true | |
| 17 | + } | |
| 18 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/opt_chain_v1_options_chain__ticker__get.json
+119 −0
@@ -0,0 +1,119 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "opt_chain_v1_options_chain__ticker__get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/options/chain/{ticker}", | |
| 5 | + "params": { | |
| 6 | + "ticker": "AAPL", | |
| 7 | + "trade_date": "2024-06-21", | |
| 8 | + "call_put": "c", | |
| 9 | + "strike_min": "200", | |
| 10 | + "strike_max": "210", | |
| 11 | + "limit": "5" | |
| 12 | + }, | |
| 13 | + "url": "https://www.hfmarketdata.io/v1/options/chain/AAPL?trade_date=2024-06-21&call_put=c&strike_min=200&strike_max=210&limit=5", | |
| 14 | + "status": 200, | |
| 15 | + "headers": { | |
| 16 | + "content-type": "application/json" | |
| 17 | + }, | |
| 18 | + "captured_at": "2026-09-04T23:37:55.585Z", | |
| 19 | + "body": { | |
| 20 | + "count": 5, | |
| 21 | + "data": [ | |
| 22 | + { | |
| 23 | + "ticker": "AAPL", | |
| 24 | + "trade_date": "2024-06-21", | |
| 25 | + "strike": 200, | |
| 26 | + "expiry": "2024-06-21", | |
| 27 | + "call_put": "c", | |
| 28 | + "last_price": 10.5, | |
| 29 | + "bid": 10.35, | |
| 30 | + "ask": 10.6, | |
| 31 | + "bid_iv": 0, | |
| 32 | + "ask_iv": 2.5183, | |
| 33 | + "open_interest": 48861, | |
| 34 | + "volume": 11238, | |
| 35 | + "delta": 1, | |
| 36 | + "gamma": 0, | |
| 37 | + "vega": 0, | |
| 38 | + "theta": -0.99, | |
| 39 | + "rho": 0.0003 | |
| 40 | + }, | |
| 41 | + { | |
| 42 | + "ticker": "AAPL", | |
| 43 | + "trade_date": "2024-06-21", | |
| 44 | + "strike": 202.5, | |
| 45 | + "expiry": "2024-06-21", | |
| 46 | + "call_put": "c", | |
| 47 | + "last_price": 8, | |
| 48 | + "bid": 7.9, | |
| 49 | + "ask": 8.1, | |
| 50 | + "bid_iv": 0, | |
| 51 | + "ask_iv": 2.0086, | |
| 52 | + "open_interest": 13402, | |
| 53 | + "volume": 1107, | |
| 54 | + "delta": 1, | |
| 55 | + "gamma": 0, | |
| 56 | + "vega": 0, | |
| 57 | + "theta": -1.0026, | |
| 58 | + "rho": 0.0003 | |
| 59 | + }, | |
| 60 | + { | |
| 61 | + "ticker": "AAPL", | |
| 62 | + "trade_date": "2024-06-21", | |
| 63 | + "strike": 205, | |
| 64 | + "expiry": "2024-06-21", | |
| 65 | + "call_put": "c", | |
| 66 | + "last_price": 5.5, | |
| 67 | + "bid": 5.4, | |
| 68 | + "ask": 5.6, | |
| 69 | + "bid_iv": 0, | |
| 70 | + "ask_iv": 1.4836, | |
| 71 | + "open_interest": 24344, | |
| 72 | + "volume": 17883, | |
| 73 | + "delta": 0.9991, | |
| 74 | + "gamma": 0.0017, | |
| 75 | + "vega": 0.0001, | |
| 76 | + "theta": -1.0674, | |
| 77 | + "rho": 0.0003 | |
| 78 | + }, | |
| 79 | + { | |
| 80 | + "ticker": "AAPL", | |
| 81 | + "trade_date": "2024-06-21", | |
| 82 | + "strike": 207.5, | |
| 83 | + "expiry": "2024-06-21", | |
| 84 | + "call_put": "c", | |
| 85 | + "last_price": 3.02, | |
| 86 | + "bid": 2.96, | |
| 87 | + "ask": 3.1, | |
| 88 | + "bid_iv": 0, | |
| 89 | + "ask_iv": 0.9295, | |
| 90 | + "open_interest": 21853, | |
| 91 | + "volume": 7029, | |
| 92 | + "delta": 0.9658, | |
| 93 | + "gamma": 0.0456, | |
| 94 | + "vega": 0.002, | |
| 95 | + "theta": -2.259, | |
| 96 | + "rho": 0.0003 | |
| 97 | + }, | |
| 98 | + { | |
| 99 | + "ticker": "AAPL", | |
| 100 | + "trade_date": "2024-06-21", | |
| 101 | + "strike": 210, | |
| 102 | + "expiry": "2024-06-21", | |
| 103 | + "call_put": "c", | |
| 104 | + "last_price": 0.85, | |
| 105 | + "bid": 0.83, | |
| 106 | + "ask": 0.87, | |
| 107 | + "bid_iv": 0.5521, | |
| 108 | + "ask_iv": 0.5959, | |
| 109 | + "open_interest": 42604, | |
| 110 | + "volume": 71812, | |
| 111 | + "delta": 0.6401, | |
| 112 | + "gamma": 0.2656, | |
| 113 | + "vega": 0.0091, | |
| 114 | + "theta": -5.9742, | |
| 115 | + "rho": 0.0002 | |
| 116 | + } | |
| 117 | + ] | |
| 118 | + } | |
| 119 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/opt_expirations_v1_options_expirations__ticker__get.json
+33 −0
@@ -0,0 +1,33 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "opt_expirations_v1_options_expirations__ticker__get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/options/expirations/{ticker}", | |
| 5 | + "params": { | |
| 6 | + "ticker": "AAPL", | |
| 7 | + "trade_date": "2024-06-21" | |
| 8 | + }, | |
| 9 | + "url": "https://www.hfmarketdata.io/v1/options/expirations/AAPL?trade_date=2024-06-21", | |
| 10 | + "status": 200, | |
| 11 | + "headers": { | |
| 12 | + "content-type": "application/json" | |
| 13 | + }, | |
| 14 | + "captured_at": "2026-09-04T23:37:55.674Z", | |
| 15 | + "body": { | |
| 16 | + "ticker": "AAPL", | |
| 17 | + "expirations": [ | |
| 18 | + "2024-06-21", | |
| 19 | + "2024-06-28", | |
| 20 | + "2024-07-05", | |
| 21 | + "2024-07-12", | |
| 22 | + "2024-07-19", | |
| 23 | + "2024-07-26", | |
| 24 | + "2024-08-02", | |
| 25 | + "2024-08-16", | |
| 26 | + "2024-09-20", | |
| 27 | + "2024-10-18", | |
| 28 | + "2024-11-15", | |
| 29 | + "2024-12-20", | |
| 30 | + "… 8 more" | |
| 31 | + ] | |
| 32 | + } | |
| 33 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/opt_history_v1_options_history__ticker__get.json
+118 −0
@@ -0,0 +1,118 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "opt_history_v1_options_history__ticker__get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/options/history/{ticker}", | |
| 5 | + "params": { | |
| 6 | + "ticker": "AAPL", | |
| 7 | + "strike": "200", | |
| 8 | + "expiry": "2024-12-20", | |
| 9 | + "call_put": "c", | |
| 10 | + "limit": "5" | |
| 11 | + }, | |
| 12 | + "url": "https://www.hfmarketdata.io/v1/options/history/AAPL?strike=200&expiry=2024-12-20&call_put=c&limit=5", | |
| 13 | + "status": 200, | |
| 14 | + "headers": { | |
| 15 | + "content-type": "application/json" | |
| 16 | + }, | |
| 17 | + "captured_at": "2026-09-04T23:37:55.763Z", | |
| 18 | + "body": { | |
| 19 | + "count": 5, | |
| 20 | + "data": [ | |
| 21 | + { | |
| 22 | + "ticker": "AAPL", | |
| 23 | + "trade_date": "2023-03-08", | |
| 24 | + "strike": 200, | |
| 25 | + "expiry": "2024-12-20", | |
| 26 | + "call_put": "c", | |
| 27 | + "last_price": 10.43, | |
| 28 | + "bid": 8.6, | |
| 29 | + "ask": 11.85, | |
| 30 | + "bid_iv": 0.2279, | |
| 31 | + "ask_iv": 0.2708, | |
| 32 | + "open_interest": 0, | |
| 33 | + "volume": 0, | |
| 34 | + "delta": 0.3525, | |
| 35 | + "gamma": 0.0072, | |
| 36 | + "vega": 0.7844, | |
| 37 | + "theta": -0.0203, | |
| 38 | + "rho": 0.7796 | |
| 39 | + }, | |
| 40 | + { | |
| 41 | + "ticker": "AAPL", | |
| 42 | + "trade_date": "2023-03-09", | |
| 43 | + "strike": 200, | |
| 44 | + "expiry": "2024-12-20", | |
| 45 | + "call_put": "c", | |
| 46 | + "last_price": 10.88, | |
| 47 | + "bid": 8.75, | |
| 48 | + "ask": 13, | |
| 49 | + "bid_iv": 0.2178, | |
| 50 | + "ask_iv": 0.2739, | |
| 51 | + "open_interest": 0, | |
| 52 | + "volume": 0, | |
| 53 | + "delta": 0.3835, | |
| 54 | + "gamma": 0.0074, | |
| 55 | + "vega": 0.7542, | |
| 56 | + "theta": -0.024, | |
| 57 | + "rho": 0.8238 | |
| 58 | + }, | |
| 59 | + { | |
| 60 | + "ticker": "AAPL", | |
| 61 | + "trade_date": "2023-03-21", | |
| 62 | + "strike": 200, | |
| 63 | + "expiry": "2024-12-20", | |
| 64 | + "call_put": "c", | |
| 65 | + "last_price": 12.45, | |
| 66 | + "bid": 11.4, | |
| 67 | + "ask": 14.15, | |
| 68 | + "bid_iv": 0.2452, | |
| 69 | + "ask_iv": 0.2793, | |
| 70 | + "open_interest": 0, | |
| 71 | + "volume": 2, | |
| 72 | + "delta": 0.3927, | |
| 73 | + "gamma": 0.0069, | |
| 74 | + "vega": 0.8087, | |
| 75 | + "theta": -0.0224, | |
| 76 | + "rho": 0.8699 | |
| 77 | + }, | |
| 78 | + { | |
| 79 | + "ticker": "AAPL", | |
| 80 | + "trade_date": "2023-03-22", | |
| 81 | + "strike": 200, | |
| 82 | + "expiry": "2024-12-20", | |
| 83 | + "call_put": "c", | |
| 84 | + "last_price": 12.01, | |
| 85 | + "bid": 10.8, | |
| 86 | + "ask": 13.4, | |
| 87 | + "bid_iv": 0.2369, | |
| 88 | + "ask_iv": 0.2693, | |
| 89 | + "open_interest": 1, | |
| 90 | + "volume": 1, | |
| 91 | + "delta": 0.3836, | |
| 92 | + "gamma": 0.0072, | |
| 93 | + "vega": 0.8187, | |
| 94 | + "theta": -0.0216, | |
| 95 | + "rho": 0.8586 | |
| 96 | + }, | |
| 97 | + { | |
| 98 | + "ticker": "AAPL", | |
| 99 | + "trade_date": "2023-03-23", | |
| 100 | + "strike": 200, | |
| 101 | + "expiry": "2024-12-20", | |
| 102 | + "call_put": "c", | |
| 103 | + "last_price": 11.78, | |
| 104 | + "bid": 10.8, | |
| 105 | + "ask": 12.75, | |
| 106 | + "bid_iv": 0.2463, | |
| 107 | + "ask_iv": 0.2708, | |
| 108 | + "open_interest": 2, | |
| 109 | + "volume": 0, | |
| 110 | + "delta": 0.3692, | |
| 111 | + "gamma": 0.007, | |
| 112 | + "vega": 0.8063, | |
| 113 | + "theta": -0.0204, | |
| 114 | + "rho": 0.8254 | |
| 115 | + } | |
| 116 | + ] | |
| 117 | + } | |
| 118 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/opt_quarters_v1_options_quarters_get.json
+29 −0
@@ -0,0 +1,29 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "opt_quarters_v1_options_quarters_get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/options/quarters", | |
| 5 | + "params": {}, | |
| 6 | + "url": "https://www.hfmarketdata.io/v1/options/quarters", | |
| 7 | + "status": 200, | |
| 8 | + "headers": { | |
| 9 | + "content-type": "application/json" | |
| 10 | + }, | |
| 11 | + "captured_at": "2026-09-04T23:37:54.713Z", | |
| 12 | + "body": { | |
| 13 | + "quarters": [ | |
| 14 | + "2010_q1", | |
| 15 | + "2010_q2", | |
| 16 | + "2010_q3", | |
| 17 | + "2010_q4", | |
| 18 | + "2011_q1", | |
| 19 | + "2011_q2", | |
| 20 | + "2011_q3", | |
| 21 | + "2011_q4", | |
| 22 | + "2012_q1", | |
| 23 | + "2012_q2", | |
| 24 | + "2012_q3", | |
| 25 | + "2012_q4", | |
| 26 | + "… 55 more" | |
| 27 | + ] | |
| 28 | + } | |
| 29 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/opt_tickers_v1_options_tickers_get.json
+31 −0
@@ -0,0 +1,31 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "opt_tickers_v1_options_tickers_get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/options/tickers", | |
| 5 | + "params": { | |
| 6 | + "search": "TSL", | |
| 7 | + "limit": "10" | |
| 8 | + }, | |
| 9 | + "url": "https://www.hfmarketdata.io/v1/options/tickers?search=TSL&limit=10", | |
| 10 | + "status": 200, | |
| 11 | + "headers": { | |
| 12 | + "content-type": "application/json" | |
| 13 | + }, | |
| 14 | + "captured_at": "2026-09-04T23:37:54.794Z", | |
| 15 | + "body": { | |
| 16 | + "quarter": "2026_q3_partial", | |
| 17 | + "count": 10, | |
| 18 | + "tickers": [ | |
| 19 | + "TSL", | |
| 20 | + "TSLA", | |
| 21 | + "TSLG", | |
| 22 | + "TSLL", | |
| 23 | + "TSLP", | |
| 24 | + "TSLQ", | |
| 25 | + "TSLR", | |
| 26 | + "TSLS", | |
| 27 | + "TSLT", | |
| 28 | + "TSLW" | |
| 29 | + ] | |
| 30 | + } | |
| 31 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/snapshot_v1_snapshot__asset__get.json
+49 −0
@@ -0,0 +1,49 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "snapshot_v1_snapshot__asset__get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/snapshot/{asset}", | |
| 5 | + "params": { | |
| 6 | + "asset": "stock", | |
| 7 | + "tickers": "AAPL,MSFT,NVDA", | |
| 8 | + "at": "2024-06-03 10:35:00", | |
| 9 | + "timeframe": "1min" | |
| 10 | + }, | |
| 11 | + "url": "https://www.hfmarketdata.io/v1/snapshot/stock?tickers=AAPL,MSFT,NVDA&at=2024-06-03%2010:35:00&timeframe=1min", | |
| 12 | + "status": 200, | |
| 13 | + "headers": { | |
| 14 | + "content-type": "application/json" | |
| 15 | + }, | |
| 16 | + "captured_at": "2026-09-04T23:37:54.632Z", | |
| 17 | + "body": { | |
| 18 | + "count": 3, | |
| 19 | + "data": [ | |
| 20 | + { | |
| 21 | + "ticker": "AAPL", | |
| 22 | + "datetime": "2024-06-03 10:35:00", | |
| 23 | + "open": 193.1275, | |
| 24 | + "high": 193.1363, | |
| 25 | + "low": 192.8796, | |
| 26 | + "close": 192.8796, | |
| 27 | + "volume": 93654 | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + "ticker": "MSFT", | |
| 31 | + "datetime": "2024-06-03 10:35:00", | |
| 32 | + "open": 408.1722, | |
| 33 | + "high": 408.3888, | |
| 34 | + "low": 408.0294, | |
| 35 | + "close": 408.1229, | |
| 36 | + "volume": 21898 | |
| 37 | + }, | |
| 38 | + { | |
| 39 | + "ticker": "NVDA", | |
| 40 | + "datetime": "2024-06-03 10:35:00", | |
| 41 | + "open": 113.4484, | |
| 42 | + "high": 113.4823, | |
| 43 | + "low": 113.2238, | |
| 44 | + "close": 113.2557, | |
| 45 | + "volume": 870250 | |
| 46 | + } | |
| 47 | + ] | |
| 48 | + } | |
| 49 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/status_v1_status_get.json
+236 −0
@@ -0,0 +1,236 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "status_v1_status_get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/status", | |
| 5 | + "params": {}, | |
| 6 | + "url": "https://www.hfmarketdata.io/v1/status", | |
| 7 | + "status": 200, | |
| 8 | + "headers": { | |
| 9 | + "content-type": "application/json" | |
| 10 | + }, | |
| 11 | + "captured_at": "2026-09-04T23:37:54.183Z", | |
| 12 | + "body": { | |
| 13 | + "datasets": { | |
| 14 | + "stock": { | |
| 15 | + "1day": { | |
| 16 | + "UNADJUSTED": 7665, | |
| 17 | + "adj_split": 7664, | |
| 18 | + "adj_splitdiv": 7664 | |
| 19 | + }, | |
| 20 | + "1hour": { | |
| 21 | + "adj_split": 7669, | |
| 22 | + "adj_splitdiv": 7669 | |
| 23 | + }, | |
| 24 | + "1min": { | |
| 25 | + "UNADJUSTED": 7670, | |
| 26 | + "adj_split": 7670, | |
| 27 | + "adj_splitdiv": 7670 | |
| 28 | + }, | |
| 29 | + "30min": { | |
| 30 | + "adj_split": 7669, | |
| 31 | + "adj_splitdiv": 7669 | |
| 32 | + }, | |
| 33 | + "5min": { | |
| 34 | + "adj_split": 7669, | |
| 35 | + "adj_splitdiv": 7669 | |
| 36 | + } | |
| 37 | + }, | |
| 38 | + "etf": { | |
| 39 | + "1day": { | |
| 40 | + "UNADJUSTED": 5161, | |
| 41 | + "adj_split": 5161, | |
| 42 | + "adj_splitdiv": 5161 | |
| 43 | + }, | |
| 44 | + "1hour": { | |
| 45 | + "adj_split": 5161, | |
| 46 | + "adj_splitdiv": 5161 | |
| 47 | + }, | |
| 48 | + "1min": { | |
| 49 | + "UNADJUSTED": 5161, | |
| 50 | + "adj_split": 5161, | |
| 51 | + "adj_splitdiv": 5161 | |
| 52 | + }, | |
| 53 | + "30min": { | |
| 54 | + "adj_split": 5161, | |
| 55 | + "adj_splitdiv": 5161 | |
| 56 | + }, | |
| 57 | + "5min": { | |
| 58 | + "adj_split": 5161, | |
| 59 | + "adj_splitdiv": 5161 | |
| 60 | + } | |
| 61 | + }, | |
| 62 | + "futures": { | |
| 63 | + "1day": { | |
| 64 | + "contin_UNadj": 132, | |
| 65 | + "contin_adj_absolute": 132, | |
| 66 | + "contin_adj_ratio": 132 | |
| 67 | + }, | |
| 68 | + "1hour": { | |
| 69 | + "contin_UNadj": 131, | |
| 70 | + "contin_adj_absolute": 131, | |
| 71 | + "contin_adj_ratio": 131 | |
| 72 | + }, | |
| 73 | + "1min": { | |
| 74 | + "contin_UNadj": 131, | |
| 75 | + "contin_adj_absolute": 131, | |
| 76 | + "contin_adj_ratio": 131 | |
| 77 | + }, | |
| 78 | + "30min": { | |
| 79 | + "contin_UNadj": 131, | |
| 80 | + "contin_adj_absolute": 131, | |
| 81 | + "contin_adj_ratio": 131 | |
| 82 | + }, | |
| 83 | + "5min": { | |
| 84 | + "contin_UNadj": 131, | |
| 85 | + "contin_adj_absolute": 131, | |
| 86 | + "contin_adj_ratio": 131 | |
| 87 | + } | |
| 88 | + }, | |
| 89 | + "futures_contracts": { | |
| 90 | + "1day": { | |
| 91 | + "archive": 15279, | |
| 92 | + "update": 3288 | |
| 93 | + }, | |
| 94 | + "1hour": { | |
| 95 | + "archive": 14497, | |
| 96 | + "update": 2581 | |
| 97 | + }, | |
| 98 | + "1min": { | |
| 99 | + "archive": 14577, | |
| 100 | + "update": 2581 | |
| 101 | + }, | |
| 102 | + "30min": { | |
| 103 | + "archive": 14497, | |
| 104 | + "update": 2581 | |
| 105 | + }, | |
| 106 | + "5min": { | |
| 107 | + "archive": 14497, | |
| 108 | + "update": 2581 | |
| 109 | + } | |
| 110 | + }, | |
| 111 | + "crypto": { | |
| 112 | + "1day": { | |
| 113 | + "none": 74 | |
| 114 | + }, | |
| 115 | + "1hour": { | |
| 116 | + "none": 74 | |
| 117 | + }, | |
| 118 | + "1min": { | |
| 119 | + "none": 74 | |
| 120 | + }, | |
| 121 | + "30min": { | |
| 122 | + "none": 74 | |
| 123 | + }, | |
| 124 | + "5min": { | |
| 125 | + "none": 74 | |
| 126 | + } | |
| 127 | + }, | |
| 128 | + "index": { | |
| 129 | + "1day": { | |
| 130 | + "none": 125 | |
| 131 | + }, | |
| 132 | + "1hour": { | |
| 133 | + "none": 122 | |
| 134 | + }, | |
| 135 | + "1min": { | |
| 136 | + "none": 123 | |
| 137 | + }, | |
| 138 | + "30min": { | |
| 139 | + "none": 122 | |
| 140 | + }, | |
| 141 | + "5min": { | |
| 142 | + "none": 122 | |
| 143 | + } | |
| 144 | + }, | |
| 145 | + "fx": { | |
| 146 | + "1day": { | |
| 147 | + "none": 79 | |
| 148 | + }, | |
| 149 | + "1hour": { | |
| 150 | + "none": 79 | |
| 151 | + }, | |
| 152 | + "1min": { | |
| 153 | + "none": 79 | |
| 154 | + }, | |
| 155 | + "30min": { | |
| 156 | + "none": 79 | |
| 157 | + }, | |
| 158 | + "5min": { | |
| 159 | + "none": 79 | |
| 160 | + } | |
| 161 | + }, | |
| 162 | + "options": { | |
| 163 | + "quarters": [ | |
| 164 | + "2010_q1", | |
| 165 | + "2010_q2", | |
| 166 | + "2010_q3", | |
| 167 | + "2010_q4", | |
| 168 | + "2011_q1", | |
| 169 | + "2011_q2", | |
| 170 | + "2011_q3", | |
| 171 | + "2011_q4", | |
| 172 | + "2012_q1", | |
| 173 | + "2012_q2", | |
| 174 | + "2012_q3", | |
| 175 | + "2012_q4", | |
| 176 | + "2013_q1", | |
| 177 | + "2013_q2", | |
| 178 | + "2013_q3", | |
| 179 | + "2013_q4", | |
| 180 | + "2014_q1", | |
| 181 | + "2014_q2", | |
| 182 | + "2014_q3", | |
| 183 | + "2014_q4", | |
| 184 | + "2015_q1", | |
| 185 | + "2015_q2", | |
| 186 | + "2015_q3", | |
| 187 | + "2015_q4", | |
| 188 | + "2016_q1", | |
| 189 | + "2016_q2", | |
| 190 | + "2016_q3", | |
| 191 | + "2016_q4", | |
| 192 | + "2017_q1", | |
| 193 | + "2017_q2", | |
| 194 | + "2017_q3", | |
| 195 | + "2017_q4", | |
| 196 | + "2018_q1", | |
| 197 | + "2018_q2", | |
| 198 | + "2018_q3", | |
| 199 | + "2018_q4", | |
| 200 | + "2019_q1", | |
| 201 | + "2019_q2", | |
| 202 | + "2019_q3", | |
| 203 | + "2019_q4", | |
| 204 | + "2020_q1", | |
| 205 | + "2020_q2", | |
| 206 | + "2020_q3", | |
| 207 | + "2020_q4", | |
| 208 | + "2021_q1", | |
| 209 | + "2021_q2", | |
| 210 | + "2021_q3", | |
| 211 | + "2021_q4", | |
| 212 | + "2022_q1", | |
| 213 | + "2022_q2", | |
| 214 | + "2022_q3", | |
| 215 | + "2022_q4", | |
| 216 | + "2023_q1", | |
| 217 | + "2023_q2", | |
| 218 | + "2023_q3", | |
| 219 | + "2023_q4", | |
| 220 | + "2024_q1", | |
| 221 | + "2024_q2", | |
| 222 | + "2024_q3", | |
| 223 | + "2024_q4", | |
| 224 | + "2025_q1", | |
| 225 | + "2025_q2", | |
| 226 | + "2025_q3", | |
| 227 | + "2025_q4", | |
| 228 | + "2026_q1", | |
| 229 | + "2026_q2", | |
| 230 | + "2026_q3_partial" | |
| 231 | + ], | |
| 232 | + "files_latest_quarter": 6056 | |
| 233 | + } | |
| 234 | + } | |
| 235 | + } | |
| 236 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/examples/tickers_v1__asset__tickers_get.json
+29 −0
@@ -0,0 +1,29 @@ | ||
| 1 | +{ | |
| 2 | + "operationId": "tickers_v1__asset__tickers_get", | |
| 3 | + "method": "GET", | |
| 4 | + "path": "/v1/{asset}/tickers", | |
| 5 | + "params": { | |
| 6 | + "asset": "stock", | |
| 7 | + "timeframe": "1day", | |
| 8 | + "search": "AAP", | |
| 9 | + "limit": "10" | |
| 10 | + }, | |
| 11 | + "url": "https://www.hfmarketdata.io/v1/stock/tickers?timeframe=1day&search=AAP&limit=10", | |
| 12 | + "status": 200, | |
| 13 | + "headers": { | |
| 14 | + "content-type": "application/json" | |
| 15 | + }, | |
| 16 | + "captured_at": "2026-09-04T23:37:54.270Z", | |
| 17 | + "body": { | |
| 18 | + "asset": "stock", | |
| 19 | + "timeframe": "1day", | |
| 20 | + "adjustment": "adj_splitdiv", | |
| 21 | + "count": 4, | |
| 22 | + "tickers": [ | |
| 23 | + "AAP", | |
| 24 | + "AAPG", | |
| 25 | + "AAPL", | |
| 26 | + "CAAP" | |
| 27 | + ] | |
| 28 | + } | |
| 29 | +} | |
| \ No newline at end of file | ||
added
hfmarketdata/web/content/guides/authentication.mdx
+119 −0
@@ -0,0 +1,119 @@ | ||
| 1 | +export const meta = { title: 'Authentication & API keys', description: 'Keyless access, free accounts, high-usage tier, and how to send your API key safely.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Send the key (header, preferred)', | |
| 6 | + curl: `export HFMD_API_KEY="hfmd_live_…" # from the dashboard, shown once | |
| 7 | + | |
| 8 | +curl "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min&limit=5" \\ | |
| 9 | + -H "Authorization: Bearer $HFMD_API_KEY"`, | |
| 10 | + python: `import os, requests | |
| 11 | + | |
| 12 | +session = requests.Session() | |
| 13 | +session.headers["Authorization"] = f"Bearer {os.environ['HFMD_API_KEY']}" | |
| 14 | + | |
| 15 | +r = session.get("https://www.hfmarketdata.io/v1/bars/stock/AAPL", params={"timeframe": "1min", "limit": 5}) | |
| 16 | +r.raise_for_status() | |
| 17 | +print(r.headers["X-RateLimit-Limit-Requests"]) # 120 with a free key`, | |
| 18 | + javascript: `const headers = { Authorization: \`Bearer \${process.env.HFMD_API_KEY}\` }; | |
| 19 | +const res = await fetch("https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min&limit=5", { headers }); | |
| 20 | +console.log(res.headers.get("X-RateLimit-Limit-Requests")); // 120 with a free key`, | |
| 21 | + r: `library(httr2) | |
| 22 | + | |
| 23 | +resp <- request("https://www.hfmarketdata.io/v1/bars/stock/AAPL") |> | |
| 24 | + req_url_query(timeframe = "1min", limit = 5) |> | |
| 25 | + req_auth_bearer_token(Sys.getenv("HFMD_API_KEY")) |> | |
| 26 | + req_perform() | |
| 27 | +resp_header(resp, "X-RateLimit-Limit-Requests")`, | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + title: 'Check who you are and what you have left', | |
| 31 | + curl: `curl "https://www.hfmarketdata.io/v1/limits" -H "Authorization: Bearer $HFMD_API_KEY"`, | |
| 32 | + python: `r = session.get("https://www.hfmarketdata.io/v1/limits") | |
| 33 | +print(r.json()) # tier, windows, remaining requests / rows`, | |
| 34 | + javascript: `const limits = await (await fetch("https://www.hfmarketdata.io/v1/limits", { headers })).json(); | |
| 35 | +console.log(limits);`, | |
| 36 | + r: `request("https://www.hfmarketdata.io/v1/limits") |> | |
| 37 | + req_auth_bearer_token(Sys.getenv("HFMD_API_KEY")) |> | |
| 38 | + req_perform() |> resp_body_json()`, | |
| 39 | + }, | |
| 40 | +] | |
| 41 | + | |
| 42 | +# Authentication & API keys | |
| 43 | + | |
| 44 | +Authentication on HF Market Data is **optional and progressive**. Every data endpoint works without credentials; a key only changes how much you can pull and how fast. | |
| 45 | + | |
| 46 | +## The three tiers | |
| 47 | + | |
| 48 | +| Tier | How you get it | Requests | Rows | Max rows per request | | |
| 49 | +| --- | --- | --- | --- | --- | | |
| 50 | +| **Keyless** — identified by IP | Nothing to do | 30 / hour | 100 000 / hour | 5 000 | | |
| 51 | +| **Free account** — API key | [Create an account](/signup) (e-mail verification) | 120 / minute | 1 000 000 / minute | 50 000 | | |
| 52 | +| **High usage** — API key | E-mail [contact@spboucher.ai](mailto:contact@spboucher.ai?subject=High%20usage%20tier%20request) with your use case | 600 / minute | 10 000 000 / minute | 200 000 | | |
| 53 | + | |
| 54 | +Two details often missed: the keyless window is **one hour**, the keyed windows are **one minute** — so a free key is roughly 240× more requests than keyless, not 4×. And the *rows* budget is the one that bites for bulk pulls: see [rate limits](/docs/rate-limits) for what counts (Parquet counts half, bulk endpoints and `304 Not Modified` count zero). | |
| 55 | + | |
| 56 | +## Keyless access | |
| 57 | + | |
| 58 | +Requests without a key are metered per client IP. It is deliberately small — enough to evaluate the API, read the docs examples, and run the [playground](/playground). Corporate NATs share one IP, so a team behind the same egress shares the 30 requests: if you hit `RATE_LIMIT_EXCEEDED` sooner than expected, that is usually why. The fix is a free key each. | |
| 59 | + | |
| 60 | +## Free account and your first key | |
| 61 | + | |
| 62 | +1. [Sign up](/signup) with an e-mail address and a password, then click the verification link you receive. Sign-up itself is rate limited per IP. | |
| 63 | +2. In the dashboard, open **API keys → Create key**. Give it a name (`laptop`, `airflow-prod`…). | |
| 64 | +3. The full key — `hfmd_live_` followed by 32 base62 characters — is displayed **once**. Copy it into a secret store or an environment variable (`HFMD_API_KEY`). We only keep a salted SHA-256 hash and the 8-character prefix shown in the key list, so it cannot be shown again. | |
| 65 | + | |
| 66 | +You can create several keys (one per environment is a good habit), **rotate** a key (revokes it and issues a new one) or **revoke** it. Revocation is immediate; requests with a revoked key get `401 INVALID_API_KEY`. `last_used_at` in the key list tells you whether a key is still alive before you revoke it. | |
| 67 | + | |
| 68 | +## Sending the key | |
| 69 | + | |
| 70 | +Two ways, in order of preference: | |
| 71 | + | |
| 72 | +```http | |
| 73 | +GET /v1/bars/stock/AAPL?timeframe=1min HTTP/1.1 | |
| 74 | +Host: www.hfmarketdata.io | |
| 75 | +Authorization: Bearer hfmd_live_ab12cd34… | |
| 76 | +``` | |
| 77 | + | |
| 78 | +- **`Authorization: Bearer <key>`** — the header keeps the key out of URLs, proxy logs, browser history and `Referer` headers. Use this everywhere you can. | |
| 79 | +- **`?api_key=<key>`** — for tools that cannot set headers (a spreadsheet `IMPORTDATA`, a quick `pandas.read_csv(url)`). Treat any URL that contains a key as a secret. | |
| 80 | + | |
| 81 | +The snippets on the right show both the header and a call to `GET /v1/limits`, which answers the two questions everybody asks first: *which tier am I on?* and *how much is left in the current window?* | |
| 82 | + | |
| 83 | +<Callout type="warning" title="Never put a key in client-side code or public examples"> | |
| 84 | +A key in a public repository, a shared notebook or a browser bundle is a key you no longer control. Our own documentation only ever shows `$HFMD_API_KEY`. If a key leaks, rotate it from the dashboard — the old value is dead the moment you click. | |
| 85 | +</Callout> | |
| 86 | + | |
| 87 | +## What a key changes — and what it does not | |
| 88 | + | |
| 89 | +A key **raises limits**. It does not unlock hidden data: the same datasets, endpoints and history are available keyless. The one exception is the account surface itself — `/v1/me/*` (your profile, keys, usage) — which naturally requires a key or a signed-in session, and `/v1/admin/*`, which requires the admin role. | |
| 90 | + | |
| 91 | +Every response, keyed or not, carries the limit headers, so you can see the effect immediately: | |
| 92 | + | |
| 93 | +```http | |
| 94 | +X-RateLimit-Limit-Requests: 120 | |
| 95 | +X-RateLimit-Remaining-Requests: 119 | |
| 96 | +X-RateLimit-Limit-Rows: 1000000 | |
| 97 | +X-RateLimit-Remaining-Rows: 999995 | |
| 98 | +X-RateLimit-Reset: 1757030460 | |
| 99 | +X-Row-Count: 5 | |
| 100 | +``` | |
| 101 | + | |
| 102 | +## High usage | |
| 103 | + | |
| 104 | +If you are backfilling a research database, running many workers, or serving an application, ask for the high-usage tier: **600 requests and 10 000 000 rows per minute, 200 000 rows per request**. Send a short e-mail to [contact@spboucher.ai](mailto:contact@spboucher.ai?subject=High%20usage%20tier%20request) from the address of your account — what you are building, roughly how much you need — and the higher limits are switched on your existing keys; nothing to change on your side. Like everything else here, it is free — you only have to ask. | |
| 105 | + | |
| 106 | +## Errors you may see | |
| 107 | + | |
| 108 | +| Status | `error.code` | Meaning | | |
| 109 | +| --- | --- | --- | | |
| 110 | +| 401 | `INVALID_API_KEY` | Malformed, unknown or revoked key. Keyless requests would still work — check the header value. | | |
| 111 | +| 401 | `AUTH_REQUIRED` | Account endpoint called without a key or session. | | |
| 112 | +| 403 | `FORBIDDEN` | Your role or tier does not allow this action (admin endpoints, some features). | | |
| 113 | +| 429 | `RATE_LIMIT_EXCEEDED` | Quota exhausted — `Retry-After` tells you how long to wait. | | |
| 114 | + | |
| 115 | +The full list with remediation lives on the [Errors](/docs/errors) page. | |
| 116 | + | |
| 117 | +## Sessions in the browser | |
| 118 | + | |
| 119 | +The website (dashboard, authenticated playground) uses a cookie session created by `POST /v1/auth/login`, not the API key. Inside the dashboard, the playground runs with your account's quota so you can test at free-tier speed without pasting a key. You never need this flow for programmatic access — use the key. | |
added
hfmarketdata/web/content/guides/bulk-downloads.mdx
+87 −0
@@ -0,0 +1,87 @@ | ||
| 1 | +export const meta = { title: 'Bulk downloads', description: 'Whole-universe Parquet extracts outside the rows quota, and the right way to pull large histories from the regular endpoints.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'One year of fundamentals for every company', | |
| 6 | + curl: `curl -L "https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet" \\ | |
| 7 | + -H "Authorization: Bearer $HFMD_API_KEY" -o fundamentals_2024.parquet`, | |
| 8 | + python: `import os, requests, pandas as pd, io | |
| 9 | + | |
| 10 | +r = requests.get("https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet", | |
| 11 | + headers={"Authorization": f"Bearer {os.environ['HFMD_API_KEY']}"}, timeout=600) | |
| 12 | +r.raise_for_status() | |
| 13 | +df = pd.read_parquet(io.BytesIO(r.content)) | |
| 14 | +print(df.shape, r.headers.get("X-Row-Count"), "rows — counted as 0 against the rows quota")`, | |
| 15 | + javascript: `import { writeFile } from "node:fs/promises"; | |
| 16 | +const res = await fetch("https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet", { | |
| 17 | + headers: { Authorization: \`Bearer \${process.env.HFMD_API_KEY}\` }, | |
| 18 | +}); | |
| 19 | +await writeFile("fundamentals_2024.parquet", Buffer.from(await res.arrayBuffer()));`, | |
| 20 | + r: `library(httr2); library(arrow) | |
| 21 | +resp <- request("https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet") |> | |
| 22 | + req_auth_bearer_token(Sys.getenv("HFMD_API_KEY")) |> | |
| 23 | + req_perform(path = "fundamentals_2024.parquet") | |
| 24 | +df <- read_parquet("fundamentals_2024.parquet")`, | |
| 25 | + }, | |
| 26 | + { | |
| 27 | + title: 'DuckDB straight from the URL', | |
| 28 | + curl: `duckdb -c "INSTALL httpfs; LOAD httpfs; | |
| 29 | +SELECT ticker, count(*) FROM read_parquet('https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet') | |
| 30 | +GROUP BY 1 ORDER BY 2 DESC LIMIT 10;"`, | |
| 31 | + python: `import duckdb | |
| 32 | +con = duckdb.connect() | |
| 33 | +con.execute("INSTALL httpfs; LOAD httpfs;") | |
| 34 | +con.execute("CREATE SECRET (TYPE HTTP, BEARER_TOKEN getenv('HFMD_API_KEY'));") | |
| 35 | +print(con.sql(""" | |
| 36 | + SELECT ticker, count(*) AS facts | |
| 37 | + FROM read_parquet('https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet') | |
| 38 | + GROUP BY 1 ORDER BY 2 DESC LIMIT 10 | |
| 39 | +""").df())`, | |
| 40 | + javascript: `// duckdb-wasm / @duckdb/node-api | |
| 41 | +const db = await DuckDBInstance.create(); | |
| 42 | +const con = await db.connect(); | |
| 43 | +await con.run("INSTALL httpfs; LOAD httpfs;"); | |
| 44 | +const rows = await con.runAndReadAll("SELECT count(*) FROM read_parquet('https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet')"); | |
| 45 | +console.log(rows.getRows());`, | |
| 46 | + r: `library(duckdb) | |
| 47 | +con <- dbConnect(duckdb()) | |
| 48 | +dbExecute(con, "INSTALL httpfs; LOAD httpfs;") | |
| 49 | +dbGetQuery(con, "SELECT count(*) FROM read_parquet('https://www.hfmarketdata.io/v1/bulk/fundamentals/2024.parquet')")`, | |
| 50 | + }, | |
| 51 | +] | |
| 52 | + | |
| 53 | +# Bulk downloads | |
| 54 | + | |
| 55 | +Some questions are about *everything*: every company's balance sheet in 2024, every contract of a futures root. Answering them one request at a time is slow for you and wasteful for the API. Bulk endpoints hand you the whole thing as a single Parquet file — and **they do not count against your rows quota at all** (one request, zero rows). | |
| 56 | + | |
| 57 | +## What is available | |
| 58 | + | |
| 59 | +| Endpoint | Content | Notes | | |
| 60 | +| --- | --- | --- | | |
| 61 | +| <Endpoint method="GET" path="/v1/bulk/fundamentals/{year}.parquet" /> | Every SEC EDGAR fact filed in `{year}` for every covered company: concept, value, unit, fiscal period, period end, filing date, accession number | Point-in-time friendly: the filing date is in every row. Since 2010. | | |
| 62 | + | |
| 63 | +The bulk family grows with the platform; the [reference](/docs/reference/tag/bulk) is always the authoritative list and the [changelog](/docs/changelog) announces additions. A bulk endpoint requires an API key (a free account is enough) because a single call can move hundreds of megabytes. | |
| 64 | + | |
| 65 | +## Why Parquet | |
| 66 | + | |
| 67 | +Bulk files are Parquet with zstd compression: columnar, typed, splittable. Every serious data tool reads it natively — pandas / pyarrow, Polars, DuckDB, Spark, Arrow in R, duckdb-wasm in the browser. Two habits make it painless: | |
| 68 | + | |
| 69 | +- **Query, don't load.** DuckDB can read a Parquet file *over HTTP* and only fetch the row groups and columns your query touches (second snippet). `SELECT ticker, value FROM read_parquet(url) WHERE concept = 'Assets'` downloads a fraction of the file. | |
| 70 | +- **Cache locally.** A yearly file changes only when restatements arrive. Store it, and revalidate with `If-None-Match` — a `304 Not Modified` is free. | |
| 71 | + | |
| 72 | +## Large histories from the regular endpoints | |
| 73 | + | |
| 74 | +Not everything has a bulk file, and often you want *one* instrument's whole history. The regular endpoints handle that well if you use them right: | |
| 75 | + | |
| 76 | +1. **v1 bars with `format=csv`** accept up to **2 000 000 rows** in one request — a full 1-minute history of a liquid stock (about 200 000 bars per year including extended hours) is roughly 10 years per call. One request, streamed. This is the cheapest path for price history. | |
| 77 | +2. **v2 endpoints with `format=parquet`** count **half** the rows and respect your tier's per-request cap (5 000 keyless, 50 000 free, 200 000 high usage). Page with `from`/`to` windows or with `cursor` from `meta.next_cursor`. | |
| 78 | +3. **Parallelise gently.** The requests window is per minute for keyed access: 120 requests spread over a minute is fine; 120 fired in one second also works but then you wait 59 seconds. Two to four concurrent workers is the sweet spot. | |
| 79 | +4. **Split by natural boundaries** — one call per year, per contract, per quarter — so that partial failures are easy to resume and the files you cache have obvious names. | |
| 80 | + | |
| 81 | +<Callout type="tip" title="Rule of thumb"> | |
| 82 | +Whole universe → bulk. One instrument, long history → v1 CSV (bars) or v2 Parquet windows. Many instruments, short window → the multi-ticker endpoints (`/v1/bars/{asset}?tickers=…`, up to 50 per call). | |
| 83 | +</Callout> | |
| 84 | + | |
| 85 | +## Integrity | |
| 86 | + | |
| 87 | +Every bulk response carries an `ETag` (a hash of the file) and `Content-Length`; verify the length after download, and store the ETag with the file so a later `If-None-Match` can confirm nothing changed. Bulk files are regenerated when the underlying data is updated (new filings, restatements); the ETag changes and the [status page](/status) shows the last update time of each dataset. | |
added
hfmarketdata/web/content/guides/data-formats.mdx
+118 −0
@@ -0,0 +1,118 @@ | ||
| 1 | +export const meta = { title: 'Data formats', description: 'JSON, CSV and Parquet responses compared on real requests — sizes, speed, quota cost, and how to load each in pandas, JavaScript and R.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Same request, three formats', | |
| 6 | + curl: `BASE="https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=volume&adjust=back_adjusted&from=2024-01-01&to=2024-12-31" | |
| 7 | +curl "$BASE&format=json" -o es.json | |
| 8 | +curl "$BASE&format=csv" -o es.csv | |
| 9 | +curl "$BASE&format=parquet" -o es.parquet # rows count half against the quota`, | |
| 10 | + python: `import io, requests, pandas as pd | |
| 11 | + | |
| 12 | +base = "https://www.hfmarketdata.io/v1/futures/ES/continuous" | |
| 13 | +params = {"roll": "volume", "adjust": "back_adjusted", "from": "2024-01-01", "to": "2024-12-31"} | |
| 14 | + | |
| 15 | +js = requests.get(base, params={**params, "format": "json"}).json() | |
| 16 | +df_json = pd.DataFrame(js["data"]) # + js["meta"]["roll_dates"] | |
| 17 | + | |
| 18 | +df_csv = pd.read_csv(requests.get(base, params={**params, "format": "csv"}, stream=True).raw, parse_dates=["datetime"]) | |
| 19 | + | |
| 20 | +raw = requests.get(base, params={**params, "format": "parquet"}).content | |
| 21 | +df_pq = pd.read_parquet(io.BytesIO(raw)) # typed columns, UTC timestamps preserved | |
| 22 | +print(len(df_json), len(df_csv), len(df_pq))`, | |
| 23 | + javascript: `const base = "https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=volume&adjust=back_adjusted&from=2024-01-01&to=2024-12-31"; | |
| 24 | + | |
| 25 | +const { data, meta } = await (await fetch(base + "&format=json")).json(); | |
| 26 | +console.log(data.length, meta.roll_dates); | |
| 27 | + | |
| 28 | +const csv = await (await fetch(base + "&format=csv")).text(); | |
| 29 | +const rows = csv.trim().split("\\n").slice(1).map((l) => l.split(",")); | |
| 30 | + | |
| 31 | +// Parquet in the browser/Node: parquet-wasm or duckdb-wasm | |
| 32 | +const bytes = new Uint8Array(await (await fetch(base + "&format=parquet")).arrayBuffer());`, | |
| 33 | + r: `library(httr2); library(arrow) | |
| 34 | +base <- "https://www.hfmarketdata.io/v1/futures/ES/continuous" | |
| 35 | +q <- list(roll = "volume", adjust = "back_adjusted", from = "2024-01-01", to = "2024-12-31") | |
| 36 | + | |
| 37 | +js <- request(base) |> req_url_query(!!!q, format = "json") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 38 | +csv <- request(base) |> req_url_query(!!!q, format = "csv") |> req_perform() |> resp_body_string() |> (\\(s) read.csv(text = s))() | |
| 39 | +pq <- request(base) |> req_url_query(!!!q, format = "parquet") |> req_perform() |> resp_body_raw() |> read_parquet() | |
| 40 | +nrow(js$data); nrow(csv); nrow(pq)`, | |
| 41 | + }, | |
| 42 | +] | |
| 43 | + | |
| 44 | +# Data formats | |
| 45 | + | |
| 46 | +Tabular endpoints return **JSON** by default and accept `format=csv`; v2 endpoints (futures contracts, fundamentals, bulk) also accept `format=parquet`. Same rows, same column names — different bytes, different quota cost, different ergonomics. | |
| 47 | + | |
| 48 | +## Benchmark on real requests | |
| 49 | + | |
| 50 | +Measured on 2026-09-04 against production, from a laptop (single request each, sizes are exact bytes of the response body; gzip is the size the body would have over a compressed connection). Parquet was produced from the identical CSV rows with pandas/pyarrow, zstd codec. | |
| 51 | + | |
| 52 | +| Request | Rows | JSON | JSON gz | CSV | CSV gz | Parquet (zstd) | | |
| 53 | +| --- | --- | --- | --- | --- | --- | --- | | |
| 54 | +| AAPL 1-minute, one week (`start=2024-06-03&end=2024-06-08`) | 3 621 | 474 142 B | 61 169 B | 246 039 B | 50 428 B | 69 370 B | | |
| 55 | +| AAPL daily, full history (`timeframe=1day`) | 6 688 | 812 954 B | 155 894 B | 391 630 B | 132 535 B | 233 456 B | | |
| 56 | +| AAPL option chain, one day (`trade_date=2024-06-21`) | 2 126 | 580 480 B | 74 404 B | 227 355 B | 60 618 B | 89 162 B | | |
| 57 | + | |
| 58 | +Reading the table: | |
| 59 | + | |
| 60 | +- **CSV is about half the size of JSON** before compression (no repeated keys per row) and slightly smaller after — and it was the faster of the two to serve in every run (roughly 30–45 % lower latency on these calls, dominated by serialisation). | |
| 61 | +- **Parquet is competitive with gzipped CSV in bytes** and wins on everything else: column types are preserved (timestamps are real timestamps, not strings), it loads 5–10× faster than parsing text, and **it counts half against your rows quota**. When the client can read it, it is the best choice for anything beyond a few thousand rows. | |
| 62 | +- Compressed sizes converge because price series compress well whatever the container. The gap that matters is the *uncompressed* one when your HTTP client does not negotiate compression. | |
| 63 | + | |
| 64 | +## JSON | |
| 65 | + | |
| 66 | +```json | |
| 67 | +{ | |
| 68 | + "count": 2, | |
| 69 | + "data": [ | |
| 70 | + { "ticker": "AAPL", "datetime": "2024-06-03", "open": 191.2434, "high": 193.3154, "low": 190.8666, "close": 192.3637, "volume": 50080500 }, | |
| 71 | + { "ticker": "AAPL", "datetime": "2024-06-04", "open": 192.9684, "high": 193.6426, "low": 191.3722, "close": 192.6809, "volume": 47471400 } | |
| 72 | + ] | |
| 73 | +} | |
| 74 | +``` | |
| 75 | + | |
| 76 | +- **v1 bars & options endpoints**: `{"count", "data"}` — unchanged since 1.0 and guaranteed by the [versioning policy](/docs/versioning). | |
| 77 | +- **v2 endpoints**: `{"data", "meta"}` where `meta` carries what you would otherwise have to infer — `symbol`, `interval`, `count`, `next_cursor` for pagination, `roll_dates` on continuous series, `as_of` on point-in-time queries, `coverage` notes when something is null. | |
| 78 | +- Numbers are JSON numbers (never quoted); missing values are `null`, never `0` or `NaN`. Timestamps are strings — see [time zones](/docs/time-zones) for their meaning per endpoint. | |
| 79 | + | |
| 80 | +Best for: small responses, browsers, anything where you want `meta` without a second call. | |
| 81 | + | |
| 82 | +## CSV | |
| 83 | + | |
| 84 | +```csv | |
| 85 | +ticker,datetime,open,high,low,close,volume | |
| 86 | +AAPL,2024-06-03,191.3528,193.7175,191.0053,193.0455,50080539 | |
| 87 | +AAPL,2024-06-04,193.0157,193.6976,191.4025,193.3638,47471445 | |
| 88 | +``` | |
| 89 | + | |
| 90 | +- Header row, comma separator, `\n` line endings, UTF-8, RFC 4180 quoting where needed. Empty field = null. | |
| 91 | +- On the v1 bars endpoints `format=csv` lifts the per-request ceiling to **2 000 000 rows** — the way to pull a full 1-minute history in one call. | |
| 92 | +- The `meta` information is not in the body; on v2 endpoints it moves to headers (`X-Row-Count`, and `X-Next-Cursor` when paginated). | |
| 93 | + | |
| 94 | +Best for: `pandas.read_csv(url)`, spreadsheets, shell pipelines, any language. | |
| 95 | + | |
| 96 | +## Parquet | |
| 97 | + | |
| 98 | +- Available on v2 tabular endpoints (`/v1/futures/*`, `/v1/fundamentals/*`) and it *is* the format of the [bulk downloads](/docs/bulk-downloads). | |
| 99 | +- Columnar, typed, zstd-compressed. Timestamps are `TIMESTAMP(UTC)`, prices `DOUBLE`, volumes `BIGINT`, so nothing is lost or re-parsed. | |
| 100 | +- **Quota incentive: rows count ½.** A 50 000-row Parquet response is counted as 25 000 rows. | |
| 101 | +- Readers: pandas / pyarrow, Polars, DuckDB (`SELECT * FROM read_parquet('es.parquet')` — or straight from the URL with the `httpfs` extension), Arrow in R, Spark, parquet-wasm and duckdb-wasm in JavaScript. | |
| 102 | + | |
| 103 | +Best for: research pipelines, anything you will store, anything above a few thousand rows. | |
| 104 | + | |
| 105 | +## Choosing | |
| 106 | + | |
| 107 | +| Situation | Use | | |
| 108 | +| --- | --- | | |
| 109 | +| Exploring in a browser, playground, quick check | JSON | | |
| 110 | +| A DataFrame from a URL in one line | CSV | | |
| 111 | +| More than 50 000 rows from a v1 bars endpoint | CSV (up to 2 M rows) | | |
| 112 | +| Any v2 endpoint you will store or reload | Parquet | | |
| 113 | +| The whole universe for a year | [Bulk](/docs/bulk-downloads) (Parquet, outside the rows quota) | | |
| 114 | +| Re-reading something you already fetched | Anything + `If-None-Match` → 304 is free | | |
| 115 | + | |
| 116 | +## Content negotiation | |
| 117 | + | |
| 118 | +`format=` in the query string is the only switch; the `Accept` header is not used for selection (so links stay shareable). The response `Content-Type` is `application/json`, `text/csv; charset=utf-8` or `application/vnd.apache.parquet`, and CSV/Parquet responses carry a `Content-Disposition: attachment; filename="…"` so a browser download gets a sensible name. | |
added
hfmarketdata/web/content/guides/fundamentals-balance-sheet.mdx
+100 −0
@@ -0,0 +1,100 @@ | ||
| 1 | +export const meta = { title: 'Reading a balance sheet through the API', description: 'Apple\'s Q2 fiscal 2024 balance sheet (period ending 2024-03-30), line by line, as XBRL concepts you can pull from /v1/fundamentals.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'The whole statement in one call', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/AAPL/statements?statement=balance_sheet&period=quarterly&from=2024-01-01&to=2024-06-30"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +BASE = "https://www.hfmarketdata.io" | |
| 10 | +r = requests.get(f"{BASE}/v1/fundamentals/AAPL/statements", | |
| 11 | + params={"statement": "balance_sheet", "period": "quarterly", "from": "2024-01-01", "to": "2024-06-30"}) | |
| 12 | +r.raise_for_status() | |
| 13 | +bs = pd.DataFrame(r.json()["data"]) | |
| 14 | +q2 = bs[bs["period_end"] == "2024-03-30"] | |
| 15 | +print(q2[["concept", "label", "value"]].to_string(index=False))`, | |
| 16 | + javascript: `const url = "https://www.hfmarketdata.io/v1/fundamentals/AAPL/statements?statement=balance_sheet&period=quarterly&from=2024-01-01&to=2024-06-30"; | |
| 17 | +const { data } = await (await fetch(url)).json(); | |
| 18 | +const q2 = data.filter((f) => f.period_end === "2024-03-30"); | |
| 19 | +console.table(q2.map(({ concept, value }) => ({ concept, usd_millions: value / 1e6 })));`, | |
| 20 | + r: `library(httr2) | |
| 21 | +bs <- request("https://www.hfmarketdata.io/v1/fundamentals/AAPL/statements") |> | |
| 22 | + req_url_query(statement = "balance_sheet", period = "quarterly", from = "2024-01-01", to = "2024-06-30") |> | |
| 23 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 24 | +subset(bs$data, period_end == "2024-03-30", c(concept, value))`, | |
| 25 | + }, | |
| 26 | + { | |
| 27 | + title: 'One concept through time', | |
| 28 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/Assets?from=2023-01-01"`, | |
| 29 | + python: `assets = pd.DataFrame(requests.get(f"{BASE}/v1/fundamentals/AAPL/facts/Assets", params={"from": "2023-01-01"}).json()["data"]) | |
| 30 | +print(assets[["period_end", "filed", "value"]])`, | |
| 31 | + javascript: `const { data } = await (await fetch("https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/Assets?from=2023-01-01")).json(); | |
| 32 | +console.table(data.map(({ period_end, filed, value }) => ({ period_end, filed, value })));`, | |
| 33 | + r: `assets <- request("https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/Assets") |> | |
| 34 | + req_url_query(from = "2023-01-01") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 35 | +assets$data[, c("period_end", "filed", "value")]`, | |
| 36 | + }, | |
| 37 | +] | |
| 38 | + | |
| 39 | +# Reading a balance sheet through the API | |
| 40 | + | |
| 41 | +The fundamentals endpoints serve what companies file with the SEC — their XBRL-tagged financial statements — **as filed, point-in-time, since 2010**. This section of the docs uses one running example throughout: **Apple's balance sheet at the end of its second fiscal quarter of 2024, 30 March 2024**, reported in the Form 10-Q filed on 3 May 2024. If you can read that one statement through the API, you can read any of them. | |
| 42 | + | |
| 43 | +## The statement, as Apple filed it | |
| 44 | + | |
| 45 | +Figures in millions of US dollars, from the 10-Q for the quarter ended March 30, 2024 (accession `0000320193-24-000069`, CIK `320193`). The right-hand column is the XBRL concept (`us-gaap` taxonomy) under which the value is tagged — that is the name you use in `/facts/{concept}`. | |
| 46 | + | |
| 47 | +| Line | USD millions | Concept | | |
| 48 | +| --- | --- | --- | | |
| 49 | +| **Assets** | | | | |
| 50 | +| Cash and cash equivalents | 32,695 | `CashAndCashEquivalentsAtCarryingValue` | | |
| 51 | +| Marketable securities (current) | 34,455 | `MarketableSecuritiesCurrent` | | |
| 52 | +| Accounts receivable, net | 21,837 | `AccountsReceivableNetCurrent` | | |
| 53 | +| Vendor non-trade receivables | 19,313 | `NontradeReceivablesCurrent` | | |
| 54 | +| Inventories | 6,232 | `InventoryNet` | | |
| 55 | +| Other current assets | 13,884 | `OtherAssetsCurrent` | | |
| 56 | +| **Total current assets** | **128,416** | `AssetsCurrent` | | |
| 57 | +| Marketable securities (non-current) | 91,240 | `MarketableSecuritiesNoncurrent` | | |
| 58 | +| Property, plant and equipment, net | 43,546 | `PropertyPlantAndEquipmentNet` | | |
| 59 | +| Other non-current assets | 74,209 | `OtherAssetsNoncurrent` | | |
| 60 | +| **Total non-current assets** | **208,995** | `AssetsNoncurrent` | | |
| 61 | +| **Total assets** | **337,411** | `Assets` | | |
| 62 | +| **Liabilities** | | | | |
| 63 | +| Accounts payable | 45,753 | `AccountsPayableCurrent` | | |
| 64 | +| Other current liabilities | 55,335 | `OtherLiabilitiesCurrent` | | |
| 65 | +| Deferred revenue | 8,053 | `ContractWithCustomerLiabilityCurrent` | | |
| 66 | +| Commercial paper | 1,997 | `CommercialPaper` | | |
| 67 | +| Term debt (current) | 12,684 | `LongTermDebtCurrent` | | |
| 68 | +| **Total current liabilities** | **123,822** | `LiabilitiesCurrent` | | |
| 69 | +| Term debt (non-current) | 91,831 | `LongTermDebtNoncurrent` | | |
| 70 | +| Other non-current liabilities | 47,564 | `OtherLiabilitiesNoncurrent` | | |
| 71 | +| **Total non-current liabilities** | **139,395** | `LiabilitiesNoncurrent` | | |
| 72 | +| **Total liabilities** | **263,217** | `Liabilities` | | |
| 73 | +| **Shareholders' equity** | | | | |
| 74 | +| Common stock and additional paid-in capital | 78,815 | `CommonStocksIncludingAdditionalPaidInCapital` | | |
| 75 | +| Accumulated deficit | (4,726) | `RetainedEarningsAccumulatedDeficit` | | |
| 76 | +| Accumulated other comprehensive income | 105 | `AccumulatedOtherComprehensiveIncomeLossNetOfTax` | | |
| 77 | +| **Total shareholders' equity** | **74,194** | `StockholdersEquity` | | |
| 78 | +| **Total liabilities and equity** | **337,411** | `LiabilitiesAndStockholdersEquity` | | |
| 79 | + | |
| 80 | +The identity holds: 263,217 + 74,194 = 337,411. Note the *accumulated deficit* — Apple has returned more to shareholders through buybacks than it has retained, so retained earnings are negative while the company is enormously profitable. Reading the concept name (`RetainedEarningsAccumulatedDeficit`) rather than the label avoids the surprise. | |
| 81 | + | |
| 82 | +## Pulling it from the API | |
| 83 | + | |
| 84 | +**The whole statement**: <Endpoint method="GET" path="/v1/fundamentals/{ticker}/statements" /> with `statement=balance_sheet&period=quarterly` and a date window returns every tagged line of the balance sheets whose period end falls in the window — one row per concept per period, with the value in the unit reported by the filer (`USD`, so `32695000000`, not millions), the fiscal period, the period end date, the filing date and the accession number. The first snippet filters the 30 March 2024 column. | |
| 85 | + | |
| 86 | +**One line through time**: <Endpoint method="GET" path="/v1/fundamentals/{ticker}/facts/{concept}" /> — `facts/Assets` returns total assets for every period Apple has reported since 2010, each with its `filed` date. This is the shape you want for charts and models. | |
| 87 | + | |
| 88 | +**What is covered**: <Endpoint method="GET" path="/v1/fundamentals/{ticker}/coverage" /> tells you which statements, periods and concepts exist for a company, and where the API knows something is missing (a concept the filer never tagged, a period filed under a custom extension) — so a `null` is always explainable. | |
| 89 | + | |
| 90 | +## Things that trip people up | |
| 91 | + | |
| 92 | +1. **Units are as filed.** Values are in dollars (`USD`) or shares (`shares`) or pure numbers (`pure` for ratios like EPS in `USD/shares`). Divide by 1e6 yourself for millions. Never assume a scale. | |
| 93 | +2. **Custom extensions.** Apple tags most lines with standard `us-gaap` concepts, but some filers use company-specific extensions (`aapl:…`) for lines that do not fit. `statements` returns them too, marked with their namespace; `facts/{concept}` needs the exact name. | |
| 94 | +3. **The same fact appears in several filings.** The 30 March 2024 balance sheet is reported in the Q2 FY2024 10-Q *and* again as the comparative column of the Q2 FY2025 10-Q a year later. The API keeps both observations with their own `filed` dates — that is what makes [point-in-time](/docs/fundamentals/point-in-time) queries possible. | |
| 95 | +4. **Instant vs duration.** Balance sheet concepts are *instants* (a value at `period_end`); income statement and cash flow concepts are *durations* (`period_start` to `period_end`, and a 10-K's Q4 is often only derivable by subtraction, which the API does for you where the pieces exist and marks as derived). | |
| 96 | +5. **Fiscal calendars.** Apple's Q2 ends in late March, not June. See [fiscal vs calendar](/docs/fundamentals/fiscal-vs-calendar). | |
| 97 | + | |
| 98 | +## From lines to ratios | |
| 99 | + | |
| 100 | +With the lines above you can compute the classic liquidity and leverage ratios by hand — current ratio 128,416 / 123,822 = **1.04**, quick ratio (32,695 + 34,455 + 21,837) / 123,822 = **0.72**, total debt (1,997 + 12,684 + 91,831 = 106,512) over equity = **1.44**, net cash (158,390 of cash and securities minus 106,512 of debt) = **+51,878**. The API computes these and many more for you — <Endpoint method="GET" path="/v1/fundamentals/{ticker}/ratios" /> for each period, `ratios/daily` for market-based ones that move with the price. Every formula is on the [ratios page](/docs/fundamentals/ratios), where the same Apple quarter is worked through. | |
added
hfmarketdata/web/content/guides/fundamentals-fiscal-vs-calendar.mdx
+81 −0
@@ -0,0 +1,81 @@ | ||
| 1 | +export const meta = { title: 'Fiscal vs calendar periods', description: 'Why Apple\'s Q2 ends in March, how fiscal years and quarters are labelled in the API, and how to align companies on a calendar grid without look-ahead.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Fiscal labels and period ends side by side', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/RevenueFromContractWithCustomerExcludingAssessedTax?from=2023-10-01&to=2024-09-30"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +BASE = "https://www.hfmarketdata.io" | |
| 10 | +rev = pd.DataFrame(requests.get( | |
| 11 | + f"{BASE}/v1/fundamentals/AAPL/facts/RevenueFromContractWithCustomerExcludingAssessedTax", | |
| 12 | + params={"from": "2023-10-01", "to": "2024-09-30"}).json()["data"]) | |
| 13 | +print(rev[["fiscal_year", "fiscal_period", "period_start", "period_end", "filed", "value"]]) | |
| 14 | +# FY2024 Q2 → period 2023-12-31 … 2024-03-30, filed 2024-05-03`, | |
| 15 | + javascript: `const url = "https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/RevenueFromContractWithCustomerExcludingAssessedTax?from=2023-10-01&to=2024-09-30"; | |
| 16 | +const { data } = await (await fetch(url)).json(); | |
| 17 | +console.table(data.map(({ fiscal_year, fiscal_period, period_end, filed, value }) => ({ fiscal_year, fiscal_period, period_end, filed, value })));`, | |
| 18 | + r: `library(httr2) | |
| 19 | +rev <- request("https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/RevenueFromContractWithCustomerExcludingAssessedTax") |> | |
| 20 | + req_url_query(from = "2023-10-01", to = "2024-09-30") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 21 | +rev$data[, c("fiscal_year", "fiscal_period", "period_end", "filed", "value")]`, | |
| 22 | + }, | |
| 23 | + { | |
| 24 | + title: 'Align on calendar quarters (by period end)', | |
| 25 | + python: `# Assign each fiscal quarter to the calendar quarter in which its period ENDS. | |
| 26 | +rev["period_end"] = pd.to_datetime(rev["period_end"]) | |
| 27 | +rev["calendar_q"] = rev["period_end"].dt.to_period("Q") # 2024-03-30 → 2024Q1 | |
| 28 | +print(rev.groupby("calendar_q")["value"].sum()) | |
| 29 | +# Compare across companies on calendar_q, but only use rows whose 'filed' date is ≤ your as-of date`, | |
| 30 | + curl: `# Alignment is a client-side step — see the Python tab`, | |
| 31 | + javascript: `const calQ = (d) => \`\${d.slice(0, 4)}Q\${Math.ceil(Number(d.slice(5, 7)) / 3)}\`; | |
| 32 | +console.log(data.map((f) => [f.fiscal_year, f.fiscal_period, calQ(f.period_end)])); // ["2024","Q2","2024Q1"]`, | |
| 33 | + r: `rev$data$calendar_q <- paste0(substr(rev$data$period_end, 1, 4), "Q", ceiling(as.integer(substr(rev$data$period_end, 6, 7)) / 3)) | |
| 34 | +rev$data[, c("fiscal_year", "fiscal_period", "period_end", "calendar_q")]`, | |
| 35 | + }, | |
| 36 | +] | |
| 37 | + | |
| 38 | +# Fiscal vs calendar periods | |
| 39 | + | |
| 40 | +Apple's **second fiscal quarter of 2024 ended on 30 March 2024**. Its fiscal year 2024 ended on 28 September 2024. Neither matches the calendar, and Apple is not unusual: Microsoft's year ends in June, Walmart's in January, Nvidia's in late January, Oracle's in May. Every number in the fundamentals endpoints is labelled the way the *company* labels it, because that is how it was filed and how analysts talk about it. This page explains the labels and how to put companies on a common grid. | |
| 41 | + | |
| 42 | +## How a fiscal calendar works | |
| 43 | + | |
| 44 | +A **fiscal year** (FY) is the twelve-month accounting period a company chooses. It is named after the calendar year in which it *ends*: Apple's FY2024 ran from 1 October 2023 to 28 September 2024. Quarters are numbered inside it, so Apple's Q1 FY2024 is October–December 2023 — the holiday quarter, its largest. | |
| 45 | + | |
| 46 | +Some companies, Apple included, use a **52/53-week** year that ends on a fixed weekday (Apple: the last Saturday of September). That is why the period end is 30 March 2024 and not 31 March, and why every sixth year or so a fiscal year has 53 weeks — a fact that matters when you annualise a quarter. | |
| 47 | + | |
| 48 | +| Apple label | Period | Filed (10-Q / 10-K) | | |
| 49 | +| --- | --- | --- | | |
| 50 | +| Q1 FY2024 | 2023-10-01 → 2023-12-30 | 2024-02-02 | | |
| 51 | +| **Q2 FY2024** | **2023-12-31 → 2024-03-30** | **2024-05-03** | | |
| 52 | +| Q3 FY2024 | 2024-03-31 → 2024-06-29 | 2024-08-02 | | |
| 53 | +| Q4 FY2024 (in the 10-K) | 2024-06-30 → 2024-09-28 | 2024-11-01 | | |
| 54 | + | |
| 55 | +## How the API labels periods | |
| 56 | + | |
| 57 | +Every fact carries four period fields: | |
| 58 | + | |
| 59 | +- **`fiscal_year`** and **`fiscal_period`** — as declared in the filing (`2024`, `Q2`; `FY` for annual figures). These are the labels you see in press releases. | |
| 60 | +- **`period_start`** / **`period_end`** — the actual calendar dates covered (an *instant* concept such as `Assets` only has `period_end`). | |
| 61 | +- **`filed`** — when the SEC accepted the document. This is the date the information became public, and the only date that matters for [point-in-time](/docs/fundamentals/point-in-time) work. | |
| 62 | + | |
| 63 | +Query parameters `from` / `to` on `/facts` and `/statements` filter on **`period_end`**; `as_of` (where available) filters on **`filed`**. Keep the distinction in mind: “Apple's revenue for the quarter ending in March 2024” (`period_end`) versus “what was known about Apple's revenue on 15 April 2024” (`filed`). | |
| 64 | + | |
| 65 | +## Putting companies on one grid | |
| 66 | + | |
| 67 | +There is no perfect way to compare a March-ending quarter with a December-ending one; there are two honest ones. | |
| 68 | + | |
| 69 | +**By period end (most common).** Assign each fiscal quarter to the calendar quarter in which its period *ends*. Apple's Q2 FY2024 (ending 30 March) becomes calendar 2024Q1; Microsoft's Q3 FY2024 (ending 31 March) also 2024Q1. Companies whose quarter ends mid-quarter (Nvidia, late January → 2024Q1 for Q4 FY2024) land where most of their quarter's activity is. The second snippet does this with `to_period("Q")`. | |
| 70 | + | |
| 71 | +**By filing date (for backtests).** If a signal is computed on day *D*, only facts with `filed ≤ D` may be used, whatever their fiscal label. A calendar-2024Q1 cross-section built on 15 April 2024 contains Apple's *Q1* FY2024 (filed 2 February), not its Q2 (filed 3 May). This is not a nuisance to be engineered away — it is the truth of what an investor knew. | |
| 72 | + | |
| 73 | +<Callout type="warning" title="The classic mistake"> | |
| 74 | +Joining fundamentals to prices on `period_end` gives every backtest a five-to-six-week look into the future — a company's quarter ends on 30 March but the numbers are only public on 3 May. Join on `filed` (or use `as_of`), always. | |
| 75 | +</Callout> | |
| 76 | + | |
| 77 | +## Trailing twelve months | |
| 78 | + | |
| 79 | +Because fiscal years are staggered, the cleanest cross-company measure of “the last year” is the **trailing twelve months** (TTM): the sum of the last four reported quarters of a *duration* concept (revenue, net income, cash flow), or the latest value of an *instant* concept (assets, equity). Apple's TTM net income as of the Q2 FY2024 filing is Q3 FY2023 + Q4 FY2023 + Q1 FY2024 + Q2 FY2024. The [ratios](/docs/fundamentals/ratios) endpoint uses TTM for every flow-over-stock ratio (ROE, margins on daily ratios, P/E), and the ratio row tells you the four periods it summed. | |
| 80 | + | |
| 81 | +One wrinkle the API handles for you: annual filers report the **fourth quarter only implicitly** — the 10-K contains the full year, and Q4 = FY − (Q1 + Q2 + Q3). Where the three quarters exist, the API derives Q4 and flags it as derived; where they do not (a company that just went public), the value is `null` with the reason in `coverage`, never a guess. | |
added
hfmarketdata/web/content/guides/fundamentals-point-in-time.mdx
+92 −0
@@ -0,0 +1,92 @@ | ||
| 1 | +export const meta = { title: 'Point-in-time & backtests without look-ahead bias', description: 'How the fundamentals data preserves every observation with its filing date, what as_of does, and how to build a survivorship- and look-ahead-free backtest.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'What was known on 15 April 2024?', | |
| 6 | + curl: `# as_of filters on the FILING date: Q2 FY2024 (filed 2024-05-03) is not visible yet | |
| 7 | +curl "https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/Assets?as_of=2024-04-15&from=2023-01-01"`, | |
| 8 | + python: `import requests, pandas as pd | |
| 9 | + | |
| 10 | +BASE = "https://www.hfmarketdata.io" | |
| 11 | +def facts(concept, as_of): | |
| 12 | + r = requests.get(f"{BASE}/v1/fundamentals/AAPL/facts/{concept}", params={"as_of": as_of, "from": "2023-01-01"}) | |
| 13 | + return pd.DataFrame(r.json()["data"])[["period_end", "filed", "value"]] | |
| 14 | + | |
| 15 | +print(facts("Assets", "2024-04-15").tail(2)) # latest visible: 2023-12-30 (filed 2024-02-02) | |
| 16 | +print(facts("Assets", "2024-05-15").tail(2)) # now 2024-03-30 (filed 2024-05-03) appears`, | |
| 17 | + javascript: `const facts = async (asOf) => (await (await fetch(\`https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/Assets?as_of=\${asOf}&from=2023-01-01\`)).json()).data; | |
| 18 | +console.log((await facts("2024-04-15")).at(-1)); // period_end 2023-12-30 | |
| 19 | +console.log((await facts("2024-05-15")).at(-1)); // period_end 2024-03-30`, | |
| 20 | + r: `library(httr2) | |
| 21 | +facts <- function(as_of) request("https://www.hfmarketdata.io/v1/fundamentals/AAPL/facts/Assets") |> | |
| 22 | + req_url_query(as_of = as_of, from = "2023-01-01") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 23 | +tail(facts("2024-04-15")$data[, c("period_end", "filed", "value")], 1) | |
| 24 | +tail(facts("2024-05-15")$data[, c("period_end", "filed", "value")], 1)`, | |
| 25 | + }, | |
| 26 | + { | |
| 27 | + title: 'A universe cross-section on one date', | |
| 28 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/frames/Assets?fiscal_year=2024&fiscal_period=Q2"`, | |
| 29 | + python: `# Every company's Q2 FY2024 total assets, with each row's own filing date → filter filed <= your date | |
| 30 | +frame = pd.DataFrame(requests.get(f"{BASE}/v1/fundamentals/frames/Assets", | |
| 31 | + params={"fiscal_year": 2024, "fiscal_period": "Q2"}).json()["data"]) | |
| 32 | +known_by_june = frame[frame["filed"] <= "2024-06-30"] | |
| 33 | +print(len(frame), "filers;", len(known_by_june), "had filed by 2024-06-30")`, | |
| 34 | + javascript: `const { data } = await (await fetch("https://www.hfmarketdata.io/v1/fundamentals/frames/Assets?fiscal_year=2024&fiscal_period=Q2")).json(); | |
| 35 | +console.log(data.length, "filers;", data.filter((f) => f.filed <= "2024-06-30").length, "filed by 2024-06-30");`, | |
| 36 | + r: `frame <- request("https://www.hfmarketdata.io/v1/fundamentals/frames/Assets") |> | |
| 37 | + req_url_query(fiscal_year = 2024, fiscal_period = "Q2") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 38 | +sum(frame$data$filed <= "2024-06-30")`, | |
| 39 | + }, | |
| 40 | +] | |
| 41 | + | |
| 42 | +# Point-in-time & backtests without look-ahead bias | |
| 43 | + | |
| 44 | +A fundamentals dataset can be *right* and still ruin a backtest. The trap is not wrong numbers; it is **numbers that were not yet public** on the day your model pretends to use them. This page explains how the API stores filings so that this cannot happen by accident, and the discipline you still need on your side. | |
| 45 | + | |
| 46 | +## Two dates for every number | |
| 47 | + | |
| 48 | +Apple's total assets at 30 March 2024 were $337,411 million. That sentence contains one date, and it is the wrong one for a backtest. The number **became public on 3 May 2024**, when the 10-Q was accepted by the SEC. Between 30 March and 3 May, an investor knew Apple's *December* balance sheet, not March's. | |
| 49 | + | |
| 50 | +Every fact in the API therefore carries both: | |
| 51 | + | |
| 52 | +- `period_end` — what period the number describes (30 March 2024). | |
| 53 | +- `filed` — when it became public (3 May 2024), plus the acceptance timestamp and the accession number of the document. | |
| 54 | + | |
| 55 | +And every fact is stored **once per observation**. When Apple's Q2 FY2025 10-Q (May 2025) repeats the 30 March 2024 balance sheet as a comparative column, that is a second observation with `filed = 2025-05-02`. If the value had been restated, both values exist: the original one (what was known in 2024) and the restated one (what is known now). Nothing is overwritten. | |
| 56 | + | |
| 57 | +## `as_of`: the point-in-time switch | |
| 58 | + | |
| 59 | +`as_of=YYYY-MM-DD` on `/facts`, `/statements` and `/ratios` restricts the result to observations with `filed ≤ as_of`, and — where several observations of the same period qualify — returns the **latest filed before `as_of`**. The first snippet shows the effect: asking for Apple's assets `as_of=2024-04-15` returns the December 2023 balance sheet as the most recent; `as_of=2024-05-15` returns March 2024. | |
| 60 | + | |
| 61 | +Without `as_of`, you get the current best view — latest observation of each period, restatements included. That is what you want for a company profile page; it is exactly what you do *not* want for a historical simulation. | |
| 62 | + | |
| 63 | +<Callout type="danger" title="The rule"> | |
| 64 | +In a backtest, at simulation date D, every fundamentals query must carry `as_of=D` (or you must filter `filed ≤ D` yourself on bulk data). Joining on `period_end` alone gives your strategy about five weeks of clairvoyance every quarter — and more after a restatement. | |
| 65 | +</Callout> | |
| 66 | + | |
| 67 | +## Cross-sections: `frames` | |
| 68 | + | |
| 69 | +For universe-wide work you need the same concept for every company at once. <Endpoint method="GET" path="/v1/fundamentals/frames/{concept}" /> returns one row per filer for a fiscal year and period — `frames/Assets?fiscal_year=2024&fiscal_period=Q2` — each with its own `filed` date, because companies report at different times (a March-ending quarter is filed anywhere between late April and mid-May). Filter on `filed ≤ D` client-side, or pass `as_of` and let the API do it. The second snippet counts how many Q2 FY2024 filers were actually visible by 30 June 2024. | |
| 70 | + | |
| 71 | +The [bulk file](/docs/bulk-downloads) `/v1/bulk/fundamentals/{year}.parquet` is the same idea for a whole year of filings: every observation with its `filed` date, ready for a `WHERE filed <= D` in DuckDB. | |
| 72 | + | |
| 73 | +## Survivorship | |
| 74 | + | |
| 75 | +Point-in-time is half the story; the other half is the universe. A backtest that only considers companies that *exist today* has already excluded every bankruptcy and delisting — an optimistic bias of its own. The fundamentals coverage includes companies that stopped filing (they simply have no facts after their last filing), and the bars datasets keep delisted tickers' history. Build your universe on each date D from what was filing *then* (a `frames` call `as_of=D` is a decent proxy), not from today's index constituents. | |
| 76 | + | |
| 77 | +## A minimal correct loop | |
| 78 | + | |
| 79 | +```python | |
| 80 | +for D in rebalance_dates: # e.g. month ends | |
| 81 | + frame = get(f"/v1/fundamentals/frames/NetIncomeLoss", fiscal_year=..., fiscal_period=..., as_of=D) | |
| 82 | + ratios = get("/v1/fundamentals/screener", as_of=D, ...) # or compute from frames | |
| 83 | + prices = get("/v1/snapshot/stock", tickers=universe(D), at=f"{D} 15:59:00", timeframe="1min") | |
| 84 | + weights = rank_and_weight(ratios, prices) # only uses data filed ≤ D | |
| 85 | + pnl.append(hold(weights, D, next_D)) | |
| 86 | +``` | |
| 87 | + | |
| 88 | +Everything on the right-hand side of `weights` was public at D. The price snapshot uses the v1 endpoint with an explicit time (US/Eastern, see [time zones](/docs/time-zones)), so even intraday you cannot accidentally read the close before it happened. | |
| 89 | + | |
| 90 | +## What the API does not do | |
| 91 | + | |
| 92 | +It does not decide *when* the market “knew” a number beyond the SEC acceptance time. Earnings press releases (Form 8-K) often precede the 10-Q by a day and contain the headline figures; the API's `filed` for a 10-Q is the 10-Q's acceptance, and 8-K facts, where tagged, carry their own. If your strategy trades on the earnings release, use the 8-K observation. The daily ratios (`ratios/daily`) follow the same rule — a P/E for 2 May 2024 uses the Q1 FY2024 EPS, and switches to Q2 only from 3 May onwards. | |
added
hfmarketdata/web/content/guides/fundamentals-ratios.mdx
+145 −0
@@ -0,0 +1,145 @@ | ||
| 1 | +export const meta = { title: 'All ratio formulas', description: 'Every ratio computed by the fundamentals endpoints, with its exact formula and the inputs\' XBRL concepts — worked on Apple\'s Q2 fiscal 2024.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Ratios per fiscal period', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/AAPL/ratios?period=quarterly&from=2024-01-01&to=2024-06-30"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +BASE = "https://www.hfmarketdata.io" | |
| 10 | +r = requests.get(f"{BASE}/v1/fundamentals/AAPL/ratios", params={"period": "quarterly", "from": "2024-01-01", "to": "2024-06-30"}) | |
| 11 | +ratios = pd.DataFrame(r.json()["data"]) | |
| 12 | +print(ratios.set_index("period_end")[["current_ratio", "quick_ratio", "debt_to_equity", "gross_margin", "operating_margin", "net_margin", "roe_ttm"]].T)`, | |
| 13 | + javascript: `const { data } = await (await fetch("https://www.hfmarketdata.io/v1/fundamentals/AAPL/ratios?period=quarterly&from=2024-01-01&to=2024-06-30")).json(); | |
| 14 | +console.table(data.map(({ period_end, current_ratio, net_margin, roe_ttm }) => ({ period_end, current_ratio, net_margin, roe_ttm })));`, | |
| 15 | + r: `library(httr2) | |
| 16 | +ratios <- request("https://www.hfmarketdata.io/v1/fundamentals/AAPL/ratios") |> | |
| 17 | + req_url_query(period = "quarterly", from = "2024-01-01", to = "2024-06-30") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 18 | +ratios$data[, c("period_end", "current_ratio", "quick_ratio", "debt_to_equity", "net_margin")]`, | |
| 19 | + }, | |
| 20 | + { | |
| 21 | + title: 'Market ratios, every trading day', | |
| 22 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/AAPL/ratios/daily?from=2024-04-01&to=2024-05-31"`, | |
| 23 | + python: `daily = pd.DataFrame(requests.get(f"{BASE}/v1/fundamentals/AAPL/ratios/daily", params={"from": "2024-04-01", "to": "2024-05-31"}).json()["data"]) | |
| 24 | +daily["date"] = pd.to_datetime(daily["date"]) | |
| 25 | +print(daily.set_index("date")[["close", "eps_ttm", "pe", "pb", "ps", "ev_ebitda"]].loc["2024-05-01":"2024-05-06"]) | |
| 26 | +# pe switches from the Q1 FY2024 TTM EPS to the Q2 one on 2024-05-03 — the filing date, not the period end`, | |
| 27 | + javascript: `const { data } = await (await fetch("https://www.hfmarketdata.io/v1/fundamentals/AAPL/ratios/daily?from=2024-04-01&to=2024-05-31")).json(); | |
| 28 | +console.table(data.filter((d) => d.date >= "2024-05-01" && d.date <= "2024-05-06").map(({ date, close, pe, pb }) => ({ date, close, pe, pb })));`, | |
| 29 | + r: `daily <- request("https://www.hfmarketdata.io/v1/fundamentals/AAPL/ratios/daily") |> | |
| 30 | + req_url_query(from = "2024-04-01", to = "2024-05-31") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 31 | +subset(daily$data, date >= "2024-05-01" & date <= "2024-05-06", c(date, close, pe, pb))`, | |
| 32 | + }, | |
| 33 | +] | |
| 34 | + | |
| 35 | +# All ratio formulas | |
| 36 | + | |
| 37 | +Two endpoints compute ratios so you do not have to: <Endpoint method="GET" path="/v1/fundamentals/{ticker}/ratios" /> gives **statement ratios per fiscal period** (liquidity, leverage, profitability, efficiency), and <Endpoint method="GET" path="/v1/fundamentals/{ticker}/ratios/daily" /> gives **market ratios per trading day** (P/E, P/B, EV/EBITDA…), which combine the latest *filed* fundamentals with that day's close. This page is the complete list of definitions. Where a company lacks an input (a bank has no inventory, a pre-revenue biotech has no gross margin), the ratio is `null` and `coverage` says which concept was missing — never a substituted value. | |
| 38 | + | |
| 39 | +Conventions used below: **TTM** = sum of the last four reported quarters for flow items (income statement, cash flow); **avg** = average of the current and the year-ago balance; instant items (balance sheet) are taken at `period_end`. Percentages are returned as decimals (`0.466`, not `46.6`). | |
| 40 | + | |
| 41 | +## The worked example | |
| 42 | + | |
| 43 | +Apple, Q2 FY2024 (period ending 2024-03-30, filed 2024-05-03). Balance sheet lines are on the [balance sheet page](/docs/fundamentals/balance-sheet); the quarter's income statement, in USD millions: net sales **90,753** (products 66,886 + services 23,867), cost of sales 48,482, **gross margin 42,271**, R&D 7,903, SG&A 6,468, **operating income 27,900**, other income 158, pre-tax income 28,058, tax 4,422, **net income 23,636**, diluted EPS **1.53** on 15,406 million diluted shares. TTM net income (Q3 FY2023 19,881 + Q4 FY2023 22,956 + Q1 FY2024 33,916 + Q2 FY2024 23,636) = **100,389**; TTM diluted EPS (1.26 + 1.46 + 2.18 + 1.53) = **6.43**. | |
| 44 | + | |
| 45 | +## Liquidity | |
| 46 | + | |
| 47 | +| Ratio | Formula | Concepts | Apple Q2 FY2024 | | |
| 48 | +| --- | --- | --- | --- | | |
| 49 | +| `current_ratio` | current assets ÷ current liabilities | `AssetsCurrent` / `LiabilitiesCurrent` | 128,416 / 123,822 = **1.037** | | |
| 50 | +| `quick_ratio` | (cash + marketable securities + receivables) ÷ current liabilities | (`CashAndCashEquivalentsAtCarryingValue` + `MarketableSecuritiesCurrent` + `AccountsReceivableNetCurrent`) / `LiabilitiesCurrent` | 88,987 / 123,822 = **0.719** | | |
| 51 | +| `cash_ratio` | (cash + marketable securities) ÷ current liabilities | (`Cash…` + `MarketableSecuritiesCurrent`) / `LiabilitiesCurrent` | 67,150 / 123,822 = **0.542** | | |
| 52 | +| `working_capital` | current assets − current liabilities | `AssetsCurrent` − `LiabilitiesCurrent` | **4,594** | | |
| 53 | + | |
| 54 | +## Leverage & solvency | |
| 55 | + | |
| 56 | +| Ratio | Formula | Concepts | Apple Q2 FY2024 | | |
| 57 | +| --- | --- | --- | --- | | |
| 58 | +| `total_debt` | short-term borrowings + current portion of long-term debt + long-term debt | `CommercialPaper` (+ `ShortTermBorrowings`) + `LongTermDebtCurrent` + `LongTermDebtNoncurrent` | 1,997 + 12,684 + 91,831 = **106,512** | | |
| 59 | +| `net_debt` | total debt − cash − marketable securities (current and non-current) | above − (`Cash…` + `MarketableSecuritiesCurrent` + `MarketableSecuritiesNoncurrent`) | 106,512 − 158,390 = **−51,878** (net cash) | | |
| 60 | +| `debt_to_equity` | total debt ÷ shareholders' equity | / `StockholdersEquity` | 106,512 / 74,194 = **1.436** | | |
| 61 | +| `liabilities_to_equity` | total liabilities ÷ equity | `Liabilities` / `StockholdersEquity` | 263,217 / 74,194 = **3.548** | | |
| 62 | +| `debt_to_assets` | total debt ÷ total assets | / `Assets` | 106,512 / 337,411 = **0.316** | | |
| 63 | +| `equity_ratio` | equity ÷ total assets | `StockholdersEquity` / `Assets` | 74,194 / 337,411 = **0.220** | | |
| 64 | +| `interest_coverage` | EBIT (TTM) ÷ interest expense (TTM) | `OperatingIncomeLoss` / `InterestExpense` | null when interest expense is not separately tagged | | |
| 65 | +| `net_debt_to_ebitda` | net debt ÷ EBITDA (TTM) | see EBITDA below | negative for net-cash companies | | |
| 66 | + | |
| 67 | +## Profitability | |
| 68 | + | |
| 69 | +| Ratio | Formula | Concepts | Apple Q2 FY2024 | | |
| 70 | +| --- | --- | --- | --- | | |
| 71 | +| `gross_margin` | gross profit ÷ revenue | `GrossProfit` / `Revenues` (or `RevenueFromContractWithCustomerExcludingAssessedTax`) | 42,271 / 90,753 = **0.466** | | |
| 72 | +| `operating_margin` | operating income ÷ revenue | `OperatingIncomeLoss` / revenue | 27,900 / 90,753 = **0.307** | | |
| 73 | +| `net_margin` | net income ÷ revenue | `NetIncomeLoss` / revenue | 23,636 / 90,753 = **0.260** | | |
| 74 | +| `ebitda` | operating income + depreciation & amortisation | `OperatingIncomeLoss` + `DepreciationDepletionAndAmortization` | quarterly figure; TTM used in multiples | | |
| 75 | +| `ebitda_margin` | EBITDA ÷ revenue | | | | |
| 76 | +| `effective_tax_rate` | income tax ÷ pre-tax income | `IncomeTaxExpenseBenefit` / `IncomeLossFromContinuingOperationsBeforeIncomeTaxes…` | 4,422 / 28,058 = **0.158** | | |
| 77 | +| `roe_ttm` | net income (TTM) ÷ avg equity | `NetIncomeLoss` TTM / avg `StockholdersEquity` | 100,389 / avg(74,194; 62,158) ≈ **1.47** — Apple's buybacks make equity tiny | | |
| 78 | +| `roa_ttm` | net income (TTM) ÷ avg total assets | / avg `Assets` | 100,389 / avg(337,411; 332,160) ≈ **0.30** | | |
| 79 | +| `roic_ttm` | operating income × (1 − tax rate), TTM ÷ (equity + total debt − cash) | | | | |
| 80 | + | |
| 81 | +## Efficiency | |
| 82 | + | |
| 83 | +| Ratio | Formula | Concepts | | |
| 84 | +| --- | --- | --- | | |
| 85 | +| `asset_turnover` | revenue (TTM) ÷ avg total assets | revenue / avg `Assets` | | |
| 86 | +| `inventory_turnover` | cost of revenue (TTM) ÷ avg inventory | `CostOfRevenue` (or `CostOfGoodsAndServicesSold`) / avg `InventoryNet` | | |
| 87 | +| `days_inventory` | 365 ÷ inventory turnover | | | |
| 88 | +| `receivables_turnover` | revenue (TTM) ÷ avg receivables | / avg `AccountsReceivableNetCurrent` | | |
| 89 | +| `days_sales_outstanding` | 365 ÷ receivables turnover | | | |
| 90 | +| `payables_turnover` | cost of revenue (TTM) ÷ avg payables | / avg `AccountsPayableCurrent` | | |
| 91 | +| `days_payables_outstanding` | 365 ÷ payables turnover | | | |
| 92 | +| `cash_conversion_cycle` | DIO + DSO − DPO | | | |
| 93 | + | |
| 94 | +## Cash flow | |
| 95 | + | |
| 96 | +| Ratio | Formula | Concepts | | |
| 97 | +| --- | --- | --- | | |
| 98 | +| `operating_cash_flow` | as reported (TTM) | `NetCashProvidedByUsedInOperatingActivities` | | |
| 99 | +| `capex` | purchases of PP&E (TTM) | `PaymentsToAcquirePropertyPlantAndEquipment` | | |
| 100 | +| `free_cash_flow` | operating cash flow − capex | | | |
| 101 | +| `fcf_margin` | FCF ÷ revenue | | | |
| 102 | +| `cash_flow_to_debt` | operating cash flow ÷ total debt | | | |
| 103 | +| `dividends_paid` | as reported (TTM) | `PaymentsOfDividends` (or `PaymentsOfDividendsCommonStock`) | | |
| 104 | +| `buybacks` | as reported (TTM) | `PaymentsForRepurchaseOfCommonStock` | | |
| 105 | +| `payout_ratio` | dividends ÷ net income (TTM) | | | |
| 106 | +| `shareholder_yield_inputs` | dividends + buybacks (TTM) | used by the daily `shareholder_yield` | | |
| 107 | + | |
| 108 | +## Per share | |
| 109 | + | |
| 110 | +| Ratio | Formula | Concepts | Apple | | |
| 111 | +| --- | --- | --- | --- | | |
| 112 | +| `eps_diluted` | as reported | `EarningsPerShareDiluted` | 1.53 (Q2 FY2024) | | |
| 113 | +| `eps_ttm` | sum of the last four quarters' diluted EPS | | **6.43** | | |
| 114 | +| `book_value_per_share` | equity ÷ diluted shares | `StockholdersEquity` / `WeightedAverageNumberOfDilutedSharesOutstanding` | 74,194 / 15,406 ≈ **4.82** | | |
| 115 | +| `revenue_per_share_ttm` | revenue (TTM) ÷ diluted shares | | | | |
| 116 | +| `fcf_per_share_ttm` | FCF (TTM) ÷ diluted shares | | | | |
| 117 | + | |
| 118 | +## Market ratios (`ratios/daily`) | |
| 119 | + | |
| 120 | +Computed for every trading day from the **split-adjusted close** of that day (`/v1/bars/stock/{ticker}`, `adj_split`) and the fundamentals **filed on or before that day** — the point-in-time rule, so the P/E on 2 May 2024 uses Q1 FY2024's TTM EPS and the one on 3 May uses Q2's. Shares outstanding come from the cover page of the latest filing (`EntityCommonStockSharesOutstanding`) when available, else diluted weighted shares. | |
| 121 | + | |
| 122 | +| Ratio | Formula | Apple, 3 May 2024 (close ≈ 183.4) | | |
| 123 | +| --- | --- | --- | | |
| 124 | +| `market_cap` | close × shares outstanding | ≈ 183.4 × 15,334 M ≈ **2.81 T** | | |
| 125 | +| `pe` | close ÷ `eps_ttm` | 183.4 / 6.43 ≈ **28.5** | | |
| 126 | +| `pb` | market cap ÷ equity | 2.81 T / 74.2 B ≈ **37.9** | | |
| 127 | +| `ps` | market cap ÷ revenue (TTM) | | | |
| 128 | +| `enterprise_value` | market cap + total debt − cash & securities | ≈ 2.81 T − 51.9 B ≈ **2.76 T** | | |
| 129 | +| `ev_ebitda` | EV ÷ EBITDA (TTM) | | | |
| 130 | +| `ev_sales` | EV ÷ revenue (TTM) | | | |
| 131 | +| `earnings_yield` | 1 ÷ P/E | ≈ **3.5 %** | | |
| 132 | +| `fcf_yield` | FCF (TTM) ÷ market cap | | | |
| 133 | +| `dividend_yield` | dividends per share (TTM) ÷ close | | | |
| 134 | +| `shareholder_yield` | (dividends + buybacks, TTM) ÷ market cap | | | |
| 135 | +| `peg` | P/E ÷ EPS growth (TTM vs prior TTM, %) | | | |
| 136 | + | |
| 137 | +Each daily row also repeats its inputs (`close`, `eps_ttm`, `shares_outstanding`, `fundamentals_as_of` = the filing date used), so every multiple is auditable to the dollar. | |
| 138 | + | |
| 139 | +<Callout type="info" title="Why our P/E may differ from your screener's"> | |
| 140 | +Three usual culprits: (1) GAAP diluted EPS here versus “adjusted” EPS elsewhere; (2) the point-in-time rule — many sites apply new EPS on the *earnings release* day (the 8-K, one day earlier) rather than the 10-Q; (3) shares outstanding from the cover page versus the weighted diluted count. All three inputs are in the row, so you can reconcile. | |
| 141 | +</Callout> | |
| 142 | + | |
| 143 | +## Missing values | |
| 144 | + | |
| 145 | +A ratio is `null` when any input is missing or when the denominator is zero or negative where that makes the ratio meaningless (negative equity → `pb` null, negative TTM earnings → `pe` null, with `earnings_yield` still reported). `coverage` on each period lists the concepts that were not found. Banks and insurers lack `InventoryNet`, `CostOfRevenue` and usually `GrossProfit`; their efficiency ratios are null by design rather than computed from proxies. | |
added
hfmarketdata/web/content/guides/fundamentals-screener.mdx
+75 −0
@@ -0,0 +1,75 @@ | ||
| 1 | +export const meta = { title: 'Screener', description: 'Filter the whole covered universe by fundamentals and market ratios in one request — point-in-time aware, paginated, exportable to CSV or Parquet.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Cheap, profitable, low-leverage', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/screener?pe_max=15&roe_ttm_min=0.15&debt_to_equity_max=1&market_cap_min=2000000000&sort=-fcf_yield&limit=25"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +BASE = "https://www.hfmarketdata.io" | |
| 10 | +r = requests.get(f"{BASE}/v1/fundamentals/screener", params={ | |
| 11 | + "pe_max": 15, "roe_ttm_min": 0.15, "debt_to_equity_max": 1, "market_cap_min": 2_000_000_000, | |
| 12 | + "sort": "-fcf_yield", "limit": 25, | |
| 13 | +}) | |
| 14 | +r.raise_for_status() | |
| 15 | +body = r.json() | |
| 16 | +hits = pd.DataFrame(body["data"]) | |
| 17 | +print(body["meta"]["count"], "matches; next_cursor:", body["meta"].get("next_cursor")) | |
| 18 | +print(hits[["ticker", "pe", "roe_ttm", "debt_to_equity", "fcf_yield", "market_cap"]].head(10))`, | |
| 19 | + javascript: `const q = new URLSearchParams({ pe_max: 15, roe_ttm_min: 0.15, debt_to_equity_max: 1, market_cap_min: 2e9, sort: "-fcf_yield", limit: 25 }); | |
| 20 | +const { data, meta } = await (await fetch(\`https://www.hfmarketdata.io/v1/fundamentals/screener?\${q}\`)).json(); | |
| 21 | +console.log(meta.count, "matches"); | |
| 22 | +console.table(data.slice(0, 10).map(({ ticker, pe, roe_ttm, fcf_yield }) => ({ ticker, pe, roe_ttm, fcf_yield })));`, | |
| 23 | + r: `library(httr2) | |
| 24 | +hits <- request("https://www.hfmarketdata.io/v1/fundamentals/screener") |> | |
| 25 | + req_url_query(pe_max = 15, roe_ttm_min = 0.15, debt_to_equity_max = 1, market_cap_min = 2e9, sort = "-fcf_yield", limit = 25) |> | |
| 26 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 27 | +head(hits$data[, c("ticker", "pe", "roe_ttm", "debt_to_equity", "fcf_yield")], 10)`, | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + title: 'The same screen as of a past date (backtest-safe)', | |
| 31 | + curl: `curl "https://www.hfmarketdata.io/v1/fundamentals/screener?pe_max=15&roe_ttm_min=0.15&as_of=2024-06-28&format=csv" -o screen_2024-06-28.csv`, | |
| 32 | + python: `screen = pd.read_csv(f"{BASE}/v1/fundamentals/screener?pe_max=15&roe_ttm_min=0.15&as_of=2024-06-28&format=csv") | |
| 33 | +print(len(screen), "names passed the screen with data public on 2024-06-28")`, | |
| 34 | + javascript: `const csv = await (await fetch("https://www.hfmarketdata.io/v1/fundamentals/screener?pe_max=15&roe_ttm_min=0.15&as_of=2024-06-28&format=csv")).text(); | |
| 35 | +console.log(csv.split("\\n").length - 2, "names");`, | |
| 36 | + r: `screen <- read.csv("https://www.hfmarketdata.io/v1/fundamentals/screener?pe_max=15&roe_ttm_min=0.15&as_of=2024-06-28&format=csv") | |
| 37 | +nrow(screen)`, | |
| 38 | + }, | |
| 39 | +] | |
| 40 | + | |
| 41 | +# Screener | |
| 42 | + | |
| 43 | +<Endpoint method="GET" path="/v1/fundamentals/screener" /> answers “which companies satisfy these conditions?” across the whole covered universe in one request. Every ratio listed on the [formulas page](/docs/fundamentals/ratios) — statement ratios per latest period and market ratios per day — is available as a filter and as a sort key. | |
| 44 | + | |
| 45 | +## Filters | |
| 46 | + | |
| 47 | +Filters are query parameters named `<ratio>_min` and `<ratio>_max`, with the ratio names from the formulas page: `pe_max=15`, `roe_ttm_min=0.15`, `net_debt_to_ebitda_max=2`, `market_cap_min=2000000000`, `gross_margin_min=0.4`… Combine as many as you like; they are ANDed. A few non-ratio filters exist too: `sector`, `exchange`, `filer_status` (currently filing or not), and `ticker` for a comma-separated list when you want the ratios of a fixed basket. | |
| 48 | + | |
| 49 | +Percentages are decimals (`0.15` for 15 %), money is in dollars (`2000000000` for $2 B). Unknown parameters return `400 INVALID_PARAMETER` with the list of valid names, so a typo never silently widens a screen. | |
| 50 | + | |
| 51 | +## Sorting and pagination | |
| 52 | + | |
| 53 | +`sort=-fcf_yield` sorts descending on FCF yield (`sort=pe` ascending). Results are paginated with `limit` (default 50) and `cursor` from `meta.next_cursor`; `meta.count` is the total number of matches. Because the screener scans the whole universe, **it costs 2 requests** against the requests quota instead of 1 — the reference page says so, and the `X-RateLimit-Remaining-Requests` header confirms it. | |
| 54 | + | |
| 55 | +`format=csv` or `format=parquet` returns the full result set for the page in one file (Parquet at half the rows cost), which is how the second snippet exports a screen. | |
| 56 | + | |
| 57 | +## Point-in-time screens | |
| 58 | + | |
| 59 | +`as_of=YYYY-MM-DD` runs the screen **with the data that was public on that date**: statement ratios from filings with `filed ≤ as_of`, market ratios from that day's close. This is the difference between a backtest and a story. Rebalancing a value strategy at the end of each month is a loop of `as_of` calls — 12 requests per year of history (24 with the cost factor), each returning the universe that would actually have passed the screen that day, including companies that later disappeared. | |
| 60 | + | |
| 61 | +<Callout type="tip" title="Reproducible screens"> | |
| 62 | +Save the full request URL with `as_of` alongside your results. Re-running it months later returns the same set — restatements filed after `as_of` are excluded by construction — so a screen is a citable object, not a snapshot that drifts. | |
| 63 | +</Callout> | |
| 64 | + | |
| 65 | +## Result rows | |
| 66 | + | |
| 67 | +Each row carries the ticker, the company name and CIK, the ratios you filtered or sorted on **plus** every other ratio (so one call feeds the whole factor model), the `fundamentals_as_of` filing date each statement ratio comes from, the `price_date` used for market ratios, and `coverage` notes when a ratio is `null`. Nothing is imputed: a company missing a filter's input simply does not pass that filter. | |
| 68 | + | |
| 69 | +## Typical screens | |
| 70 | + | |
| 71 | +- **Quality value**: `pe_max=15&roe_ttm_min=0.15&debt_to_equity_max=1&fcf_yield_min=0.05` | |
| 72 | +- **Net cash**: `net_debt_max=0&market_cap_min=1000000000&sort=-cash_ratio` | |
| 73 | +- **Deleveraging**: `interest_coverage_min=5&net_debt_to_ebitda_max=1.5` | |
| 74 | +- **High shareholder yield**: `shareholder_yield_min=0.06&payout_ratio_max=0.8` | |
| 75 | +- **A basket's ratios today**: `ticker=AAPL,MSFT,NVDA,AMZN,GOOGL` (no other filter) — a cheap way to get every ratio for a watchlist in one call. | |
added
hfmarketdata/web/content/guides/fundamentals-stream.mdx
+107 −0
@@ -0,0 +1,107 @@ | ||
| 1 | +export const meta = { title: 'WebSocket filings stream', description: 'Subscribe to SEC filings as they are accepted — one WebSocket, JSON events, filter by ticker or form type, resume after a disconnect.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Subscribe to Apple and Microsoft 10-Q / 10-K / 8-K', | |
| 6 | + curl: `# websocat: https://github.com/vi/websocat | |
| 7 | +websocat "wss://www.hfmarketdata.io/v1/stream?tickers=AAPL,MSFT&forms=10-Q,10-K,8-K" \\ | |
| 8 | + -H "Authorization: Bearer $HFMD_API_KEY"`, | |
| 9 | + python: `import asyncio, json, os | |
| 10 | +import websockets # pip install websockets | |
| 11 | + | |
| 12 | +URL = "wss://www.hfmarketdata.io/v1/stream?tickers=AAPL,MSFT&forms=10-Q,10-K,8-K" | |
| 13 | + | |
| 14 | +async def main(): | |
| 15 | + headers = {"Authorization": f"Bearer {os.environ['HFMD_API_KEY']}"} | |
| 16 | + last_id = None | |
| 17 | + while True: # reconnect loop with resume | |
| 18 | + url = URL + (f"&since={last_id}" if last_id else "") | |
| 19 | + try: | |
| 20 | + async with websockets.connect(url, additional_headers=headers, ping_interval=20) as ws: | |
| 21 | + async for raw in ws: | |
| 22 | + event = json.loads(raw) | |
| 23 | + if event["type"] == "filing": | |
| 24 | + f = event["data"] | |
| 25 | + print(f["accepted_at"], f["ticker"], f["form"], f["accession"], f["url"]) | |
| 26 | + last_id = event.get("id", last_id) | |
| 27 | + except (websockets.ConnectionClosed, OSError) as e: | |
| 28 | + print("reconnecting after", e); await asyncio.sleep(2) | |
| 29 | + | |
| 30 | +asyncio.run(main())`, | |
| 31 | + javascript: `// Browsers cannot set headers on WebSocket: pass the key as ?api_key= (keep it server-side in production) | |
| 32 | +const ws = new WebSocket(\`wss://www.hfmarketdata.io/v1/stream?tickers=AAPL,MSFT&forms=10-Q,10-K,8-K&api_key=\${process.env.HFMD_API_KEY}\`); | |
| 33 | +ws.onmessage = (ev) => { | |
| 34 | + const event = JSON.parse(ev.data); | |
| 35 | + if (event.type === "filing") console.log(event.data.accepted_at, event.data.ticker, event.data.form, event.data.url); | |
| 36 | +}; | |
| 37 | +ws.onclose = (ev) => console.log("closed", ev.code, ev.reason);`, | |
| 38 | + r: `library(websocket) # install.packages("websocket") | |
| 39 | +ws <- WebSocket$new(paste0("wss://www.hfmarketdata.io/v1/stream?tickers=AAPL,MSFT&forms=10-Q,10-K,8-K&api_key=", Sys.getenv("HFMD_API_KEY"))) | |
| 40 | +ws$onMessage(function(event) { | |
| 41 | + e <- jsonlite::fromJSON(event$data) | |
| 42 | + if (e$type == "filing") cat(e$data$accepted_at, e$data$ticker, e$data$form, e$data$url, "\\n") | |
| 43 | +})`, | |
| 44 | + }, | |
| 45 | +] | |
| 46 | + | |
| 47 | +# WebSocket filings stream | |
| 48 | + | |
| 49 | +Polling `GET /v1/fundamentals/{ticker}/filings` every minute for a thousand tickers is a poor use of everyone's requests. The **filings stream** pushes an event when a filing is accepted by EDGAR and indexed here, over a single WebSocket connection. | |
| 50 | + | |
| 51 | +<Callout type="info" title="Authoritative source"> | |
| 52 | +The stream endpoint is described in the [reference](/docs/reference/tag/stream), generated from the live specification; parameter names, event fields and limits there take precedence over this guide if they differ. | |
| 53 | +</Callout> | |
| 54 | + | |
| 55 | +## Connecting | |
| 56 | + | |
| 57 | +``` | |
| 58 | +wss://www.hfmarketdata.io/v1/stream?tickers=AAPL,MSFT&forms=10-Q,10-K,8-K | |
| 59 | +``` | |
| 60 | + | |
| 61 | +- **Authentication**: an API key is required (a stream is long-lived; opening one is counted as a request). Send it as `Authorization: Bearer …` where your client allows headers; browsers cannot, so `?api_key=` is accepted — prefer a small server-side relay over exposing a key in a page. | |
| 62 | +- **Filters** (optional, comma-separated): `tickers`, `forms` (`10-K`, `10-Q`, `8-K`, …). No filter = every filing indexed. | |
| 63 | +- **Resume**: events carry a monotonically increasing `id`; reconnect with `since=<id>` to receive what you missed while disconnected. The Python snippet shows the standard reconnect-and-resume loop. | |
| 64 | +- **Keep-alive**: the server sends WebSocket pings; answer them (every client library does by default) or the connection is closed. | |
| 65 | + | |
| 66 | +## Events | |
| 67 | + | |
| 68 | +Every message is a JSON object with `type`, `id` and `ts` (UTC, ISO 8601). A filing event looks like this — the Apple Q2 FY2024 10-Q used throughout these guides: | |
| 69 | + | |
| 70 | +```json | |
| 71 | +{ | |
| 72 | + "type": "filing", | |
| 73 | + "id": 184722, | |
| 74 | + "ts": "2024-05-03T10:41:52Z", | |
| 75 | + "data": { | |
| 76 | + "ticker": "AAPL", | |
| 77 | + "cik": "320193", | |
| 78 | + "form": "10-Q", | |
| 79 | + "accession": "0000320193-24-000069", | |
| 80 | + "accepted_at": "2024-05-03T10:41:37Z", | |
| 81 | + "period_end": "2024-03-30", | |
| 82 | + "fiscal_year": 2024, | |
| 83 | + "fiscal_period": "Q2", | |
| 84 | + "url": "https://www.sec.gov/Archives/edgar/data/320193/000032019324000069/" | |
| 85 | + } | |
| 86 | +} | |
| 87 | +``` | |
| 88 | + | |
| 89 | +| `type` | When | Payload | | |
| 90 | +| --- | --- | --- | | |
| 91 | +| `hello` | on connect | your filters as parsed and the current `id` high-water mark | | |
| 92 | +| `filing` | a filing is accepted and indexed | the document: ticker, CIK, form, accession, acceptance time, period, URL | | |
| 93 | +| `facts` | the filing's XBRL facts are parsed and queryable through `/v1/fundamentals` | accession and number of facts | | |
| 94 | +| `heartbeat` | periodically when idle | `id` high-water mark, so you can persist a resume point even without traffic | | |
| 95 | +| `error` | bad filter, revoked key | the standard error envelope (`code`, `message`, `docs`); the socket then closes | | |
| 96 | + | |
| 97 | +The two-step `filing` → `facts` sequence matters for point-in-time work: the `filing` event tells you *something was filed* (useful to trigger a download of the document itself); the `facts` event tells you the structured numbers are now available through the API. | |
| 98 | + | |
| 99 | +## Good citizenship | |
| 100 | + | |
| 101 | +- Filters are evaluated server-side; subscribe to what you need rather than everything-then-filter. | |
| 102 | +- After an `error` close (revoked key, invalid filter) fix the cause before reconnecting; do not reconnect in a tight loop. | |
| 103 | +- This stream reflects EDGAR acceptance; it is not a market data feed, and a filing's *market* impact often precedes it (press releases). See [point-in-time](/docs/fundamentals/point-in-time). | |
| 104 | + | |
| 105 | +## Without WebSockets | |
| 106 | + | |
| 107 | +<Endpoint method="GET" path="/v1/fundamentals/{ticker}/filings" /> lists filings with the same fields; `?from=` on the filing date and a polite polling interval (a few minutes) is a perfectly acceptable fallback for a handful of tickers. | |
added
hfmarketdata/web/content/guides/futures-contracts.mdx
+176 −0
@@ -0,0 +1,176 @@ | ||
| 1 | +export const meta = { title: 'Futures: individual contracts', description: 'Month codes, ESZ25 vs continuous series, roll methods (volume, open interest, calendar, first notice) and adjustment methods (none, back-adjusted, ratio) with a worked example.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'One contract: ESZ25 daily bars', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars?interval=1d&from=2025-06-01&to=2025-06-30"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +r = requests.get("https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars", | |
| 10 | + params={"interval": "1d", "from": "2025-06-01", "to": "2025-06-30"}, timeout=60) | |
| 11 | +r.raise_for_status() | |
| 12 | +body = r.json() | |
| 13 | +df = pd.DataFrame(body["data"]) | |
| 14 | +df["datetime"] = pd.to_datetime(df["datetime"], utc=True) | |
| 15 | +print(body["meta"]["symbol"], len(df), "bars") | |
| 16 | +print(df[["datetime", "close", "volume", "open_interest"]].tail())`, | |
| 17 | + javascript: `const res = await fetch("https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars?interval=1d&from=2025-06-01&to=2025-06-30"); | |
| 18 | +const { data, meta } = await res.json(); | |
| 19 | +console.log(meta.symbol, data.length, "bars", data.at(-1));`, | |
| 20 | + r: `library(httr2) | |
| 21 | +resp <- request("https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars") |> | |
| 22 | + req_url_query(interval = "1d", from = "2025-06-01", to = "2025-06-30") |> | |
| 23 | + req_perform() | |
| 24 | +body <- resp_body_json(resp, simplifyVector = TRUE) | |
| 25 | +tail(body$data)`, | |
| 26 | + }, | |
| 27 | + { | |
| 28 | + title: 'Continuous series, your roll & adjustment', | |
| 29 | + curl: `curl "https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=volume&adjust=back_adjusted&depth=1&interval=1d&from=2024-01-01&to=2024-12-31"`, | |
| 30 | + python: `r = requests.get("https://www.hfmarketdata.io/v1/futures/ES/continuous", | |
| 31 | + params={"roll": "volume", "adjust": "back_adjusted", "depth": 1, | |
| 32 | + "interval": "1d", "from": "2024-01-01", "to": "2024-12-31"}, timeout=60) | |
| 33 | +body = r.json() | |
| 34 | +es = pd.DataFrame(body["data"]) | |
| 35 | +print(body["meta"]["roll_dates"]) # when the series switched contract`, | |
| 36 | + javascript: `const url = "https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=volume&adjust=back_adjusted&depth=1&interval=1d&from=2024-01-01&to=2024-12-31"; | |
| 37 | +const { data, meta } = await (await fetch(url)).json(); | |
| 38 | +console.log(meta.roll_dates, data.length);`, | |
| 39 | + r: `body <- request("https://www.hfmarketdata.io/v1/futures/ES/continuous") |> | |
| 40 | + req_url_query(roll = "volume", adjust = "back_adjusted", depth = 1, | |
| 41 | + interval = "1d", from = "2024-01-01", to = "2024-12-31") |> | |
| 42 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 43 | +body$meta$roll_dates`, | |
| 44 | + }, | |
| 45 | + { | |
| 46 | + title: 'The chain and the curve on a date', | |
| 47 | + curl: `curl "https://www.hfmarketdata.io/v1/futures/CL/chain?as_of=2024-06-03" | |
| 48 | +curl "https://www.hfmarketdata.io/v1/futures/CL/term-structure?as_of=2024-06-03"`, | |
| 49 | + python: `chain = requests.get("https://www.hfmarketdata.io/v1/futures/CL/chain", params={"as_of": "2024-06-03"}).json() | |
| 50 | +curve = requests.get("https://www.hfmarketdata.io/v1/futures/CL/term-structure", params={"as_of": "2024-06-03"}).json() | |
| 51 | +print(pd.DataFrame(curve["data"])[["symbol", "expiration_date", "close", "open_interest"]])`, | |
| 52 | + javascript: `const [chain, curve] = await Promise.all([ | |
| 53 | + fetch("https://www.hfmarketdata.io/v1/futures/CL/chain?as_of=2024-06-03").then((r) => r.json()), | |
| 54 | + fetch("https://www.hfmarketdata.io/v1/futures/CL/term-structure?as_of=2024-06-03").then((r) => r.json()), | |
| 55 | +]); | |
| 56 | +console.table(curve.data.map(({ symbol, expiration_date, close }) => ({ symbol, expiration_date, close })));`, | |
| 57 | + r: `curve <- request("https://www.hfmarketdata.io/v1/futures/CL/term-structure") |> | |
| 58 | + req_url_query(as_of = "2024-06-03") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 59 | +curve$data[, c("symbol", "expiration_date", "close")]`, | |
| 60 | + }, | |
| 61 | +] | |
| 62 | + | |
| 63 | +# Futures: individual contracts | |
| 64 | + | |
| 65 | +A futures *product* — the E-mini S&P 500, `ES` — is not one time series. It is a family of **contracts**, each with its own expiry, its own price and its own life of a few months to a few years. `ESZ25` is one of them: the contract that expires in December 2025. This guide explains how to name them, when to use a single contract versus a stitched *continuous* series, and exactly what the API does when it rolls and adjusts. | |
| 66 | + | |
| 67 | +## Naming a contract | |
| 68 | + | |
| 69 | +A contract symbol is **root + month code + year**: | |
| 70 | + | |
| 71 | +``` | |
| 72 | +ES Z 25 → ESZ25 (also accepted: ESZ2025) | |
| 73 | +│ │ └─ year (2 or 4 digits) | |
| 74 | +│ └───── delivery month code | |
| 75 | +└────────── product root (1–3 characters, as in FirstRate Data) | |
| 76 | +``` | |
| 77 | + | |
| 78 | +| Code | Month | Code | Month | Code | Month | | |
| 79 | +| --- | --- | --- | --- | --- | --- | | |
| 80 | +| **F** | January | **K** | May | **U** | September | | |
| 81 | +| **G** | February | **M** | June | **V** | October | | |
| 82 | +| **H** | March | **N** | July | **X** | November | | |
| 83 | +| **J** | April | **Q** | August | **Z** | December | | |
| 84 | + | |
| 85 | +The codes look arbitrary because they are the letters left over after removing those that could be confused with other pit notations. Most financial products (equity indices, rates, FX) only list the quarterly cycle **H M U Z**; energy and metals list every month (`FGHJKMNQUVXZ`). `GET /v1/futures/roots` returns each root's `month_cycle`, exchange, tick size and contract size; `GET /v1/futures/{root}/contracts` lists every contract with `first_data_date`, `last_data_date`, `expiration_date` and `status`. | |
| 86 | + | |
| 87 | +<Callout type="info" title="Roots and aliases"> | |
| 88 | +Roots are FirstRate Data's: `ES`, `NQ`, `CL`, `GC`, `ZN`… A few differ from the exchange code — the euro FX future is `E6` in the files and the API accepts `6E` as an alias. When a symbol does not parse you get `400 INVALID_CONTRACT_SYMBOL`; when it parses but no such contract exists, `404 CONTRACT_NOT_FOUND` — the message tells you which. | |
| 89 | +</Callout> | |
| 90 | + | |
| 91 | +## Contract vs continuous: which one do you want? | |
| 92 | + | |
| 93 | +<Figure caption="The same product, two views. Each contract lives a few months and is most active for a few weeks before expiry; a continuous series stitches the active one at every date."> | |
| 94 | +<svg viewBox="0 0 680 230" role="img" aria-label="Timeline of overlapping futures contracts ESH25, ESM25, ESU25 and ESZ25 with a continuous series drawn across the most active one"> | |
| 95 | + <defs><marker id="arr" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="var(--fg-2)" /></marker></defs> | |
| 96 | + <line x1="40" y1="200" x2="650" y2="200" stroke="var(--line-3)" strokeWidth="1.5" markerEnd="url(#arr)" /> | |
| 97 | + {[["Jan", 60], ["Mar", 170], ["Jun", 300], ["Sep", 430], ["Dec", 560]].map(([m, x]) => <g key={m}><line x1={x} y1="195" x2={x} y2="205" stroke="var(--fg-3)" /><text x={x} y="222" fontSize="11" textAnchor="middle" fill="var(--fg-2)" fontFamily="var(--mono)">{m}</text></g>)} | |
| 98 | + {[["ESH25", 40, 180, 40], ["ESM25", 150, 310, 80], ["ESU25", 280, 440, 120], ["ESZ25", 410, 570, 160]].map(([s, x1, x2, y]) => ( | |
| 99 | + <g key={s}> | |
| 100 | + <rect x={x1} y={y - 12} width={x2 - x1} height="24" rx="6" fill="var(--bg-2)" stroke="var(--line-2)" /> | |
| 101 | + <rect x={x2 - 70} y={y - 12} width="60" height="24" rx="6" fill="var(--accent-2-soft)" stroke="var(--accent-2)" /> | |
| 102 | + <text x={x1 + 8} y={y + 4} fontSize="11" fill="var(--fg-1)" fontFamily="var(--mono)">{s}</text> | |
| 103 | + <text x={x2 - 40} y={y + 4} fontSize="9.5" fill="var(--accent-2)" textAnchor="middle">active</text> | |
| 104 | + </g> | |
| 105 | + ))} | |
| 106 | + <path d="M110 40 L170 40 L170 80 L300 80 L300 120 L430 120 L430 160 L560 160" fill="none" stroke="var(--accent)" strokeWidth="3" strokeLinejoin="round" /> | |
| 107 | + <text x="580" y="164" fontSize="11" fill="var(--accent)" fontWeight="600">continuous</text> | |
| 108 | + {[170, 300, 430].map(x => <g key={x}><line x1={x} y1="20" x2={x} y2="190" stroke="var(--accent)" strokeDasharray="3 4" strokeWidth="1" /><text x={x} y="14" fontSize="10" textAnchor="middle" fill="var(--accent)">roll</text></g>)} | |
| 109 | +</svg> | |
| 110 | +</Figure> | |
| 111 | + | |
| 112 | +**Use an individual contract when the contract *is* the object of study**: pricing a calendar spread, computing the basis against the index, modelling the roll yield, checking open interest migration, backtesting a strategy that actually holds `ESZ25` and must exit before expiry. The prices are exactly what traded — no synthetic adjustments. | |
| 113 | + | |
| 114 | +**Use a continuous series when you want the product's price history as one line**: long-horizon trend models, volatility estimation, anything that spans dozens of expiries. The trade-off is that a continuous series is a *construction*: somebody decided when to switch contract and how to hide the price gap. On HF Market Data that somebody is you — the API exposes both choices as parameters. | |
| 115 | + | |
| 116 | +## Roll methods (`roll=`) | |
| 117 | + | |
| 118 | +The roll date is the day the series stops following contract *n* and starts following contract *n+1*. Four rules: | |
| 119 | + | |
| 120 | +| `roll=` | Switch when… | Typical for | | |
| 121 | +| --- | --- | --- | | |
| 122 | +| `volume` (default) | the next contract's daily volume exceeds the current one's | equity indices, rates, FX — tracks where liquidity really is | | |
| 123 | +| `open_interest` | the next contract's open interest exceeds the current one's | energy, metals — OI migrates earlier and more smoothly than volume | | |
| 124 | +| `calendar` | a fixed number of business days before the contract's expiration date | reproducible research, matching a vendor's convention | | |
| 125 | +| `first_notice` | the business day before the first notice date (physically delivered products) | anything you could accidentally be asked to take delivery of | | |
| 126 | + | |
| 127 | +`meta.roll_dates` in every continuous response lists the exact dates and the contracts involved, so you can audit or reproduce the series. `depth=2` or `3` gives you the *second* or *third* contract along the curve under the same rule — useful for spread and term-structure studies. | |
| 128 | + | |
| 129 | +## Adjustment methods (`adjust=`) | |
| 130 | + | |
| 131 | +On the roll date the two contracts trade at different prices — in June 2024 `ESU24` traded several dozen points above `ESM24` because of interest-rate carry (three months of financing minus dividends). If you simply concatenate, the series shows a jump that no one could have traded. The adjustment decides what to do with that gap. | |
| 132 | + | |
| 133 | +<Figure caption="The same roll under the three adjustments. Unadjusted keeps the gap; back-adjusted shifts history by the gap (differences preserved); ratio-adjusted scales history (returns preserved)."> | |
| 134 | +<svg viewBox="0 0 680 250" role="img" aria-label="Three panels showing a price series across a roll: unadjusted with a jump, back-adjusted with history shifted down, ratio-adjusted with history scaled"> | |
| 135 | + {[["adjust=none", 0, "M20 150 L60 140 L100 146 L140 128 L180 132 L180 92 L220 96 L260 84 L300 90", true], | |
| 136 | + ["adjust=back_adjusted", 230, "M20 190 L60 180 L100 186 L140 168 L180 172 L220 176 L260 164 L300 170", false], | |
| 137 | + ["adjust=ratio_adjusted", 460, "M20 186 L60 177 L100 182 L140 166 L180 170 L220 175 L260 163 L300 168", false]].map(([label, dx, d, gap]) => ( | |
| 138 | + <g key={label} transform={`translate(${dx} 0)`}> | |
| 139 | + <rect x="10" y="30" width="200" height="190" rx="8" fill="var(--bg-1)" stroke="var(--line)" /> | |
| 140 | + <text x="20" y="20" fontSize="11.5" fill="var(--fg-1)" fontFamily="var(--mono)">{label}</text> | |
| 141 | + <line x1="110" y1="40" x2="110" y2="210" stroke="var(--accent)" strokeDasharray="3 4" /> | |
| 142 | + <text x="110" y="230" fontSize="10" textAnchor="middle" fill="var(--accent)">roll date</text> | |
| 143 | + <g transform="scale(0.6 1) translate(15 0)"><path d={d} fill="none" stroke={gap ? "var(--danger)" : "var(--accent-2)"} strokeWidth="2.5" strokeLinejoin="round" /></g> | |
| 144 | + {gap && <text x="128" y="120" fontSize="10" fill="var(--danger)">gap</text>} | |
| 145 | + </g> | |
| 146 | + ))} | |
| 147 | +</svg> | |
| 148 | +</Figure> | |
| 149 | + | |
| 150 | +| `adjust=` | What happens to bars before the roll | Preserves | Use for | | |
| 151 | +| --- | --- | --- | --- | | |
| 152 | +| `none` | nothing — raw prices of whichever contract was active | actual traded levels | anything that needs real prices (option pricing, margin, basis) | | |
| 153 | +| `back_adjusted` (default) | shifted by the price *difference* at each roll, cumulatively backwards | point differences → P&L in points/dollars is exact | futures strategies expressed in points, most backtests | | |
| 154 | +| `ratio_adjusted` | multiplied by the price *ratio* at each roll, cumulatively backwards | percentage returns | anything using log/percent returns, cross-asset comparisons | | |
| 155 | + | |
| 156 | +Two facts that surprise people. First, back-adjusted series can go **negative** far back in history for products with large carry (crude oil in 2008 back-adjusted from today easily crosses zero) — that is correct behaviour, not a bug, and the reason ratio adjustment exists. Second, **an adjusted series changes every time a new roll happens**: the whole history shifts. Store the `roll_dates` alongside any adjusted data you cache, or cache the unadjusted contracts and adjust yourself (the [custom continuous recipe](/docs/recipes/custom-continuous) shows how). | |
| 157 | + | |
| 158 | +The older v1 endpoint `GET /v1/bars/futures/{root}` still serves FirstRate Data's own pre-built continuous files (`contin_UNadj`, `contin_adj_ratio`, `contin_adj_absolute`) with a fixed vendor roll rule. They are unchanged and remain available; the v2 `continuous` endpoint is the one where the rule is yours. | |
| 159 | + | |
| 160 | +## Worked example: the June 2024 ES roll | |
| 161 | + | |
| 162 | +1. **Who was active?** `GET /v1/futures/ES/chain?as_of=2024-06-03` returns the listed contracts with that day's close, volume and open interest — `ESM24` (June) still dominant, `ESU24` (September) picking up. | |
| 163 | +2. **When did volume cross?** `GET /v1/futures/ES/continuous?roll=volume&adjust=none&from=2024-06-01&to=2024-06-30` — `meta.roll_dates` contains the day `ESU24` out-traded `ESM24` (for ES this typically happens during the CME roll week, about eight days before the third-Friday expiry). `open_interest` usually rolls a little earlier. | |
| 164 | +3. **How big was the gap?** Fetch both contracts around that date with `GET /v1/futures/contract/ESM24/bars?interval=1d&from=2024-06-10&to=2024-06-14` and the same for `ESU24`; the close-to-close difference on the roll date is the constant that `back_adjusted` subtracts from every earlier bar. | |
| 165 | +4. **Check the adjusted series.** Request the same window with `adjust=back_adjusted`: bars after the roll are identical to `ESU24`, bars before are `ESM24 − gap`. With `adjust=ratio_adjusted` they are `ESM24 × (ESU24 / ESM24 on roll date)` instead. | |
| 166 | + | |
| 167 | +The right column has the three requests ready to run — swap `ES` for `CL` and `as_of` for any date since 2010. | |
| 168 | + | |
| 169 | +## Coverage and gaps | |
| 170 | + | |
| 171 | +Individual contract data comes from two file sets — an *archive* (≤ 2025) and a rolling *update* (≥ 2025) that overlap; the API deduplicates on timestamp, update winning. Not every contract has every timeframe (the daily set has ~15 000 contracts, intraday ~14 500), and far-dated contracts trade rarely so their history has holes. Rather than filling them, the API reports them: | |
| 172 | + | |
| 173 | +- `GET /v1/futures/contract/{symbol}/coverage` — first/last date per interval, bar counts, and `gaps` (runs of more than three business days without a bar). | |
| 174 | +- In every response, a missing value is `null` and `meta.coverage` says why when the API knows. | |
| 175 | + | |
| 176 | +No value is ever interpolated or invented. If your model needs a dense calendar, forward-fill on your side, knowingly. | |
added
hfmarketdata/web/content/guides/options.mdx
+94 −0
@@ -0,0 +1,94 @@ | ||
| 1 | +export const meta = { title: 'Options chains & Greeks', description: 'End-of-day options chains since 2010 with quotes, implied volatility and the full Greek set — how the data is sampled, how to query chains, expirations and single-contract histories.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'A filtered chain on one day', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/options/chain/AAPL?trade_date=2024-06-21&call_put=c&strike_min=200&strike_max=210&limit=5"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +r = requests.get("https://www.hfmarketdata.io/v1/options/chain/AAPL", | |
| 10 | + params={"trade_date": "2024-06-21", "call_put": "c", "strike_min": 200, "strike_max": 210}, timeout=60) | |
| 11 | +chain = pd.DataFrame(r.json()["data"]) | |
| 12 | +print(chain[["expiry", "strike", "last_price", "bid", "ask", "ask_iv", "delta", "open_interest"]])`, | |
| 13 | + javascript: `const url = "https://www.hfmarketdata.io/v1/options/chain/AAPL?trade_date=2024-06-21&call_put=c&strike_min=200&strike_max=210"; | |
| 14 | +const { count, data } = await (await fetch(url)).json(); | |
| 15 | +console.log(count, data.map((c) => [c.expiry, c.strike, c.delta]));`, | |
| 16 | + r: `library(httr2) | |
| 17 | +chain <- request("https://www.hfmarketdata.io/v1/options/chain/AAPL") |> | |
| 18 | + req_url_query(trade_date = "2024-06-21", call_put = "c", strike_min = 200, strike_max = 210) |> | |
| 19 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 20 | +chain$data[, c("expiry", "strike", "last_price", "ask_iv", "delta")]`, | |
| 21 | + }, | |
| 22 | + { | |
| 23 | + title: 'The life of one contract', | |
| 24 | + curl: `curl "https://www.hfmarketdata.io/v1/options/history/AAPL?strike=200&expiry=2024-12-20&call_put=c&format=csv" -o aapl_200c_dec24.csv`, | |
| 25 | + python: `hist = pd.read_csv( | |
| 26 | + "https://www.hfmarketdata.io/v1/options/history/AAPL?strike=200&expiry=2024-12-20&call_put=c&format=csv", | |
| 27 | + parse_dates=["trade_date"], index_col="trade_date", | |
| 28 | +) | |
| 29 | +print(hist[["last_price", "bid_iv", "ask_iv", "delta", "theta", "open_interest"]].tail())`, | |
| 30 | + javascript: `const csv = await (await fetch("https://www.hfmarketdata.io/v1/options/history/AAPL?strike=200&expiry=2024-12-20&call_put=c&format=csv")).text(); | |
| 31 | +console.log(csv.split("\\n").length - 2, "trading days");`, | |
| 32 | + r: `hist <- read.csv("https://www.hfmarketdata.io/v1/options/history/AAPL?strike=200&expiry=2024-12-20&call_put=c&format=csv") | |
| 33 | +tail(hist[, c("trade_date", "last_price", "ask_iv", "delta", "theta")])`, | |
| 34 | + }, | |
| 35 | +] | |
| 36 | + | |
| 37 | +# Options chains & Greeks | |
| 38 | + | |
| 39 | +The options dataset is an **end-of-day snapshot of every listed contract, for every trading day since 2010**, for 5 900+ US underlyings — equities, ETFs and indices such as `SPX` and `VIX`. Each row is one contract on one day: quotes, implied volatility on both sides, open interest, volume and the five Greeks. | |
| 40 | + | |
| 41 | +## How the data is sampled | |
| 42 | + | |
| 43 | +Each daily row is a snapshot taken **30 seconds before the close**. That timing is deliberate: the closing auction and the rebalancing orders of the last seconds distort quotes on thinly traded strikes, so a 15:59:30 snapshot is a better description of the day's end state than the official 16:00:00 print. Consequences: | |
| 44 | + | |
| 45 | +- `last_price` is the last trade *at snapshot time* — for an illiquid contract it may be days old. | |
| 46 | +- `bid` / `ask` are the best quotes at snapshot time. `bid_iv` and `ask_iv` are the implied volatilities solved from each side; when a side is zero (no bid), its IV is reported as `0` rather than invented. | |
| 47 | +- Greeks and implied volatilities are those of the source dataset (FirstRate Data), computed from the end-of-day quotes with one consistent model across all contracts; the API serves them as delivered. `NaN` and infinite values — deep in-the-money on expiry day, for instance — are serialised as `null`. | |
| 48 | + | |
| 49 | +The row you saw in the first snippet, from 2024-06-21, illustrates this: the AAPL 200 call **expiring that very day** shows `delta: 1`, `gamma: 0`, `vega: 0` and a `bid_iv` of `0` — a contract that is pure intrinsic value with no time left. | |
| 50 | + | |
| 51 | +## Fields | |
| 52 | + | |
| 53 | +| Field | Type | Meaning | | |
| 54 | +| --- | --- | --- | | |
| 55 | +| `ticker` | string | Underlying symbol | | |
| 56 | +| `trade_date` | date | Snapshot day | | |
| 57 | +| `expiry` | date | Contract expiration | | |
| 58 | +| `strike` | number | Strike price | | |
| 59 | +| `call_put` | `c` / `p` | Side | | |
| 60 | +| `last_price` | number | Last trade at snapshot time | | |
| 61 | +| `bid`, `ask` | number | Best quotes at snapshot time | | |
| 62 | +| `bid_iv`, `ask_iv` | number | Implied volatility from bid / from ask (annualised, decimal: `0.2708` = 27.08 %) | | |
| 63 | +| `open_interest` | number | Open contracts | | |
| 64 | +| `volume` | number | Contracts traded that day | | |
| 65 | +| `delta`, `gamma`, `vega`, `theta`, `rho` | number | Greeks from the mid (per 1 point of underlying, per point of delta, per 1 vol point, per day, per 1 % rate) | | |
| 66 | + | |
| 67 | +## The five endpoints | |
| 68 | + | |
| 69 | +Everything lives under `/v1/options` and works keyless. Quarters are how the archive is organised on disk — `2010_q1` … `2026_q2`, one file per underlying per quarter — which is why two endpoints mention them. | |
| 70 | + | |
| 71 | +1. <Endpoint method="GET" path="/v1/options/quarters" id="opt_quarters_v1_options_quarters_get" /> — the 66 quarters available. | |
| 72 | +2. <Endpoint method="GET" path="/v1/options/tickers" id="opt_tickers_v1_options_tickers_get" /> — underlyings with data in a quarter (`?quarter=2024_q4&search=TSL`). | |
| 73 | +3. <Endpoint method="GET" path="/v1/options/expirations/{ticker}" id="opt_expirations_v1_options_expirations__ticker__get" /> — expiry dates listed on a `trade_date`. The natural first call: it tells you which `expiry` values a chain request can filter on. On 2024-06-21 AAPL had weeklies out to August, then monthlies (`2024-09-20`, `2024-10-18`, …, `2024-12-20`) and LEAPS. | |
| 74 | +4. <Endpoint method="GET" path="/v1/options/chain/{ticker}" id="opt_chain_v1_options_chain__ticker__get" /> — the chain on one `trade_date` (default: latest), filterable by `expiry`, `call_put`, `strike_min` / `strike_max`, `min_volume`. Up to 20 000 rows; use `format=csv` for a whole unfiltered chain (a busy AAPL day is ~2 100 contracts, SPX several times that). | |
| 75 | +5. <Endpoint method="GET" path="/v1/options/history/{ticker}" id="opt_history_v1_options_history__ticker__get" /> — one contract (`strike` + `expiry` + `call_put`) day by day across its life. The second snippet pulls the AAPL 200 call of December 2024 from its listing in 2023 to expiry. | |
| 76 | + | |
| 77 | +<Callout type="tip" title="Chain first, then history"> | |
| 78 | +`chain` answers “what was available and how was it priced on day D”; `history` answers “how did this one contract evolve”. Combine them: pick a contract from a chain (say, the ~25-delta put three months out), then pull its history to study how it decayed. | |
| 79 | +</Callout> | |
| 80 | + | |
| 81 | +## Practical notes | |
| 82 | + | |
| 83 | +- **Adjusted strikes.** After a split or special dividend the OCC adjusts existing contracts; they show up with non-standard strikes (`142.86`) or a modified deliverable. They are kept as listed — filter on `strike % 0.5 == 0` if you only want standard contracts. | |
| 84 | +- **Sparse rows for illiquid strikes.** Far out-of-the-money contracts can go days without a quote; a `history` for such a contract may have fewer rows than trading days. Nothing is filled. | |
| 85 | +- **Index options** (`SPX`, `NDX`, `RUT`, `VIX`) are cash-settled and European-style; equity options are American-style. Keep that in mind when comparing Greeks across the two families, especially deep in-the-money close to a dividend. | |
| 86 | +- **Volume is contracts, not notional.** Multiply by 100 (the multiplier for standard US equity options) for shares-equivalent. | |
| 87 | +- **Quota.** A full chain is a few thousand rows, so keyless callers (5 000 rows per request) should filter or use CSV; a free key comfortably handles unfiltered chains and long histories. | |
| 88 | + | |
| 89 | +## Common recipes | |
| 90 | + | |
| 91 | +- **Implied volatility surface for a day**: chain with no filters, then pivot `ask_iv` (or the mid IV `(bid_iv + ask_iv) / 2`, excluding zero bids) by `expiry` × `strike`. | |
| 92 | +- **Put/call ratio series**: for each `trade_date`, sum `volume` per side from the chain — or, cheaper, pull the chain once per day with `min_volume=1`. | |
| 93 | +- **Term structure of ATM IV**: `expirations` for the date, then one filtered `chain` call per expiry with `strike_min`/`strike_max` around the spot from `GET /v1/bars/stock/{ticker}`. | |
| 94 | +- **Realised vs implied**: 1-minute bars from `/v1/bars/stock/{ticker}?timeframe=1min` give you realised variance to compare with `ask_iv` of the front expiry. | |
added
hfmarketdata/web/content/guides/quickstart.mdx
+114 −0
@@ -0,0 +1,114 @@ | ||
| 1 | +export const meta = { title: 'Quickstart', description: 'Your first HF Market Data request in 30 seconds — no signup, no key.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: '1 · One week of 1-minute AAPL bars', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min&start=2024-06-03%2009:30:00&end=2024-06-03%2010:00:00"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +r = requests.get( | |
| 10 | + "https://www.hfmarketdata.io/v1/bars/stock/AAPL", | |
| 11 | + params={"timeframe": "1min", "start": "2024-06-03 09:30:00", "end": "2024-06-03 10:00:00"}, | |
| 12 | + timeout=60, | |
| 13 | +) | |
| 14 | +r.raise_for_status() | |
| 15 | +df = pd.DataFrame(r.json()["data"]) | |
| 16 | +df["datetime"] = pd.to_datetime(df["datetime"]) | |
| 17 | +print(df.head())`, | |
| 18 | + javascript: `const url = new URL("https://www.hfmarketdata.io/v1/bars/stock/AAPL"); | |
| 19 | +url.search = new URLSearchParams({ timeframe: "1min", start: "2024-06-03 09:30:00", end: "2024-06-03 10:00:00" }); | |
| 20 | +const res = await fetch(url); | |
| 21 | +const { count, data } = await res.json(); | |
| 22 | +console.log(count, data[0]);`, | |
| 23 | + r: `library(httr2) | |
| 24 | + | |
| 25 | +resp <- request("https://www.hfmarketdata.io/v1/bars/stock/AAPL") |> | |
| 26 | + req_url_query(timeframe = "1min", start = "2024-06-03 09:30:00", end = "2024-06-03 10:00:00") |> | |
| 27 | + req_perform() | |
| 28 | +bars <- resp_body_json(resp, simplifyVector = TRUE)$data | |
| 29 | +head(bars)`, | |
| 30 | + }, | |
| 31 | + { | |
| 32 | + title: '2 · Straight into pandas with CSV', | |
| 33 | + curl: `curl "https://www.hfmarketdata.io/v1/bars/etf/SPY?timeframe=1day&start=2020-01-01&format=csv" -o spy.csv`, | |
| 34 | + python: `import pandas as pd | |
| 35 | + | |
| 36 | +df = pd.read_csv( | |
| 37 | + "https://www.hfmarketdata.io/v1/bars/etf/SPY?timeframe=1day&start=2020-01-01&format=csv", | |
| 38 | + parse_dates=["datetime"], index_col="datetime", | |
| 39 | +) | |
| 40 | +print(df["close"].resample("ME").last().pct_change().tail())`, | |
| 41 | + javascript: `const res = await fetch("https://www.hfmarketdata.io/v1/bars/etf/SPY?timeframe=1day&start=2020-01-01&format=csv"); | |
| 42 | +const [header, ...rows] = (await res.text()).trim().split("\\n"); | |
| 43 | +console.log(header, rows.length, "rows");`, | |
| 44 | + r: `spy <- read.csv("https://www.hfmarketdata.io/v1/bars/etf/SPY?timeframe=1day&start=2020-01-01&format=csv") | |
| 45 | +spy$datetime <- as.Date(spy$datetime) | |
| 46 | +tail(spy)`, | |
| 47 | + }, | |
| 48 | +] | |
| 49 | + | |
| 50 | +# Quickstart | |
| 51 | + | |
| 52 | +You can pull real data **right now**, with nothing but a URL. No account, no key, no SDK. The base URL is `https://www.hfmarketdata.io` and every endpoint lives under `/v1`. | |
| 53 | + | |
| 54 | +<Callout type="tip" title="30-second version"> | |
| 55 | +Paste this in a terminal: `curl "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day&limit=5"`. You just made a keyless request (30 per hour, 5 000 rows each). Everything else on this page is refinement. | |
| 56 | +</Callout> | |
| 57 | + | |
| 58 | +## 1. Pick an instrument | |
| 59 | + | |
| 60 | +The lake covers seven asset types. Use the one that matches your symbol: | |
| 61 | + | |
| 62 | +| `asset` | What it holds | Example ticker | Adjustments | | |
| 63 | +| --- | --- | --- | --- | | |
| 64 | +| `stock` | ~7 600 US equities | `AAPL` | `adj_splitdiv` (default), `adj_split`, `UNADJUSTED` | | |
| 65 | +| `etf` | ~5 100 US ETFs | `SPY` | same as stocks | | |
| 66 | +| `futures` | 130+ continuous front-month series | `ES`, `CL`, `GC` | `contin_UNadj` (default), `contin_adj_ratio`, `contin_adj_absolute` | | |
| 67 | +| `futures_contracts` | ~15 000 individual expiries | `ESH24` | `archive`, `update` | | |
| 68 | +| `crypto` | 74 pairs | `BTCUSD` | `none` | | |
| 69 | +| `index` | 120+ equity & vol indices | `SPX`, `VIX` | `none` | | |
| 70 | +| `fx` | 79 pairs | `EURUSD` | `none` | | |
| 71 | + | |
| 72 | +Not sure of a symbol? `GET /v1/{asset}/tickers?search=AAP` returns every match for a timeframe / adjustment pair. The live counts per dataset are on the [status page](/status). | |
| 73 | + | |
| 74 | +## 2. Ask for bars | |
| 75 | + | |
| 76 | +The workhorse is <Endpoint method="GET" path="/v1/bars/{asset}/{ticker}" id="bars_v1_bars__asset___ticker__get" />. Five timeframes — `1min`, `5min`, `30min`, `1hour`, `1day` — and any window you like: `start` and `end` accept a date (`2024-06-03`) or a full timestamp (`2024-06-03 09:30:00`). The first snippet on the right fetches the opening half-hour of a single session. | |
| 77 | + | |
| 78 | +```json title="Response (JSON)" | |
| 79 | +{ | |
| 80 | + "count": 30, | |
| 81 | + "data": [ | |
| 82 | + { "ticker": "AAPL", "datetime": "2024-06-03 09:30:00", "open": 191.3528, "high": 191.6799, "low": 191.1446, "close": 191.6601, "volume": 1521665 }, | |
| 83 | + { "ticker": "AAPL", "datetime": "2024-06-03 09:31:00", "open": 191.6403, "high": 191.7295, "low": 190.9265, "close": 191.1248, "volume": 385549 } | |
| 84 | + ] | |
| 85 | +} | |
| 86 | +``` | |
| 87 | + | |
| 88 | +(Prices are `adj_splitdiv` — split **and** dividend adjusted — because that is the default for stocks; pass `adjustment=UNADJUSTED` to get the prices as printed that day.) | |
| 89 | + | |
| 90 | +A few things worth knowing before you go further: | |
| 91 | + | |
| 92 | +- **Intraday timestamps of the v1 bars endpoints are US/Eastern**, without an offset, exactly as the exchange publishes them. v2 endpoints (futures contracts, fundamentals) return UTC ISO 8601. The [time zones guide](/docs/time-zones) has the details. | |
| 93 | +- **`limit` defaults to 5 000 rows.** Keyless callers are capped at 5 000 per request; a free key raises that to 50 000. Above that, switch to `format=csv` (up to 2 000 000 rows on the v1 bars endpoints), `format=parquet` on v2 endpoints, or the [bulk downloads](/docs/bulk-downloads). | |
| 94 | +- **Zero-volume bars are not in the dataset**, so a minute with no trade simply has no row. Reindex if your model needs a dense grid. | |
| 95 | + | |
| 96 | +## 3. Load it into your tools | |
| 97 | + | |
| 98 | +Prefer `format=csv` when the destination is a DataFrame — half the bytes of JSON and `pandas.read_csv` (or R's `read.csv`) can consume the URL directly, as the second snippet shows. The [data formats guide](/docs/data-formats) benchmarks JSON, CSV and Parquet on real requests. | |
| 99 | + | |
| 100 | +## 4. Go further | |
| 101 | + | |
| 102 | +- **Watchlists in one call** — <Endpoint method="GET" path="/v1/bars/{asset}" id="bars_multi_v1_bars__asset__get" /> takes `tickers=AAPL,MSFT,NVDA` (up to 50), `limit` applies per ticker. | |
| 103 | +- **The market at 10:35:00** — <Endpoint method="GET" path="/v1/snapshot/{asset}" id="snapshot_v1_snapshot__asset__get" /> returns the last bar at or before an instant for every ticker you pass. | |
| 104 | +- **Options with Greeks** — <Endpoint method="GET" path="/v1/options/chain/{ticker}" id="opt_chain_v1_options_chain__ticker__get" /> gives the full end-of-day chain; see the [options guide](/docs/options). | |
| 105 | +- **Individual futures contracts, roll methods, term structure** — the [futures guide](/docs/futures-contracts). | |
| 106 | +- **Balance sheets, ratios, point-in-time** — the [fundamentals section](/docs/fundamentals/balance-sheet). | |
| 107 | + | |
| 108 | +## 5. When you outgrow keyless access | |
| 109 | + | |
| 110 | +Thirty requests an hour is plenty to explore, not to run a pipeline. A [free account](/signup) gives you an API key with **120 requests and 1 000 000 rows per minute** — free, just an e-mail address. Heavier needs (600 req/min, 10 000 000 rows/min) are granted on request to [contact@spboucher.ai](mailto:contact@spboucher.ai?subject=High%20usage%20tier%20request). All numbers, including how they are counted, are on the [rate limits](/docs/rate-limits) page. | |
| 111 | + | |
| 112 | +<Callout type="info" title="Try any endpoint without writing code"> | |
| 113 | +Every reference page has a **Try it** button that opens the [playground](/playground) with the example pre-filled. The playground also shows you the remaining quota after each call. | |
| 114 | +</Callout> | |
added
hfmarketdata/web/content/guides/rate-limits.mdx
+176 −0
@@ -0,0 +1,176 @@ | ||
| 1 | +export const meta = { title: 'Rate limits', description: 'The three tiers, how requests and rows are counted, the X-RateLimit headers, and how to handle 429 in curl, Python, JavaScript and R.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Handle 429 with Retry-After', | |
| 6 | + curl: `# --retry honours Retry-After on 429 (curl ≥ 7.71) | |
| 7 | +curl --retry 5 --retry-all-errors --retry-max-time 300 \\ | |
| 8 | + -H "Authorization: Bearer $HFMD_API_KEY" \\ | |
| 9 | + "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min&start=2024-06-03&end=2024-06-07&format=csv" \\ | |
| 10 | + -o aapl.csv`, | |
| 11 | + python: `import os, time, requests | |
| 12 | + | |
| 13 | +session = requests.Session() | |
| 14 | +session.headers["Authorization"] = f"Bearer {os.environ['HFMD_API_KEY']}" | |
| 15 | + | |
| 16 | +def get(url, **params): | |
| 17 | + for attempt in range(6): | |
| 18 | + r = session.get(url, params=params, timeout=60) | |
| 19 | + if r.status_code != 429: | |
| 20 | + r.raise_for_status() | |
| 21 | + return r | |
| 22 | + err = r.json()["error"] # code, message, docs, type | |
| 23 | + wait = int(r.headers.get("Retry-After", 2 ** attempt)) | |
| 24 | + print(f"429 {err['type']} — sleeping {wait}s ({err['docs']})") | |
| 25 | + time.sleep(wait) | |
| 26 | + raise RuntimeError("still rate limited after 6 attempts") | |
| 27 | + | |
| 28 | +r = get("https://www.hfmarketdata.io/v1/bars/stock/AAPL", timeframe="1min", start="2024-06-03", end="2024-06-07") | |
| 29 | +print(r.headers["X-RateLimit-Remaining-Rows"], "rows left this minute")`, | |
| 30 | + javascript: `const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000)); | |
| 31 | + | |
| 32 | +async function get(url, attempt = 0) { | |
| 33 | + const res = await fetch(url, { headers: { Authorization: \`Bearer \${process.env.HFMD_API_KEY}\` } }); | |
| 34 | + if (res.status !== 429) { | |
| 35 | + if (!res.ok) { const { error } = await res.json(); throw new Error(\`\${error.code}: \${error.message}\`); } | |
| 36 | + return res; | |
| 37 | + } | |
| 38 | + if (attempt >= 5) throw new Error("still rate limited"); | |
| 39 | + const { error } = await res.json(); | |
| 40 | + const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt); | |
| 41 | + console.warn(\`429 \${error.type} — waiting \${wait}s\`); | |
| 42 | + await sleep(wait); | |
| 43 | + return get(url, attempt + 1); | |
| 44 | +} | |
| 45 | + | |
| 46 | +const res = await get("https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min&start=2024-06-03&end=2024-06-07"); | |
| 47 | +console.log(res.headers.get("X-RateLimit-Remaining-Rows"), "rows left this minute");`, | |
| 48 | + r: `library(httr2) | |
| 49 | + | |
| 50 | +resp <- request("https://www.hfmarketdata.io/v1/bars/stock/AAPL") |> | |
| 51 | + req_url_query(timeframe = "1min", start = "2024-06-03", end = "2024-06-07") |> | |
| 52 | + req_auth_bearer_token(Sys.getenv("HFMD_API_KEY")) |> | |
| 53 | + req_retry(max_tries = 6, is_transient = \\(r) resp_status(r) == 429, | |
| 54 | + after = \\(r) as.numeric(resp_header(r, "Retry-After", 2))) |> | |
| 55 | + req_perform() | |
| 56 | + | |
| 57 | +resp_header(resp, "X-RateLimit-Remaining-Rows")`, | |
| 58 | + }, | |
| 59 | + { | |
| 60 | + title: 'Free re-reads with ETag', | |
| 61 | + curl: `# first call: store the ETag | |
| 62 | +curl -sD headers.txt "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day&start=2024-01-01&end=2024-06-30" -o bars.json | |
| 63 | +ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2 | tr -d '\\r') | |
| 64 | +# later: 304 costs nothing against your quota | |
| 65 | +curl -s -o /dev/null -w "%{http_code}\\n" -H "If-None-Match: $ETAG" \\ | |
| 66 | + "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day&start=2024-01-01&end=2024-06-30"`, | |
| 67 | + python: `etag, cached = None, None | |
| 68 | +def fetch_cached(url): | |
| 69 | + global etag, cached | |
| 70 | + headers = {"If-None-Match": etag} if etag else {} | |
| 71 | + r = session.get(url, headers=headers, timeout=60) | |
| 72 | + if r.status_code == 304: # free: 0 requests, 0 rows counted | |
| 73 | + return cached | |
| 74 | + r.raise_for_status() | |
| 75 | + etag, cached = r.headers.get("ETag"), r.json() | |
| 76 | + return cached`, | |
| 77 | + javascript: `let etag = null, cached = null; | |
| 78 | +async function fetchCached(url) { | |
| 79 | + const res = await fetch(url, { headers: etag ? { "If-None-Match": etag } : {} }); | |
| 80 | + if (res.status === 304) return cached; // free: not counted | |
| 81 | + etag = res.headers.get("ETag"); | |
| 82 | + cached = await res.json(); | |
| 83 | + return cached; | |
| 84 | +}`, | |
| 85 | + r: `# httr2 has a built-in cache that honours ETag / If-None-Match: | |
| 86 | +resp <- request("https://www.hfmarketdata.io/v1/bars/stock/AAPL") |> | |
| 87 | + req_url_query(timeframe = "1day", start = "2024-01-01", end = "2024-06-30") |> | |
| 88 | + req_cache(tempdir()) |> | |
| 89 | + req_perform()`, | |
| 90 | + }, | |
| 91 | +] | |
| 92 | + | |
| 93 | +# Rate limits | |
| 94 | + | |
| 95 | +Limits exist so that one runaway loop cannot degrade the service for everyone. They are generous by design, transparent (every response tells you where you stand), and there are three cheap ways to stretch them — Parquet, bulk endpoints and ETags. | |
| 96 | + | |
| 97 | +## The tiers | |
| 98 | + | |
| 99 | +| | Keyless (per IP) | Free account + key | High usage | | |
| 100 | +| --- | --- | --- | --- | | |
| 101 | +| Window | **1 hour** | **1 minute** | **1 minute** | | |
| 102 | +| Requests per window | 30 | 120 | 600 | | |
| 103 | +| Rows per window | 100 000 | 1 000 000 | 10 000 000 | | |
| 104 | +| Max rows per request | 5 000 | 50 000 | 200 000 | | |
| 105 | +| How to get it | nothing | [sign up](/signup) | e-mail [contact@spboucher.ai](mailto:contact@spboucher.ai?subject=High%20usage%20tier%20request) | | |
| 106 | + | |
| 107 | +The same numbers are published by the API itself in `info.x-tiers` of [`/openapi.json`](/openapi.json), and `GET /v1/limits` returns your current tier and remaining budget. The [access & limits page](/limits) is a friendlier view of the same table. Everything is free — the higher tiers are granted, not sold. | |
| 108 | + | |
| 109 | +## Two budgets, not one | |
| 110 | + | |
| 111 | +Every request is checked against **two sliding windows** — a *requests* counter and a *rows* counter — and both must have room. | |
| 112 | + | |
| 113 | +- **Requests**: each HTTP request costs 1, whatever it returns. A few expensive endpoints (the fundamentals screener and `frames`) cost 2 — the reference page says so. | |
| 114 | +- **Rows**: the number of data rows in the response (the `X-Row-Count` header) is deducted *after* the response is produced. A 5 000-row bars call costs 5 000 rows. Metadata endpoints (`/v1/status`, ticker lists, coverage) cost 0 rows. | |
| 115 | + | |
| 116 | +The windows are *sliding*, evaluated at one-second resolution: if you spend your 120 requests in the first 10 seconds of a minute, you wait ~50 seconds, not until the next wall-clock minute. `X-RateLimit-Reset` gives the Unix time when enough budget will have expired for one more request. | |
| 117 | + | |
| 118 | +## How to stretch your limits | |
| 119 | + | |
| 120 | +| Trick | Effect on requests | Effect on rows | | |
| 121 | +| --- | --- | --- | | |
| 122 | +| `format=parquet` (v2 tabular endpoints) | 1 | **½** — rows count half | | |
| 123 | +| `/v1/bulk/*` downloads | 1 | **0** — whole-universe extracts are outside the rows quota | | |
| 124 | +| `If-None-Match` → `304 Not Modified` | **0** | **0** — unchanged data is free | | |
| 125 | +| Larger `limit` (fewer calls) | fewer | same | | |
| 126 | +| `format=csv` on v1 bars endpoints (up to 2 000 000 rows) | fewer | same — but a single call instead of forty | | |
| 127 | + | |
| 128 | +A practical consequence: a free key downloading a 10-year 1-minute history (~2.7 M rows) is best served by one `format=parquet` request per year-ish chunk (each ≤ 50 000 rows… so about 55 calls, each counted at half its rows), or by the [bulk downloads](/docs/bulk-downloads) when the dataset exists there. The [data formats guide](/docs/data-formats) has the byte sizes. | |
| 129 | + | |
| 130 | +## The headers | |
| 131 | + | |
| 132 | +Every response — success or error, keyed or keyless — carries: | |
| 133 | + | |
| 134 | +| Header | Meaning | | |
| 135 | +| --- | --- | | |
| 136 | +| `X-RateLimit-Limit-Requests` | Requests allowed in the window for your tier | | |
| 137 | +| `X-RateLimit-Remaining-Requests` | Requests left right now | | |
| 138 | +| `X-RateLimit-Limit-Rows` | Rows allowed in the window | | |
| 139 | +| `X-RateLimit-Remaining-Rows` | Rows left right now | | |
| 140 | +| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window has slid enough to allow the next request | | |
| 141 | +| `X-Row-Count` | Rows in *this* response (what was just deducted) | | |
| 142 | +| `Retry-After` | On 429 only — seconds to wait | | |
| 143 | + | |
| 144 | +Logging `X-RateLimit-Remaining-Rows` next to your job progress is the cheapest observability you will ever add. | |
| 145 | + | |
| 146 | +## What happens on 429 | |
| 147 | + | |
| 148 | +The request is **not executed** and you get: | |
| 149 | + | |
| 150 | +```json | |
| 151 | +{ | |
| 152 | + "error": { | |
| 153 | + "code": "RATE_LIMIT_EXCEEDED", | |
| 154 | + "message": "Row quota exhausted: 1000000 rows per minute for the free tier.", | |
| 155 | + "docs": "https://www.hfmarketdata.io/docs/errors#rate_limit_exceeded", | |
| 156 | + "type": "rows_per_minute", | |
| 157 | + "details": { "limit": 1000000, "window": "1m", "reset": 1757030460 } | |
| 158 | + }, | |
| 159 | + "detail": "Row quota exhausted: 1000000 rows per minute for the free tier." | |
| 160 | +} | |
| 161 | +``` | |
| 162 | + | |
| 163 | +`error.type` tells you *which* budget ran out — `requests_per_hour`, `requests_per_minute`, `rows_per_hour` or `rows_per_minute` — and `Retry-After` how long to wait. The right column shows a robust loop in each language: wait `Retry-After` (fall back to exponential backoff if the header is missing), retry a bounded number of times, and never retry other 4xx (those are bugs in the request, not transient conditions). | |
| 164 | + | |
| 165 | +A separate, related error is `400 ROW_LIMIT_EXCEEDED`: you asked for more rows in one request than your tier allows (`limit=100000` on a free key). That one is not about pacing — lower `limit`, paginate with `cursor`, or use CSV / Parquet / bulk. | |
| 166 | + | |
| 167 | +## Best practices | |
| 168 | + | |
| 169 | +1. **Get a free key.** It moves you from 30 requests/hour to 120/minute. It takes a minute and it is the single biggest lever. | |
| 170 | +2. **Ask for big windows, not many small ones.** One call with `start`/`end` spanning a year beats 252 daily calls by 252×. | |
| 171 | +3. **Cache aggressively.** History does not change. Store what you pull; revalidate with `If-None-Match` — a 304 is free. | |
| 172 | +4. **Prefer Parquet** on v2 endpoints when you can read it (pandas, DuckDB, Arrow, Polars all can). Half the rows cost, a fraction of the bytes. | |
| 173 | +5. **Use bulk endpoints** for universe-wide work — they do not touch the rows budget at all. | |
| 174 | +6. **Respect `Retry-After`.** Hammering a 429 gains nothing — the window slides at one-second resolution, so the header already tells you the earliest useful moment. | |
| 175 | +7. **One key per environment.** When a job misbehaves you can revoke that key alone and read its usage in the dashboard. | |
| 176 | +8. **Need more?** The high-usage tier is one e-mail away; describe the workload and we will size it. | |
added
hfmarketdata/web/content/guides/recipe-crude-term-structure.mdx
+98 −0
@@ -0,0 +1,98 @@ | ||
| 1 | +export const meta = { title: 'Recipe: analyse crude oil term structure', description: 'Contango and backwardation from the CL futures curve — the term-structure endpoint, the front spread through time, and the roll yield a long position actually earns.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'The curve on one day', | |
| 6 | + curl: `curl "https://www.hfmarketdata.io/v1/futures/CL/term-structure?as_of=2024-06-03"`, | |
| 7 | + python: `import requests, pandas as pd | |
| 8 | + | |
| 9 | +BASE = "https://www.hfmarketdata.io" | |
| 10 | +curve = pd.DataFrame(requests.get(f"{BASE}/v1/futures/CL/term-structure", params={"as_of": "2024-06-03"}).json()["data"]) | |
| 11 | +curve["expiration_date"] = pd.to_datetime(curve["expiration_date"]) | |
| 12 | +curve["months_out"] = ((curve["expiration_date"] - pd.Timestamp("2024-06-03")).dt.days / 30.4).round(1) | |
| 13 | +print(curve[["symbol", "expiration_date", "months_out", "close", "open_interest"]].head(12)) | |
| 14 | +front, second = curve["close"].iloc[0], curve["close"].iloc[1] | |
| 15 | +print("front spread:", round(front - second, 2), "→", "backwardation" if front > second else "contango")`, | |
| 16 | + javascript: `const { data } = await (await fetch("https://www.hfmarketdata.io/v1/futures/CL/term-structure?as_of=2024-06-03")).json(); | |
| 17 | +console.table(data.slice(0, 12).map(({ symbol, expiration_date, close }) => ({ symbol, expiration_date, close }))); | |
| 18 | +console.log(data[0].close > data[1].close ? "backwardation" : "contango");`, | |
| 19 | + r: `library(httr2) | |
| 20 | +curve <- request("https://www.hfmarketdata.io/v1/futures/CL/term-structure") |> | |
| 21 | + req_url_query(as_of = "2024-06-03") |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 22 | +head(curve$data[, c("symbol", "expiration_date", "close", "open_interest")], 12) | |
| 23 | +with(curve$data, ifelse(close[1] > close[2], "backwardation", "contango"))`, | |
| 24 | + }, | |
| 25 | + { | |
| 26 | + title: 'The front spread through time (depth=1 vs depth=2)', | |
| 27 | + curl: `curl "https://www.hfmarketdata.io/v1/futures/CL/continuous?roll=open_interest&adjust=none&depth=1&interval=1d&from=2020-01-01" -o cl1.json | |
| 28 | +curl "https://www.hfmarketdata.io/v1/futures/CL/continuous?roll=open_interest&adjust=none&depth=2&interval=1d&from=2020-01-01" -o cl2.json`, | |
| 29 | + python: `def cont(depth): | |
| 30 | + r = requests.get(f"{BASE}/v1/futures/CL/continuous", params={ | |
| 31 | + "roll": "open_interest", "adjust": "none", "depth": depth, "interval": "1d", "from": "2020-01-01", "limit": 50000}) | |
| 32 | + df = pd.DataFrame(r.json()["data"]); df["datetime"] = pd.to_datetime(df["datetime"], utc=True) | |
| 33 | + return df.set_index("datetime")["close"] | |
| 34 | + | |
| 35 | +m1, m2 = cont(1), cont(2) | |
| 36 | +spread = (m1 - m2).rename("front_spread") # > 0 backwardation, < 0 contango | |
| 37 | +pct = (spread / m2 * 100).rename("spread_pct") | |
| 38 | +print(pct.resample("QE").mean().round(2).tail(12)) | |
| 39 | +print("days in contango:", (spread < 0).mean().round(3))`, | |
| 40 | + javascript: `const get = (depth) => fetch(\`https://www.hfmarketdata.io/v1/futures/CL/continuous?roll=open_interest&adjust=none&depth=\${depth}&interval=1d&from=2020-01-01&limit=50000\`).then((r) => r.json()); | |
| 41 | +const [m1, m2] = await Promise.all([get(1), get(2)]); | |
| 42 | +const byDate = new Map(m2.data.map((b) => [b.datetime, b.close])); | |
| 43 | +const spread = m1.data.filter((b) => byDate.has(b.datetime)).map((b) => ({ date: b.datetime, spread: b.close - byDate.get(b.datetime) })); | |
| 44 | +console.log(spread.at(-1), "days in contango:", spread.filter((s) => s.spread < 0).length / spread.length);`, | |
| 45 | + r: `cont <- function(depth) { | |
| 46 | + b <- request("https://www.hfmarketdata.io/v1/futures/CL/continuous") |> | |
| 47 | + req_url_query(roll = "open_interest", adjust = "none", depth = depth, interval = "1d", from = "2020-01-01", limit = 50000) |> | |
| 48 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 49 | + setNames(b$data$close, b$data$datetime) | |
| 50 | +} | |
| 51 | +m1 <- cont(1); m2 <- cont(2); common <- intersect(names(m1), names(m2)) | |
| 52 | +spread <- m1[common] - m2[common] | |
| 53 | +mean(spread < 0) # share of days in contango`, | |
| 54 | + }, | |
| 55 | +] | |
| 56 | + | |
| 57 | +# Recipe: analyse crude oil term structure | |
| 58 | + | |
| 59 | +The *term structure* of a futures product is the set of prices of all its listed contracts on one day, plotted against expiry. For WTI crude (`CL`) it is the single most informative picture of the physical market: a downward-sloping curve (**backwardation**) says barrels are wanted *now*; an upward-sloping one (**contango**) says storage is full and someone must be paid to hold oil. This recipe extracts the curve, tracks its slope through time, and computes what that slope costs or earns a position. | |
| 60 | + | |
| 61 | +## 1. The curve on one day | |
| 62 | + | |
| 63 | +<Endpoint method="GET" path="/v1/futures/{root}/term-structure" /> with `as_of=2024-06-03` returns, for every `CL` contract listed that day, its symbol, expiration date, close, volume and open interest, sorted by expiry. Twelve monthly expiries cover the first year; the curve then thins out to quarterly and December contracts several years ahead. The first snippet prints it and labels the front spread. | |
| 64 | + | |
| 65 | +Two things to check in the output before trusting a curve: | |
| 66 | + | |
| 67 | +- **Open interest**: far-dated contracts with a few hundred lots have stale, wide prices. Filter `open_interest > 1000` (or so) for anything quantitative beyond 18 months. | |
| 68 | +- **The front contract in delivery**: in the last days before expiry the front month decouples from the rest of the curve (delivery logistics, position squaring). If `as_of` is within a week of the front expiry, treat the *second* contract as the economically meaningful front. | |
| 69 | + | |
| 70 | +## 2. The slope through time | |
| 71 | + | |
| 72 | +A curve per day is a lot of data; what most analyses need is the **front spread** — first minus second contract — as a daily series. Instead of calling `term-structure` for every day, ask <Endpoint method="GET" path="/v1/futures/{root}/continuous" /> twice with `adjust=none`: `depth=1` follows the front contract, `depth=2` the second, under the same roll rule. Subtract, and you have the spread since 2020 in two requests (second snippet). Divide by the second month for a percentage that is comparable across price regimes. | |
| 73 | + | |
| 74 | +We roll on `open_interest` here rather than `volume`: for crude, open interest migrates to the next contract several days before volume does, and it is the OI-weighted contract that defines “the front” for most index products (which is who pays the roll). | |
| 75 | + | |
| 76 | +<Callout type="info" title="Why adjust=none"> | |
| 77 | +Spreads are differences between two *real* prices on the same day, so adjustment must be off — a back-adjusted depth-1 series minus an unadjusted depth-2 series is meaningless. Both legs must share the roll rule, otherwise the pair switches contract on different days and the spread jumps. | |
| 78 | +</Callout> | |
| 79 | + | |
| 80 | +## 3. From slope to roll yield | |
| 81 | + | |
| 82 | +Holding a long position in the front contract and rolling it every month earns (or pays) the spread each roll: in backwardation you sell the expensive front and buy the cheaper second — a gain; in contango the opposite. Annualised, that **roll yield** is roughly | |
| 83 | + | |
| 84 | +``` | |
| 85 | +roll_yield ≈ (front − second) / second × 12 | |
| 86 | +``` | |
| 87 | + | |
| 88 | +for monthly rolls. Over 2020–2025 crude alternated between deep contango (spring 2020, when the front spread was famously negative by tens of dollars in April) and sustained backwardation (2021–2022). The `days in contango` share printed by the script summarises the regime; resampling `spread_pct` by quarter shows the transitions. | |
| 89 | + | |
| 90 | +## 4. Beyond the front | |
| 91 | + | |
| 92 | +- **Curve shape indicators**: 1st–6th and 1st–12th month spreads from `term-structure`, or from `continuous` with `depth=3` and the `contracts` list for farther months. | |
| 93 | +- **Curve on many days**: loop `term-structure` over the roll dates from `meta.roll_dates` (a dozen calls per year) — the curve changes shape mostly around rolls and macro events. | |
| 94 | +- **Cross-product**: repeat for Brent (`BRN` where available), `RB` and `HO` to study the crack spread, or `NG` where seasonality dominates the curve. | |
| 95 | + | |
| 96 | +## Reading the numbers responsibly | |
| 97 | + | |
| 98 | +Prices in this dataset are end-of-day settlements/closes; intra-day the curve moves, and the front spread in the last hour before expiry can be wild. Volume and open interest are as reported by the exchange with a one-day lag for OI. Nothing here is interpolated: a contract with no trade that day has no row, and `term-structure` tells you so instead of inventing a point on the curve. | |
added
hfmarketdata/web/content/guides/recipe-custom-continuous.mdx
+112 −0
@@ -0,0 +1,112 @@ | ||
| 1 | +export const meta = { title: 'Recipe: build a custom continuous contract', description: 'Stitch individual futures contracts with your own roll rule and adjustment in pandas, and check it against the API\'s continuous endpoint.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Stitch CL contracts, roll 5 business days before expiry', | |
| 6 | + python: `import requests, pandas as pd, numpy as np | |
| 7 | + | |
| 8 | +BASE = "https://www.hfmarketdata.io" | |
| 9 | +ROOT, DAYS_BEFORE = "CL", 5 | |
| 10 | + | |
| 11 | +# 1. Which contracts exist, and when do they expire? | |
| 12 | +contracts = pd.DataFrame(requests.get(f"{BASE}/v1/futures/{ROOT}/contracts").json()["data"]) | |
| 13 | +contracts["expiration_date"] = pd.to_datetime(contracts["expiration_date"]) | |
| 14 | +contracts = contracts[contracts["expiration_date"] >= "2023-01-01"].sort_values("expiration_date") | |
| 15 | + | |
| 16 | +# 2. Daily bars of each contract (one request per contract; cache them!) | |
| 17 | +def bars(symbol): | |
| 18 | + r = requests.get(f"{BASE}/v1/futures/contract/{symbol}/bars", params={"interval": "1d", "from": "2022-06-01"}) | |
| 19 | + r.raise_for_status() | |
| 20 | + df = pd.DataFrame(r.json()["data"]) | |
| 21 | + df["datetime"] = pd.to_datetime(df["datetime"], utc=True).dt.tz_localize(None).dt.normalize() | |
| 22 | + return df.set_index("datetime")["close"].rename(symbol) | |
| 23 | + | |
| 24 | +closes = pd.concat([bars(s) for s in contracts["symbol"]], axis=1).sort_index() | |
| 25 | + | |
| 26 | +# 3. Roll schedule: each contract is "active" until DAYS_BEFORE business days before its expiry | |
| 27 | +roll_days = contracts.set_index("symbol")["expiration_date"] - pd.offsets.BDay(DAYS_BEFORE) | |
| 28 | +active = pd.Series(index=closes.index, dtype=object) | |
| 29 | +prev_end = pd.Timestamp.min | |
| 30 | +for sym, end in roll_days.items(): | |
| 31 | + active[(closes.index > prev_end) & (closes.index <= end)] = sym | |
| 32 | + prev_end = end | |
| 33 | +active = active.ffill().bfill() | |
| 34 | + | |
| 35 | +# 4. Unadjusted stitched series + roll gaps → back-adjusted and ratio-adjusted versions | |
| 36 | +unadj = pd.Series([closes.at[d, s] for d, s in active.items()], index=closes.index, name="unadj") | |
| 37 | +switch = active != active.shift(1) | |
| 38 | +gap = pd.Series(0.0, index=closes.index); ratio = pd.Series(1.0, index=closes.index) | |
| 39 | +for d in closes.index[switch][1:]: | |
| 40 | + old, new = active.shift(1)[d], active[d] | |
| 41 | + gap[d] = closes.at[d, new] - closes.at[d, old] # new minus old, on the roll day | |
| 42 | + ratio[d] = closes.at[d, new] / closes.at[d, old] | |
| 43 | +back_adj = unadj + gap[::-1].cumsum()[::-1].shift(-1).fillna(0) # add all future gaps to past bars | |
| 44 | +ratio_adj = unadj * ratio[::-1].cumprod()[::-1].shift(-1).fillna(1) | |
| 45 | + | |
| 46 | +out = pd.DataFrame({"contract": active, "unadj": unadj, "back_adj": back_adj, "ratio_adj": ratio_adj}) | |
| 47 | +print(out[switch | switch.shift(-1, fill_value=False)].head(12)) # the rows around each roll | |
| 48 | + | |
| 49 | +# 5. Compare with the API's own construction | |
| 50 | +api = requests.get(f"{BASE}/v1/futures/{ROOT}/continuous", params={ | |
| 51 | + "roll": "calendar", "adjust": "back_adjusted", "interval": "1d", "from": str(out.index[0].date())}).json() | |
| 52 | +print(api["meta"]["roll_dates"][:3])`, | |
| 53 | + curl: `# The building blocks (the stitching itself is a pandas job — Python tab) | |
| 54 | +curl "https://www.hfmarketdata.io/v1/futures/CL/contracts" | |
| 55 | +curl "https://www.hfmarketdata.io/v1/futures/contract/CLN24/bars?interval=1d&from=2024-01-01" | |
| 56 | +curl "https://www.hfmarketdata.io/v1/futures/contract/CLQ24/bars?interval=1d&from=2024-01-01"`, | |
| 57 | + javascript: `const BASE = "https://www.hfmarketdata.io"; | |
| 58 | +const { data: contracts } = await (await fetch(\`\${BASE}/v1/futures/CL/contracts\`)).json(); | |
| 59 | +const recent = contracts.filter((c) => c.expiration_date >= "2024-01-01").slice(0, 3); | |
| 60 | +for (const c of recent) { | |
| 61 | + const { data } = await (await fetch(\`\${BASE}/v1/futures/contract/\${c.symbol}/bars?interval=1d&from=2024-01-01\`)).json(); | |
| 62 | + console.log(c.symbol, c.expiration_date, data.length, "bars"); | |
| 63 | +}`, | |
| 64 | + r: `library(httr2) | |
| 65 | +BASE <- "https://www.hfmarketdata.io" | |
| 66 | +contracts <- request(paste0(BASE, "/v1/futures/CL/contracts")) |> req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 67 | +recent <- head(subset(contracts$data, expiration_date >= "2024-01-01"), 3) | |
| 68 | +for (s in recent$symbol) { | |
| 69 | + b <- request(paste0(BASE, "/v1/futures/contract/", s, "/bars")) |> req_url_query(interval = "1d", from = "2024-01-01") |> | |
| 70 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 71 | + cat(s, nrow(b$data), "bars\\n") | |
| 72 | +}`, | |
| 73 | + }, | |
| 74 | +] | |
| 75 | + | |
| 76 | +# Recipe: build a custom continuous contract | |
| 77 | + | |
| 78 | +The API's `continuous` endpoint offers four roll rules and three adjustments. Sometimes you need a fifth rule — “roll when the front month's open interest drops below 60 % of the next”, “roll on the Monday after the first Thursday”, a rule that matches a broker's statement — or you want to *see* the construction to trust it. This recipe builds a continuous series from individual contracts in pandas, then checks it against the API's. | |
| 79 | + | |
| 80 | +## The ingredients | |
| 81 | + | |
| 82 | +1. **The contract list** — <Endpoint method="GET" path="/v1/futures/{root}/contracts" /> gives every `CL` contract with `expiration_date`, `first_notice_date`, `first_data_date`, `last_data_date` and `status`. Reference data drives the roll schedule, so we never guess an expiry from a file name. | |
| 83 | +2. **Each contract's bars** — <Endpoint method="GET" path="/v1/futures/contract/{symbol}/bars" /> for every symbol in the window. One request per contract; crude has twelve expiries a year, so a two-year window is ~30 calls — cache them on disk, they never change. | |
| 84 | + | |
| 85 | +## The construction | |
| 86 | + | |
| 87 | +**Roll schedule.** Sort contracts by expiry. Contract *k* is active from the day after contract *k−1*'s roll day up to its own roll day; here the roll day is *expiry − 5 business days* (`pd.offsets.BDay(5)`). Any other rule is a change to that one line — using `first_notice_date` instead of `expiration_date` is what a physically-delivered product like `CL` really needs if you hold into the delivery month. | |
| 88 | + | |
| 89 | +**Unadjusted series.** For each date, take the close of the active contract. This is `adjust=none`: real prices, with a jump on every roll day. | |
| 90 | + | |
| 91 | +**Gaps.** On each roll day compute *new − old* (difference) and *new / old* (ratio) using both contracts' closes **on that same day** — both exist because contracts overlap. Never compute a gap across two different days. | |
| 92 | + | |
| 93 | +**Back-adjusted.** Add to every bar the sum of all *future* gaps. The reverse cumulative sum in the script does exactly that: today's bars are untouched, the oldest bars carry every gap. **Ratio-adjusted** is the same with a reverse cumulative product. | |
| 94 | + | |
| 95 | +``` | |
| 96 | +back_adj[t] = unadj[t] + Σ gap[r] for all roll days r > t | |
| 97 | +ratio_adj[t] = unadj[t] × Π ratio[r] for all roll days r > t | |
| 98 | +``` | |
| 99 | + | |
| 100 | +## Checking against the API | |
| 101 | + | |
| 102 | +Request `roll=calendar&adjust=back_adjusted` from the same start date and compare `meta.roll_dates` with your `switch` days, then the two series. They will agree exactly where the rules coincide, and the days where they do not are *precisely* the information you wanted: the effect of your rule versus the standard one. | |
| 103 | + | |
| 104 | +<Callout type="tip" title="A cheap sanity check"> | |
| 105 | +Both your series and the API's must have zero point change on a roll day beyond the market move (`back_adj.diff()` on a roll day equals `closes[new].diff()` on that day). If you see the contract gap in the diff, your adjustment is off by one day. | |
| 106 | +</Callout> | |
| 107 | + | |
| 108 | +## Notes | |
| 109 | + | |
| 110 | +- **Negative values** are legitimate in back-adjusted series of high-carry products. Crude's back-adjusted history crosses zero far enough back; use the ratio series if you need logs. | |
| 111 | +- **Missing overlap.** Rarely, a contract has no bar on the intended roll day (a holiday, a data gap reported by `/coverage`). Move the roll to the previous common day rather than interpolating — the API's `continuous` endpoint does the same and says so in `meta.roll_dates`. | |
| 112 | +- **Depth.** For the *second* contract along the curve, shift the schedule by one contract: the active symbol at each date becomes the next one in the sorted list. That is what `depth=2` does. | |
added
hfmarketdata/web/content/guides/recipe-pandas-backtest.mdx
+106 −0
@@ -0,0 +1,106 @@ | ||
| 1 | +export const meta = { title: 'Recipe: backtest a strategy in pandas', description: 'A complete moving-average crossover backtest on ES continuous futures — data pull, signals, P&L in points and dollars, roll handling — in 60 lines of pandas.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'Full script', | |
| 6 | + python: `import requests, pandas as pd, numpy as np | |
| 7 | + | |
| 8 | +BASE = "https://www.hfmarketdata.io" | |
| 9 | +POINT_VALUE = 50 # ES: $50 per index point | |
| 10 | + | |
| 11 | +# 1. Data — back-adjusted so point differences are tradeable P&L | |
| 12 | +r = requests.get(f"{BASE}/v1/futures/ES/continuous", params={ | |
| 13 | + "roll": "volume", "adjust": "back_adjusted", "depth": 1, | |
| 14 | + "interval": "1d", "from": "2015-01-01", "to": "2025-12-31", "limit": 50000, | |
| 15 | +}, timeout=120) | |
| 16 | +r.raise_for_status() | |
| 17 | +body = r.json() | |
| 18 | +px = pd.DataFrame(body["data"]) | |
| 19 | +px["datetime"] = pd.to_datetime(px["datetime"], utc=True) | |
| 20 | +px = px.set_index("datetime").sort_index() | |
| 21 | +rolls = pd.to_datetime([d["date"] for d in body["meta"]["roll_dates"]], utc=True) | |
| 22 | + | |
| 23 | +# 2. Signal — 20/100-day moving-average crossover, long/short, acted on the next bar | |
| 24 | +fast, slow = px["close"].rolling(20).mean(), px["close"].rolling(100).mean() | |
| 25 | +position = np.sign(fast - slow).shift(1).fillna(0) # +1 long, -1 short, entered next day | |
| 26 | + | |
| 27 | +# 3. P&L in points then dollars; costs = 1 tick (0.25) per side per trade + 1 tick per roll | |
| 28 | +pts = position * px["close"].diff() | |
| 29 | +trades = position.diff().abs().fillna(0) # 2 when flipping, 1 when entering | |
| 30 | +cost_pts = trades * 0.25 + px.index.isin(rolls).astype(int) * position.abs() * 0.25 | |
| 31 | +pnl = (pts - cost_pts) * POINT_VALUE | |
| 32 | + | |
| 33 | +# 4. Report | |
| 34 | +equity = pnl.cumsum() | |
| 35 | +ann = 252 | |
| 36 | +sharpe = pnl.mean() / pnl.std() * np.sqrt(ann) | |
| 37 | +dd = (equity - equity.cummax()).min() | |
| 38 | +print(f"trades: {int(trades.sum())} rolls crossed: {px.index.isin(rolls).sum()}") | |
| 39 | +print(f"total P&L: $ {equity.iloc[-1]:,.0f} Sharpe: {sharpe:.2f} max drawdown: $ {dd:,.0f}") | |
| 40 | +print(equity.resample("YE").last().diff().rename("P&L per year"))`, | |
| 41 | + curl: `# Pull the data once and keep it; the analysis is pandas (see the Python tab) | |
| 42 | +curl "https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=volume&adjust=back_adjusted&interval=1d&from=2015-01-01&to=2025-12-31&format=csv" -o es_backadj.csv`, | |
| 43 | + javascript: `// Data pull in JS; run the analysis in a notebook or with danfo.js / arquero | |
| 44 | +const url = "https://www.hfmarketdata.io/v1/futures/ES/continuous?roll=volume&adjust=back_adjusted&interval=1d&from=2015-01-01&to=2025-12-31&limit=50000"; | |
| 45 | +const { data, meta } = await (await fetch(url)).json(); | |
| 46 | +console.log(data.length, "bars,", meta.roll_dates.length, "rolls");`, | |
| 47 | + r: `library(httr2); library(TTR) | |
| 48 | +body <- request("https://www.hfmarketdata.io/v1/futures/ES/continuous") |> | |
| 49 | + req_url_query(roll = "volume", adjust = "back_adjusted", interval = "1d", from = "2015-01-01", to = "2025-12-31", limit = 50000) |> | |
| 50 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 51 | +px <- body$data; px$close <- as.numeric(px$close) | |
| 52 | +fast <- SMA(px$close, 20); slow <- SMA(px$close, 100) | |
| 53 | +pos <- c(0, head(sign(fast - slow), -1)); pos[is.na(pos)] <- 0 | |
| 54 | +pnl <- pos * c(NA, diff(px$close)) * 50 | |
| 55 | +cat("total P&L $", sum(pnl, na.rm = TRUE), " Sharpe", mean(pnl, na.rm = TRUE) / sd(pnl, na.rm = TRUE) * sqrt(252), "\\n")`, | |
| 56 | + }, | |
| 57 | +] | |
| 58 | + | |
| 59 | +# Recipe: backtest a strategy in pandas | |
| 60 | + | |
| 61 | +A backtest is only as good as the data assumptions underneath it. This recipe walks through a deliberately simple strategy — a 20/100-day moving-average crossover on the E-mini S&P 500 — so that every data decision is visible. The full script is on the right; read the reasoning here. | |
| 62 | + | |
| 63 | +## 1. Choose the right series | |
| 64 | + | |
| 65 | +We trade a *futures product* over ten years, so we need a continuous series, and we need to decide two things the [futures guide](/docs/futures-contracts) explains in depth: | |
| 66 | + | |
| 67 | +- **`roll=volume`** — switch contracts when the next one out-trades the current one. That is when a real trader would have rolled, and it keeps us in the liquid contract. | |
| 68 | +- **`adjust=back_adjusted`** — historical prices are shifted by the gap at each roll, so **a difference in points between two dates is exactly the P&L a position would have earned** (a position is rolled at no gain/loss in a back-adjusted series). With `adjust=none` the roll gaps would show up as fake profits or losses; with `ratio_adjusted` percentages would be right but point P&L would not. | |
| 69 | + | |
| 70 | +One call returns the whole decade in daily bars, and `meta.roll_dates` gives us the roll days — we will charge a tick on each because rolling a position costs a spread. | |
| 71 | + | |
| 72 | +<Callout type="warning" title="Back-adjusted levels are not prices"> | |
| 73 | +A back-adjusted close of 1 850 in 2015 does not mean ES traded at 1 850; it means “the current contract, had it existed, would have been there”. Never use back-adjusted levels for anything that depends on the *level* (option strikes, margin, percentage returns). Use them for point differences — exactly what a futures P&L is. | |
| 74 | +</Callout> | |
| 75 | + | |
| 76 | +## 2. Signals without look-ahead | |
| 77 | + | |
| 78 | +```python | |
| 79 | +fast, slow = px["close"].rolling(20).mean(), px["close"].rolling(100).mean() | |
| 80 | +position = np.sign(fast - slow).shift(1).fillna(0) | |
| 81 | +``` | |
| 82 | + | |
| 83 | +The `.shift(1)` is the whole game: the signal computed from today's close is acted upon **from tomorrow's bar**. Forgetting it is the most common backtest bug and turns most strategies into miracles. `np.sign` gives us +1 / −1 (always in the market, long or short); the first 100 days are `NaN` → flat. | |
| 84 | + | |
| 85 | +## 3. P&L, in points then in dollars | |
| 86 | + | |
| 87 | +```python | |
| 88 | +pts = position * px["close"].diff() | |
| 89 | +pnl = (pts - cost_pts) * POINT_VALUE # ES = $50 per point | |
| 90 | +``` | |
| 91 | + | |
| 92 | +Daily P&L is position × the day's point change. Because the series is back-adjusted, the point change *across a roll day* is a true change of value, not a contract gap. Costs are deliberately conservative: one tick (0.25 point, $12.50) per side per trade and one tick per roll while in a position. Halving or doubling them is a one-line sensitivity test worth running. | |
| 93 | + | |
| 94 | +## 4. Reading the result | |
| 95 | + | |
| 96 | +The script prints total P&L, an annualised Sharpe from daily P&L, the maximum drawdown and P&L per year. Whatever the numbers say, ask the data questions before the strategy questions: | |
| 97 | + | |
| 98 | +- **How many rolls did we cross?** (`meta.roll_dates`) — ~40 over ten years for ES. If the roll rule changes the outcome a lot, the strategy is trading the roll, not the market. | |
| 99 | +- **Does the result survive `roll=open_interest`?** Change one parameter, re-run. Robust ideas do not care. | |
| 100 | +- **Are you accidentally using extended hours?** Daily bars are session bars, so no; if you move to intraday, read [time zones & sessions](/docs/time-zones) and consider `session=rth`. | |
| 101 | + | |
| 102 | +## Variations | |
| 103 | + | |
| 104 | +- **Intraday**: `interval=1h`, `session=rth`, and compute P&L per bar; pull one year per request to stay under 50 000 rows (a free key), or use `format=parquet` for half the quota. | |
| 105 | +- **A portfolio**: repeat for `NQ`, `CL`, `GC`, `ZN`… — `GET /v1/futures/roots` gives you point values (`tick_value / tick_size`) so the dollar conversion is data, not a constant. | |
| 106 | +- **Individual contracts instead**: hold `ESZ25`, exit five days before expiry, re-enter the next contract — the [custom continuous recipe](/docs/recipes/custom-continuous) builds exactly that series and lets you compare it to the API's. | |
added
hfmarketdata/web/content/guides/time-zones.mdx
+105 −0
@@ -0,0 +1,105 @@ | ||
| 1 | +export const meta = { title: 'Time zones & sessions', description: 'What every timestamp means — US/Eastern naive stamps on the v1 bars endpoints, UTC ISO 8601 on v2 — plus regular vs extended trading hours and the session parameter.' } | |
| 2 | + | |
| 3 | +export const snippets = [ | |
| 4 | + { | |
| 5 | + title: 'v1 bars: localise US/Eastern, convert to UTC', | |
| 6 | + curl: `# v1 intraday stamps have no offset: "2024-06-03 09:30:00" is 09:30 New York time | |
| 7 | +curl "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min&start=2024-06-03%2009:30:00&limit=2"`, | |
| 8 | + python: `import requests, pandas as pd | |
| 9 | + | |
| 10 | +r = requests.get("https://www.hfmarketdata.io/v1/bars/stock/AAPL", | |
| 11 | + params={"timeframe": "1min", "start": "2024-06-03 09:30:00", "limit": 390}) | |
| 12 | +df = pd.DataFrame(r.json()["data"]) | |
| 13 | +df["datetime"] = pd.to_datetime(df["datetime"]).dt.tz_localize("America/New_York") # attach the exchange zone | |
| 14 | +df["utc"] = df["datetime"].dt.tz_convert("UTC") | |
| 15 | +print(df[["datetime", "utc", "close"]].head(2)) | |
| 16 | +# 2024-06-03 09:30:00-04:00 → 2024-06-03 13:30:00+00:00 (EDT, summer)`, | |
| 17 | + javascript: `// Browsers/Node have no tz database API for arbitrary zones without Intl; use Temporal or luxon. | |
| 18 | +import { DateTime } from "luxon"; | |
| 19 | +const { data } = await (await fetch("https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min&start=2024-06-03%2009:30:00&limit=2")).json(); | |
| 20 | +for (const bar of data) { | |
| 21 | + const ny = DateTime.fromFormat(bar.datetime, "yyyy-MM-dd HH:mm:ss", { zone: "America/New_York" }); | |
| 22 | + console.log(bar.datetime, "→", ny.toUTC().toISO()); | |
| 23 | +}`, | |
| 24 | + r: `library(httr2) | |
| 25 | +bars <- request("https://www.hfmarketdata.io/v1/bars/stock/AAPL") |> | |
| 26 | + req_url_query(timeframe = "1min", start = "2024-06-03 09:30:00", limit = 2) |> | |
| 27 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 28 | +ny <- as.POSIXct(bars$data$datetime, tz = "America/New_York") | |
| 29 | +utc <- format(ny, tz = "UTC", usetz = TRUE) | |
| 30 | +data.frame(eastern = bars$data$datetime, utc)`, | |
| 31 | + }, | |
| 32 | + { | |
| 33 | + title: 'v2: UTC in, UTC out, choose the session', | |
| 34 | + curl: `curl "https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars?interval=1h&session=rth&from=2025-06-02T13:30:00Z&to=2025-06-02T20:00:00Z"`, | |
| 35 | + python: `r = requests.get("https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars", | |
| 36 | + params={"interval": "1h", "session": "rth", "from": "2025-06-02T13:30:00Z", "to": "2025-06-02T20:00:00Z"}) | |
| 37 | +df = pd.DataFrame(r.json()["data"]) | |
| 38 | +df["datetime"] = pd.to_datetime(df["datetime"], utc=True) # already UTC, ISO 8601 with Z | |
| 39 | +print(df["datetime"].dt.tz_convert("America/New_York").head()) # view in exchange time if you prefer`, | |
| 40 | + javascript: `const url = "https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars?interval=1h&session=rth&from=2025-06-02T13:30:00Z&to=2025-06-02T20:00:00Z"; | |
| 41 | +const { data } = await (await fetch(url)).json(); | |
| 42 | +console.log(new Date(data[0].datetime).toISOString()); // parses directly: "…Z"`, | |
| 43 | + r: `bars <- request("https://www.hfmarketdata.io/v1/futures/contract/ESZ25/bars") |> | |
| 44 | + req_url_query(interval = "1h", session = "rth", from = "2025-06-02T13:30:00Z", to = "2025-06-02T20:00:00Z") |> | |
| 45 | + req_perform() |> resp_body_json(simplifyVector = TRUE) | |
| 46 | +as.POSIXct(bars$data$datetime, format = "%Y-%m-%dT%H:%M:%SZ", tz = "UTC")`, | |
| 47 | + }, | |
| 48 | +] | |
| 49 | + | |
| 50 | +# Time zones & sessions | |
| 51 | + | |
| 52 | +Half of all data bugs are time bugs. This page states, unambiguously, what every timestamp on this API means. | |
| 53 | + | |
| 54 | +## The two conventions | |
| 55 | + | |
| 56 | +| Endpoints | Intraday `datetime` | Daily `datetime` | Filters (`start`/`end`, `from`/`to`, `at`, `as_of`) | | |
| 57 | +| --- | --- | --- | --- | | |
| 58 | +| **v1 bars & snapshot** — `/v1/bars/*`, `/v1/snapshot/*` | `"2024-06-03 09:30:00"` — **US/Eastern**, no offset, as published by the exchange (EST or EDT depending on the date) | `"2024-06-03"` — the trading date | interpreted in the same US/Eastern convention | | |
| 59 | +| **v1 options** — `/v1/options/*` | — (end-of-day only) | `trade_date`, `expiry` — dates | dates | | |
| 60 | +| **v2** — `/v1/futures/*`, `/v1/fundamentals/*`, `/v1/bulk/*`, `/v1/stream` | `"2025-06-02T13:30:00Z"` — **UTC, ISO 8601** with `Z` | `"2025-06-02"` — the trading date | ISO 8601; a bare time is UTC, an explicit offset (`-04:00`) is honoured | | |
| 61 | + | |
| 62 | +The v1 convention predates this platform's v2 release and is kept exactly as it was so that existing code does not break — that is the [no-breaking-change rule](/docs/versioning). New code should prefer the v2 endpoints where they exist (futures) and should **always attach `America/New_York` before doing arithmetic** on v1 intraday stamps, as the first snippet shows. The daylight-saving switch happens on the second Sunday of March and the first Sunday of November; a naive stamp is 4 hours behind UTC in summer and 5 in winter. | |
| 63 | + | |
| 64 | +<Callout type="warning" title="Do not treat v1 intraday stamps as UTC"> | |
| 65 | +`pd.to_datetime(df.datetime, utc=True)` on a v1 response silently shifts every bar by 4 or 5 hours. Use `tz_localize("America/New_York")` first, then `tz_convert("UTC")`. | |
| 66 | +</Callout> | |
| 67 | + | |
| 68 | +## What a bar's timestamp denotes | |
| 69 | + | |
| 70 | +A bar is labelled by its **opening time**. The `1min` bar stamped `09:30:00` covers trades from 09:30:00.000 to 09:30:59.999; a `5min` bar stamped `09:30:00` covers 09:30–09:34. Check the first stamps of a session to see how the `30min` and `1hour` bars are aligned for a given market before assuming wall-clock boundaries. Daily bars carry the trading date and aggregate the **regular session** for equities (see below). | |
| 71 | + | |
| 72 | +There is no row for a minute without a trade. A dense grid is your responsibility (`df.asfreq("1min")` or `reindex` on a session calendar). | |
| 73 | + | |
| 74 | +## Sessions: RTH vs ETH | |
| 75 | + | |
| 76 | +| Market | Regular trading hours (RTH) | Extended (ETH) | | |
| 77 | +| --- | --- | --- | | |
| 78 | +| US equities & ETFs | 09:30–16:00 ET | pre-market 04:00–09:30, after-hours 16:00–20:00 ET | | |
| 79 | +| CME equity index futures (`ES`, `NQ`…) | 09:30–16:15 ET (the “pit” hours) | nearly 23 h/day, Sunday 18:00 → Friday 17:00 ET with a daily 17:00–18:00 break | | |
| 80 | +| Energy, metals, rates | exchange-specific, roughly 08:00/09:00–14:30/17:00 ET | near-24 h | | |
| 81 | +| Crypto | — | 24 × 7 | | |
| 82 | +| FX | — | Sunday 17:00 → Friday 17:00 ET | | |
| 83 | + | |
| 84 | +**v1 equity intraday data includes extended hours.** The AAPL 1-minute dataset starts at 04:00 and ends at 20:00 ET on a normal day — the week benchmark in the [data formats guide](/docs/data-formats) returned 3 621 one-minute bars for five sessions, i.e. ~724 per day, well above the 390 regular minutes. Filter on the time of day if you only want RTH: | |
| 85 | + | |
| 86 | +```python | |
| 87 | +ny = df["datetime"].dt.tz_localize("America/New_York") | |
| 88 | +rth = df[(ny.dt.time >= pd.Timestamp("09:30").time()) & (ny.dt.time < pd.Timestamp("16:00").time())] | |
| 89 | +``` | |
| 90 | + | |
| 91 | +**Daily equity bars** (`timeframe=1day`) reflect the regular session: the open is the 09:30 auction print, the close the 16:00 closing auction, and volume the consolidated regular-session volume. | |
| 92 | + | |
| 93 | +**v2 futures endpoints take `session=rth|eth|all`** (default `all`). `rth` keeps the exchange's regular hours for that product, `eth` the rest; the API applies the product's own session definition (from the roots reference), so you do not need to know that ES pit hours end at 16:15 while CL's end at 14:30. | |
| 94 | + | |
| 95 | +## Trading date vs calendar date | |
| 96 | + | |
| 97 | +Futures trade across midnight: the ES session that “belongs” to Monday June 2 starts Sunday June 1 at 18:00 ET. The daily bar for a futures contract is stamped with the **trading date** (June 2) and aggregates the full session that the exchange assigns to that date, including the Sunday evening hours. Intraday v2 bars keep their true UTC time (`2025-06-01T22:00:00Z` for that Sunday 18:00 ET open), so if you group intraday bars by `datetime.date()` you will not reproduce the daily bars — group by trading date instead (the API's `1d` bars already do). | |
| 98 | + | |
| 99 | +## Holidays and half days | |
| 100 | + | |
| 101 | +Exchange holidays have no rows. Early closes (day after Thanksgiving, Christmas Eve: 13:00 ET for equities) have fewer intraday bars and a daily bar built from the shorter session. Nothing is padded. | |
| 102 | + | |
| 103 | +## Fundamentals dates | |
| 104 | + | |
| 105 | +The fundamentals endpoints carry several dates per fact and none of them is a “bar time”: the **period end** (fiscal period, e.g. `2024-03-30` for Apple's Q2 FY2024), the **filing date** (when the SEC accepted the document, e.g. `2024-05-03`) and the acceptance **timestamp** (UTC). Point-in-time queries key on the filing date, never on the period end — see [Point-in-time & look-ahead bias](/docs/fundamentals/point-in-time). The exact field names are in the [reference](/docs/reference/tag/fundamentals). | |
added
hfmarketdata/web/content/guides/versioning.mdx
+52 −0
@@ -0,0 +1,52 @@ | ||
| 1 | +export const meta = { title: 'Versioning & deprecation policy', description: 'What can change in the HF Market Data API, what never will, and how you hear about it.' } | |
| 2 | + | |
| 3 | +# Versioning & deprecation policy | |
| 4 | + | |
| 5 | +The API is versioned in the path — everything lives under `/v1` — and **`/v1` is stable**. This page is the contract. | |
| 6 | + | |
| 7 | +## What we promise not to change | |
| 8 | + | |
| 9 | +For every endpoint that exists under `/v1`: | |
| 10 | + | |
| 11 | +- **Paths and methods** stay. A URL that works today works next year. | |
| 12 | +- **Parameters** keep their names, types, defaults and accepted values. New optional parameters may be added; none is removed or made required. | |
| 13 | +- **Response shapes** keep every existing field with the same type and meaning. The v1 bars & options envelope is `{"count", "data"}`; the v2 envelope is `{"data", "meta"}`. New fields may appear — inside `meta`, or as new columns at the end of a row — so parse by name, never by position. | |
| 14 | +- **Status codes** for existing conditions do not change (`404` stays `404`). | |
| 15 | +- **Time conventions** do not change per endpoint: v1 intraday stamps stay US/Eastern naive, v2 stays UTC ISO 8601. See [time zones](/docs/time-zones). | |
| 16 | +- **Error envelope** — `error.code` values are append-only. A code, once published on the [errors page](/docs/errors), keeps its meaning. | |
| 17 | + | |
| 18 | +Concretely: the additions of September 2026 (futures contracts, fundamentals, accounts, rate limits) did not alter a single byte of the pre-existing responses except for *added* headers (`X-RateLimit-*`, `X-Row-Count`) and an *added* `error` object next to the legacy `detail` field on errors. | |
| 19 | + | |
| 20 | +## What can change | |
| 21 | + | |
| 22 | +- **Data** — history is corrected when the source corrects it (late prints, restated filings, contract reference data). Adjusted series (split/dividend for equities, back/ratio-adjusted for futures) shift by construction each time a new event happens; that is expected and documented, not a breaking change. | |
| 23 | +- **Coverage** grows: new tickers, new quarters, new roots, new years of fundamentals. | |
| 24 | +- **Limits** are published in `info.x-tiers` and on the [limits page](/limits); they can be raised at any time. Lowering them would be announced two weeks ahead. | |
| 25 | +- **OpenAPI descriptions, examples and this documentation** improve continuously; they never redefine behaviour. | |
| 26 | +- **Performance characteristics** (latency, compression) may improve. | |
| 27 | + | |
| 28 | +## Deprecation | |
| 29 | + | |
| 30 | +If an endpoint or parameter must go — this has not happened yet — the process is: | |
| 31 | + | |
| 32 | +1. **Announcement** in the [changelog](/docs/changelog) with a removal date **at least 6 months** out, and a replacement. | |
| 33 | +2. **Headers** on every response of the deprecated element: `Deprecation: true`, `Sunset: <HTTP date>`, and a `Link: <docs-url>; rel="deprecation"` pointing to the migration note. | |
| 34 | +3. **Spec** — the operation or parameter is marked `deprecated: true` in `/openapi.json`; the reference page shows a badge. | |
| 35 | +4. **Removal** on the announced date, never earlier. A removed endpoint returns `410 Gone` with the migration link for a further 6 months. | |
| 36 | + | |
| 37 | +A `/v2` path prefix would only be introduced for a change that cannot be made additively. There is no such plan. | |
| 38 | + | |
| 39 | +## How to stay informed | |
| 40 | + | |
| 41 | +- The [changelog](/docs/changelog) — every change, dated, additive unless flagged. | |
| 42 | +- `info.version` in [`/openapi.json`](/openapi.json) follows semantic versioning of the *specification*: major = breaking (would come with a new path prefix), minor = additive, patch = documentation/fixes. | |
| 43 | +- The [status page](/status) reports incidents and data refresh times. | |
| 44 | +- Questions or concerns: [contact@spboucher.ai](mailto:contact@spboucher.ai). | |
| 45 | + | |
| 46 | +## Writing forward-compatible clients | |
| 47 | + | |
| 48 | +- Access fields **by name**; ignore unknown ones. | |
| 49 | +- Treat `meta` as an open object and `null` as “not available, see `meta.coverage`”. | |
| 50 | +- Match errors on `error.code`, not on the message text. | |
| 51 | +- Send `If-None-Match` and handle `304`; handle `429` with `Retry-After`. | |
| 52 | +- Prefer `interval` on v2 endpoints (`timeframe` remains as an alias). | |
added
hfmarketdata/web/content/samples/aapl-pe.json
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +{"ticker":"AAPL","metric":"pe","note":"Illustrative fallback used only when /v1/fundamentals/AAPL/ratios/daily is not live: split-adjusted weekly close (HF Market Data, adj_split) divided by the diluted EPS reported in Apple's latest Form 10-K at that date (10-K EPS stepped at each release; not a trailing-twelve-month series).","source":{"prices":"https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day&adjustment=adj_split&start=2015-01-01&limit=50000","eps":"Apple Inc. Form 10-K filings FY2014–FY2025 (diluted EPS, split-adjusted)"},"generated_at":"2026-09-04T23:38:04.919Z","points":[{"date":"2015-01-02","close":27.3325,"eps_ttm":1.61,"pe":16.98},{"date":"2015-01-09","close":28.0025,"eps_ttm":1.61,"pe":17.39},{"date":"2015-01-16","close":26.4975,"eps_ttm":1.61,"pe":16.46},{"date":"2015-01-23","close":28.245,"eps_ttm":1.61,"pe":17.54},{"date":"2015-01-30","close":29.29,"eps_ttm":1.61,"pe":18.19},{"date":"2015-02-06","close":29.7325,"eps_ttm":1.61,"pe":18.47},{"date":"2015-02-13","close":31.77,"eps_ttm":1.61,"pe":19.73},{"date":"2015-02-20","close":32.375,"eps_ttm":1.61,"pe":20.11},{"date":"2015-02-27","close":32.115,"eps_ttm":1.61,"pe":19.95},{"date":"2015-03-06","close":31.65,"eps_ttm":1.61,"pe":19.66},{"date":"2015-03-13","close":30.8975,"eps_ttm":1.61,"pe":19.19},{"date":"2015-03-20","close":31.475,"eps_ttm":1.61,"pe":19.55},{"date":"2015-03-27","close":30.8125,"eps_ttm":1.61,"pe":19.14},{"date":"2015-04-02","close":31.33,"eps_ttm":1.61,"pe":19.46},{"date":"2015-04-10","close":31.775,"eps_ttm":1.61,"pe":19.74},{"date":"2015-04-17","close":31.1875,"eps_ttm":1.61,"pe":19.37},{"date":"2015-04-24","close":32.57,"eps_ttm":1.61,"pe":20.23},{"date":"2015-05-01","close":32.2375,"eps_ttm":1.61,"pe":20.02},{"date":"2015-05-08","close":31.905,"eps_ttm":1.61,"pe":19.82},{"date":"2015-05-15","close":32.1925,"eps_ttm":1.61,"pe":20},{"date":"2015-05-22","close":33.135,"eps_ttm":1.61,"pe":20.58},{"date":"2015-05-29","close":32.57,"eps_ttm":1.61,"pe":20.23},{"date":"2015-06-05","close":32.1625,"eps_ttm":1.61,"pe":19.98},{"date":"2015-06-12","close":31.7925,"eps_ttm":1.61,"pe":19.75},{"date":"2015-06-19","close":31.65,"eps_ttm":1.61,"pe":19.66},{"date":"2015-06-26","close":31.6875,"eps_ttm":1.61,"pe":19.68},{"date":"2015-07-02","close":31.61,"eps_ttm":1.61,"pe":19.63},{"date":"2015-07-10","close":30.82,"eps_ttm":1.61,"pe":19.14},{"date":"2015-07-17","close":32.405,"eps_ttm":1.61,"pe":20.13},{"date":"2015-07-24","close":31.125,"eps_ttm":1.61,"pe":19.33},{"date":"2015-07-31","close":30.325,"eps_ttm":1.61,"pe":18.84},{"date":"2015-08-07","close":28.88,"eps_ttm":1.61,"pe":17.94},{"date":"2015-08-14","close":28.99,"eps_ttm":1.61,"pe":18.01},{"date":"2015-08-21","close":26.44,"eps_ttm":1.61,"pe":16.42},{"date":"2015-08-28","close":28.3225,"eps_ttm":1.61,"pe":17.59},{"date":"2015-09-04","close":27.3175,"eps_ttm":1.61,"pe":16.97},{"date":"2015-09-11","close":28.5525,"eps_ttm":1.61,"pe":17.73},{"date":"2015-09-18","close":28.3625,"eps_ttm":1.61,"pe":17.62},{"date":"2015-09-25","close":28.6775,"eps_ttm":1.61,"pe":17.81},{"date":"2015-10-02","close":27.595,"eps_ttm":1.61,"pe":17.14},{"date":"2015-10-09","close":28.03,"eps_ttm":1.61,"pe":17.41},{"date":"2015-10-16","close":27.76,"eps_ttm":1.61,"pe":17.24},{"date":"2015-10-23","close":29.77,"eps_ttm":1.61,"pe":18.49},{"date":"2015-10-30","close":29.875,"eps_ttm":2.31,"pe":12.93},{"date":"2015-11-06","close":30.265,"eps_ttm":2.31,"pe":13.1},{"date":"2015-11-13","close":28.085,"eps_ttm":2.31,"pe":12.16},{"date":"2015-11-20","close":29.825,"eps_ttm":2.31,"pe":12.91},{"date":"2015-11-27","close":29.4525,"eps_ttm":2.31,"pe":12.75},{"date":"2015-12-04","close":29.7575,"eps_ttm":2.31,"pe":12.88},{"date":"2015-12-11","close":28.295,"eps_ttm":2.31,"pe":12.25},{"date":"2015-12-18","close":26.5075,"eps_ttm":2.31,"pe":11.48},{"date":"2015-12-24","close":27.0075,"eps_ttm":2.31,"pe":11.69},{"date":"2015-12-31","close":26.315,"eps_ttm":2.31,"pe":11.39},{"date":"2016-01-08","close":24.24,"eps_ttm":2.31,"pe":10.49},{"date":"2016-01-15","close":24.2825,"eps_ttm":2.31,"pe":10.51},{"date":"2016-01-22","close":25.355,"eps_ttm":2.31,"pe":10.98},{"date":"2016-01-29","close":24.335,"eps_ttm":2.31,"pe":10.53},{"date":"2016-02-05","close":23.505,"eps_ttm":2.31,"pe":10.18},{"date":"2016-02-12","close":23.4975,"eps_ttm":2.31,"pe":10.17},{"date":"2016-02-19","close":24.01,"eps_ttm":2.31,"pe":10.39},{"date":"2016-02-26","close":24.2275,"eps_ttm":2.31,"pe":10.49},{"date":"2016-03-04","close":25.7525,"eps_ttm":2.31,"pe":11.15},{"date":"2016-03-11","close":25.565,"eps_ttm":2.31,"pe":11.07},{"date":"2016-03-18","close":26.48,"eps_ttm":2.31,"pe":11.46},{"date":"2016-03-24","close":26.4175,"eps_ttm":2.31,"pe":11.44},{"date":"2016-04-01","close":27.4975,"eps_ttm":2.31,"pe":11.9},{"date":"2016-04-08","close":27.165,"eps_ttm":2.31,"pe":11.76},{"date":"2016-04-15","close":27.4625,"eps_ttm":2.31,"pe":11.89},{"date":"2016-04-22","close":26.42,"eps_ttm":2.31,"pe":11.44},{"date":"2016-04-29","close":23.435,"eps_ttm":2.31,"pe":10.15},{"date":"2016-05-06","close":23.18,"eps_ttm":2.31,"pe":10.03},{"date":"2016-05-13","close":22.63,"eps_ttm":2.31,"pe":9.8},{"date":"2016-05-20","close":23.805,"eps_ttm":2.31,"pe":10.31},{"date":"2016-05-27","close":25.0875,"eps_ttm":2.31,"pe":10.86},{"date":"2016-06-03","close":24.48,"eps_ttm":2.31,"pe":10.6},{"date":"2016-06-10","close":24.7075,"eps_ttm":2.31,"pe":10.7},{"date":"2016-06-17","close":23.8325,"eps_ttm":2.31,"pe":10.32},{"date":"2016-06-24","close":23.35,"eps_ttm":2.31,"pe":10.11},{"date":"2016-07-01","close":23.9725,"eps_ttm":2.31,"pe":10.38},{"date":"2016-07-08","close":24.17,"eps_ttm":2.31,"pe":10.46},{"date":"2016-07-15","close":24.695,"eps_ttm":2.31,"pe":10.69},{"date":"2016-07-22","close":24.665,"eps_ttm":2.31,"pe":10.68},{"date":"2016-07-29","close":26.0525,"eps_ttm":2.31,"pe":11.28},{"date":"2016-08-05","close":26.87,"eps_ttm":2.31,"pe":11.63},{"date":"2016-08-12","close":27.045,"eps_ttm":2.31,"pe":11.71},{"date":"2016-08-19","close":27.34,"eps_ttm":2.31,"pe":11.84},{"date":"2016-08-26","close":26.735,"eps_ttm":2.31,"pe":11.57},{"date":"2016-09-02","close":26.9325,"eps_ttm":2.31,"pe":11.66},{"date":"2016-09-09","close":25.7825,"eps_ttm":2.31,"pe":11.16},{"date":"2016-09-16","close":28.73,"eps_ttm":2.31,"pe":12.44},{"date":"2016-09-23","close":28.1775,"eps_ttm":2.31,"pe":12.2},{"date":"2016-09-30","close":28.2625,"eps_ttm":2.31,"pe":12.23},{"date":"2016-10-07","close":28.515,"eps_ttm":2.31,"pe":12.34},{"date":"2016-10-14","close":29.4075,"eps_ttm":2.31,"pe":12.73},{"date":"2016-10-21","close":29.15,"eps_ttm":2.31,"pe":12.62},{"date":"2016-10-28","close":28.43,"eps_ttm":2.08,"pe":13.67},{"date":"2016-11-04","close":27.21,"eps_ttm":2.08,"pe":13.08},{"date":"2016-11-11","close":27.1075,"eps_ttm":2.08,"pe":13.03},{"date":"2016-11-18","close":27.515,"eps_ttm":2.08,"pe":13.23},{"date":"2016-11-25","close":27.9475,"eps_ttm":2.08,"pe":13.44},{"date":"2016-12-02","close":27.475,"eps_ttm":2.08,"pe":13.21},{"date":"2016-12-09","close":28.4875,"eps_ttm":2.08,"pe":13.7},{"date":"2016-12-16","close":28.9925,"eps_ttm":2.08,"pe":13.94},{"date":"2016-12-23","close":29.13,"eps_ttm":2.08,"pe":14},{"date":"2016-12-30","close":28.955,"eps_ttm":2.08,"pe":13.92},{"date":"2017-01-06","close":29.4775,"eps_ttm":2.08,"pe":14.17},{"date":"2017-01-13","close":29.76,"eps_ttm":2.08,"pe":14.31},{"date":"2017-01-20","close":30,"eps_ttm":2.08,"pe":14.42},{"date":"2017-01-27","close":30.4875,"eps_ttm":2.08,"pe":14.66},{"date":"2017-02-03","close":32.27,"eps_ttm":2.08,"pe":15.51},{"date":"2017-02-10","close":33.03,"eps_ttm":2.08,"pe":15.88},{"date":"2017-02-17","close":33.93,"eps_ttm":2.08,"pe":16.31},{"date":"2017-02-24","close":34.165,"eps_ttm":2.08,"pe":16.43},{"date":"2017-03-03","close":34.945,"eps_ttm":2.08,"pe":16.8},{"date":"2017-03-10","close":34.785,"eps_ttm":2.08,"pe":16.72},{"date":"2017-03-17","close":34.9975,"eps_ttm":2.08,"pe":16.83},{"date":"2017-03-24","close":35.16,"eps_ttm":2.08,"pe":16.9},{"date":"2017-03-31","close":35.915,"eps_ttm":2.08,"pe":17.27},{"date":"2017-04-07","close":35.835,"eps_ttm":2.08,"pe":17.23},{"date":"2017-04-13","close":35.2625,"eps_ttm":2.08,"pe":16.95},{"date":"2017-04-21","close":35.5675,"eps_ttm":2.08,"pe":17.1},{"date":"2017-04-28","close":35.9125,"eps_ttm":2.08,"pe":17.27},{"date":"2017-05-05","close":37.24,"eps_ttm":2.08,"pe":17.9},{"date":"2017-05-12","close":39.025,"eps_ttm":2.08,"pe":18.76},{"date":"2017-05-19","close":38.265,"eps_ttm":2.08,"pe":18.4},{"date":"2017-05-26","close":38.4025,"eps_ttm":2.08,"pe":18.46},{"date":"2017-06-02","close":38.8625,"eps_ttm":2.08,"pe":18.68},{"date":"2017-06-09","close":37.245,"eps_ttm":2.08,"pe":17.91},{"date":"2017-06-16","close":35.5675,"eps_ttm":2.08,"pe":17.1},{"date":"2017-06-23","close":36.57,"eps_ttm":2.08,"pe":17.58},{"date":"2017-06-30","close":36.005,"eps_ttm":2.08,"pe":17.31},{"date":"2017-07-07","close":36.045,"eps_ttm":2.08,"pe":17.33},{"date":"2017-07-14","close":37.26,"eps_ttm":2.08,"pe":17.91},{"date":"2017-07-21","close":37.5675,"eps_ttm":2.08,"pe":18.06},{"date":"2017-07-28","close":37.375,"eps_ttm":2.08,"pe":17.97},{"date":"2017-08-04","close":39.0975,"eps_ttm":2.08,"pe":18.8},{"date":"2017-08-11","close":39.37,"eps_ttm":2.08,"pe":18.93},{"date":"2017-08-18","close":39.375,"eps_ttm":2.08,"pe":18.93},{"date":"2017-08-25","close":39.965,"eps_ttm":2.08,"pe":19.21},{"date":"2017-09-01","close":41.0125,"eps_ttm":2.08,"pe":19.72},{"date":"2017-09-08","close":39.6575,"eps_ttm":2.08,"pe":19.07},{"date":"2017-09-15","close":39.97,"eps_ttm":2.08,"pe":19.22},{"date":"2017-09-22","close":37.9725,"eps_ttm":2.08,"pe":18.26},{"date":"2017-09-29","close":38.53,"eps_ttm":2.08,"pe":18.52},{"date":"2017-10-06","close":38.825,"eps_ttm":2.08,"pe":18.67},{"date":"2017-10-13","close":39.2475,"eps_ttm":2.08,"pe":18.87},{"date":"2017-10-20","close":39.0625,"eps_ttm":2.08,"pe":18.78},{"date":"2017-10-27","close":40.7625,"eps_ttm":2.08,"pe":19.6},{"date":"2017-11-03","close":43.125,"eps_ttm":2.3,"pe":18.75},{"date":"2017-11-10","close":43.6675,"eps_ttm":2.3,"pe":18.99},{"date":"2017-11-17","close":42.5375,"eps_ttm":2.3,"pe":18.49},{"date":"2017-11-24","close":43.7425,"eps_ttm":2.3,"pe":19.02},{"date":"2017-12-01","close":42.7625,"eps_ttm":2.3,"pe":18.59},{"date":"2017-12-08","close":42.3425,"eps_ttm":2.3,"pe":18.41},{"date":"2017-12-15","close":43.4925,"eps_ttm":2.3,"pe":18.91},{"date":"2017-12-22","close":43.7525,"eps_ttm":2.3,"pe":19.02},{"date":"2017-12-29","close":42.3075,"eps_ttm":2.3,"pe":18.39},{"date":"2018-01-05","close":43.75,"eps_ttm":2.3,"pe":19.02},{"date":"2018-01-12","close":44.2725,"eps_ttm":2.3,"pe":19.25},{"date":"2018-01-19","close":44.615,"eps_ttm":2.3,"pe":19.4},{"date":"2018-01-26","close":42.8775,"eps_ttm":2.3,"pe":18.64},{"date":"2018-02-02","close":40.125,"eps_ttm":2.3,"pe":17.45},{"date":"2018-02-09","close":39.1025,"eps_ttm":2.3,"pe":17},{"date":"2018-02-16","close":43.1075,"eps_ttm":2.3,"pe":18.74},{"date":"2018-02-23","close":43.875,"eps_ttm":2.3,"pe":19.08},{"date":"2018-03-02","close":44.0525,"eps_ttm":2.3,"pe":19.15},{"date":"2018-03-09","close":44.995,"eps_ttm":2.3,"pe":19.56},{"date":"2018-03-16","close":44.505,"eps_ttm":2.3,"pe":19.35},{"date":"2018-03-23","close":41.235,"eps_ttm":2.3,"pe":17.93},{"date":"2018-03-29","close":41.945,"eps_ttm":2.3,"pe":18.24},{"date":"2018-04-06","close":42.095,"eps_ttm":2.3,"pe":18.3},{"date":"2018-04-13","close":43.6825,"eps_ttm":2.3,"pe":18.99},{"date":"2018-04-20","close":41.43,"eps_ttm":2.3,"pe":18.01},{"date":"2018-04-27","close":40.58,"eps_ttm":2.3,"pe":17.64},{"date":"2018-05-04","close":45.9575,"eps_ttm":2.3,"pe":19.98},{"date":"2018-05-11","close":47.1475,"eps_ttm":2.3,"pe":20.5},{"date":"2018-05-18","close":46.5775,"eps_ttm":2.3,"pe":20.25},{"date":"2018-05-25","close":47.145,"eps_ttm":2.3,"pe":20.5},{"date":"2018-06-01","close":47.56,"eps_ttm":2.3,"pe":20.68},{"date":"2018-06-08","close":47.925,"eps_ttm":2.3,"pe":20.84},{"date":"2018-06-15","close":47.21,"eps_ttm":2.3,"pe":20.53},{"date":"2018-06-22","close":46.23,"eps_ttm":2.3,"pe":20.1},{"date":"2018-06-29","close":46.2775,"eps_ttm":2.3,"pe":20.12},{"date":"2018-07-06","close":46.9925,"eps_ttm":2.3,"pe":20.43},{"date":"2018-07-13","close":47.8325,"eps_ttm":2.3,"pe":20.8},{"date":"2018-07-20","close":47.86,"eps_ttm":2.3,"pe":20.81},{"date":"2018-07-27","close":47.745,"eps_ttm":2.3,"pe":20.76},{"date":"2018-08-03","close":51.9975,"eps_ttm":2.3,"pe":22.61},{"date":"2018-08-10","close":51.8825,"eps_ttm":2.3,"pe":22.56},{"date":"2018-08-17","close":54.395,"eps_ttm":2.3,"pe":23.65},{"date":"2018-08-24","close":54.04,"eps_ttm":2.3,"pe":23.5},{"date":"2018-08-31","close":56.9075,"eps_ttm":2.3,"pe":24.74},{"date":"2018-09-07","close":55.325,"eps_ttm":2.3,"pe":24.05},{"date":"2018-09-14","close":55.96,"eps_ttm":2.3,"pe":24.33},{"date":"2018-09-21","close":54.415,"eps_ttm":2.3,"pe":23.66},{"date":"2018-09-28","close":56.435,"eps_ttm":2.3,"pe":24.54},{"date":"2018-10-05","close":56.0725,"eps_ttm":2.3,"pe":24.38},{"date":"2018-10-12","close":55.5275,"eps_ttm":2.3,"pe":24.14},{"date":"2018-10-19","close":54.8275,"eps_ttm":2.3,"pe":23.84},{"date":"2018-10-26","close":54.075,"eps_ttm":2.3,"pe":23.51},{"date":"2018-11-02","close":51.87,"eps_ttm":2.98,"pe":17.41},{"date":"2018-11-09","close":51.1175,"eps_ttm":2.98,"pe":17.15},{"date":"2018-11-16","close":48.3825,"eps_ttm":2.98,"pe":16.24},{"date":"2018-11-23","close":43.0725,"eps_ttm":2.98,"pe":14.45},{"date":"2018-11-30","close":44.645,"eps_ttm":2.98,"pe":14.98},{"date":"2018-12-07","close":42.1225,"eps_ttm":2.98,"pe":14.14},{"date":"2018-12-14","close":41.37,"eps_ttm":2.98,"pe":13.88},{"date":"2018-12-21","close":37.6825,"eps_ttm":2.98,"pe":12.65},{"date":"2018-12-28","close":39.0575,"eps_ttm":2.98,"pe":13.11},{"date":"2019-01-04","close":37.065,"eps_ttm":2.98,"pe":12.44},{"date":"2019-01-11","close":38.0725,"eps_ttm":2.98,"pe":12.78},{"date":"2019-01-18","close":39.205,"eps_ttm":2.98,"pe":13.16},{"date":"2019-01-25","close":39.44,"eps_ttm":2.98,"pe":13.23},{"date":"2019-02-01","close":41.63,"eps_ttm":2.98,"pe":13.97},{"date":"2019-02-08","close":42.6025,"eps_ttm":2.98,"pe":14.3},{"date":"2019-02-15","close":42.605,"eps_ttm":2.98,"pe":14.3},{"date":"2019-02-22","close":43.2425,"eps_ttm":2.98,"pe":14.51},{"date":"2019-03-01","close":43.7425,"eps_ttm":2.98,"pe":14.68},{"date":"2019-03-08","close":43.2275,"eps_ttm":2.98,"pe":14.51},{"date":"2019-03-15","close":46.53,"eps_ttm":2.98,"pe":15.61},{"date":"2019-03-22","close":47.7625,"eps_ttm":2.98,"pe":16.03},{"date":"2019-03-29","close":47.4875,"eps_ttm":2.98,"pe":15.94},{"date":"2019-04-05","close":49.25,"eps_ttm":2.98,"pe":16.53},{"date":"2019-04-12","close":49.7175,"eps_ttm":2.98,"pe":16.68},{"date":"2019-04-18","close":50.965,"eps_ttm":2.98,"pe":17.1},{"date":"2019-04-26","close":51.075,"eps_ttm":2.98,"pe":17.14},{"date":"2019-05-03","close":52.9375,"eps_ttm":2.98,"pe":17.76},{"date":"2019-05-10","close":49.295,"eps_ttm":2.98,"pe":16.54},{"date":"2019-05-17","close":47.25,"eps_ttm":2.98,"pe":15.86},{"date":"2019-05-24","close":44.7425,"eps_ttm":2.98,"pe":15.01},{"date":"2019-05-31","close":43.7675,"eps_ttm":2.98,"pe":14.69},{"date":"2019-06-07","close":47.5375,"eps_ttm":2.98,"pe":15.95},{"date":"2019-06-14","close":48.185,"eps_ttm":2.98,"pe":16.17},{"date":"2019-06-21","close":49.695,"eps_ttm":2.98,"pe":16.68},{"date":"2019-06-28","close":49.48,"eps_ttm":2.98,"pe":16.6},{"date":"2019-07-05","close":51.0575,"eps_ttm":2.98,"pe":17.13},{"date":"2019-07-12","close":50.825,"eps_ttm":2.98,"pe":17.06},{"date":"2019-07-19","close":50.6475,"eps_ttm":2.98,"pe":17},{"date":"2019-07-26","close":51.935,"eps_ttm":2.98,"pe":17.43},{"date":"2019-08-02","close":51.005,"eps_ttm":2.98,"pe":17.12},{"date":"2019-08-09","close":50.2475,"eps_ttm":2.98,"pe":16.86},{"date":"2019-08-16","close":51.625,"eps_ttm":2.98,"pe":17.32},{"date":"2019-08-23","close":50.66,"eps_ttm":2.98,"pe":17},{"date":"2019-08-30","close":52.185,"eps_ttm":2.98,"pe":17.51},{"date":"2019-09-06","close":53.315,"eps_ttm":2.98,"pe":17.89},{"date":"2019-09-13","close":54.6875,"eps_ttm":2.98,"pe":18.35},{"date":"2019-09-20","close":54.4325,"eps_ttm":2.98,"pe":18.27},{"date":"2019-09-27","close":54.705,"eps_ttm":2.98,"pe":18.36},{"date":"2019-10-04","close":56.7525,"eps_ttm":2.98,"pe":19.04},{"date":"2019-10-11","close":59.0525,"eps_ttm":2.98,"pe":19.82},{"date":"2019-10-18","close":59.1025,"eps_ttm":2.98,"pe":19.83},{"date":"2019-10-25","close":61.645,"eps_ttm":2.98,"pe":20.69},{"date":"2019-11-01","close":63.955,"eps_ttm":2.97,"pe":21.53},{"date":"2019-11-08","close":65.035,"eps_ttm":2.97,"pe":21.9},{"date":"2019-11-15","close":66.44,"eps_ttm":2.97,"pe":22.37},{"date":"2019-11-22","close":65.445,"eps_ttm":2.97,"pe":22.04},{"date":"2019-11-29","close":66.8125,"eps_ttm":2.97,"pe":22.5},{"date":"2019-12-06","close":67.6775,"eps_ttm":2.97,"pe":22.79},{"date":"2019-12-13","close":68.7875,"eps_ttm":2.97,"pe":23.16},{"date":"2019-12-20","close":69.86,"eps_ttm":2.97,"pe":23.52},{"date":"2019-12-27","close":72.45,"eps_ttm":2.97,"pe":24.39},{"date":"2020-01-03","close":74.3575,"eps_ttm":2.97,"pe":25.04},{"date":"2020-01-10","close":77.5825,"eps_ttm":2.97,"pe":26.12},{"date":"2020-01-17","close":79.6825,"eps_ttm":2.97,"pe":26.83},{"date":"2020-01-24","close":79.5775,"eps_ttm":2.97,"pe":26.79},{"date":"2020-01-31","close":77.3775,"eps_ttm":2.97,"pe":26.05},{"date":"2020-02-07","close":80.0075,"eps_ttm":2.97,"pe":26.94},{"date":"2020-02-14","close":81.2375,"eps_ttm":2.97,"pe":27.35},{"date":"2020-02-21","close":78.2625,"eps_ttm":2.97,"pe":26.35},{"date":"2020-02-28","close":68.34,"eps_ttm":2.97,"pe":23.01},{"date":"2020-03-06","close":72.2575,"eps_ttm":2.97,"pe":24.33},{"date":"2020-03-13","close":69.4925,"eps_ttm":2.97,"pe":23.4},{"date":"2020-03-20","close":57.31,"eps_ttm":2.97,"pe":19.3},{"date":"2020-03-27","close":61.935,"eps_ttm":2.97,"pe":20.85},{"date":"2020-04-03","close":60.3525,"eps_ttm":2.97,"pe":20.32},{"date":"2020-04-09","close":66.9975,"eps_ttm":2.97,"pe":22.56},{"date":"2020-04-17","close":70.7,"eps_ttm":2.97,"pe":23.8},{"date":"2020-04-24","close":70.7425,"eps_ttm":2.97,"pe":23.82},{"date":"2020-05-01","close":72.2675,"eps_ttm":2.97,"pe":24.33},{"date":"2020-05-08","close":77.5325,"eps_ttm":2.97,"pe":26.11},{"date":"2020-05-15","close":76.9275,"eps_ttm":2.97,"pe":25.9},{"date":"2020-05-22","close":79.7225,"eps_ttm":2.97,"pe":26.84},{"date":"2020-05-29","close":79.485,"eps_ttm":2.97,"pe":26.76},{"date":"2020-06-05","close":82.875,"eps_ttm":2.97,"pe":27.9},{"date":"2020-06-12","close":84.7,"eps_ttm":2.97,"pe":28.52},{"date":"2020-06-19","close":87.43,"eps_ttm":2.97,"pe":29.44},{"date":"2020-06-26","close":88.4075,"eps_ttm":2.97,"pe":29.77},{"date":"2020-07-02","close":91.0275,"eps_ttm":2.97,"pe":30.65},{"date":"2020-07-10","close":95.92,"eps_ttm":2.97,"pe":32.3},{"date":"2020-07-17","close":96.3275,"eps_ttm":2.97,"pe":32.43},{"date":"2020-07-24","close":92.615,"eps_ttm":2.97,"pe":31.18},{"date":"2020-07-31","close":106.26,"eps_ttm":2.97,"pe":35.78},{"date":"2020-08-07","close":111.1125,"eps_ttm":2.97,"pe":37.41},{"date":"2020-08-14","close":114.9075,"eps_ttm":2.97,"pe":38.69},{"date":"2020-08-21","close":124.37,"eps_ttm":2.97,"pe":41.88},{"date":"2020-08-28","close":124.8075,"eps_ttm":2.97,"pe":42.02},{"date":"2020-09-04","close":120.96,"eps_ttm":2.97,"pe":40.73},{"date":"2020-09-11","close":112,"eps_ttm":2.97,"pe":37.71},{"date":"2020-09-18","close":106.84,"eps_ttm":2.97,"pe":35.97},{"date":"2020-09-25","close":112.28,"eps_ttm":2.97,"pe":37.8},{"date":"2020-10-02","close":113.02,"eps_ttm":2.97,"pe":38.05},{"date":"2020-10-09","close":116.97,"eps_ttm":2.97,"pe":39.38},{"date":"2020-10-16","close":119.02,"eps_ttm":2.97,"pe":40.07},{"date":"2020-10-23","close":115.04,"eps_ttm":2.97,"pe":38.73},{"date":"2020-10-30","close":108.86,"eps_ttm":3.28,"pe":33.19},{"date":"2020-11-06","close":118.69,"eps_ttm":3.28,"pe":36.19},{"date":"2020-11-13","close":119.26,"eps_ttm":3.28,"pe":36.36},{"date":"2020-11-20","close":117.34,"eps_ttm":3.28,"pe":35.77},{"date":"2020-11-27","close":116.59,"eps_ttm":3.28,"pe":35.55},{"date":"2020-12-04","close":122.25,"eps_ttm":3.28,"pe":37.27},{"date":"2020-12-11","close":122.41,"eps_ttm":3.28,"pe":37.32},{"date":"2020-12-18","close":126.66,"eps_ttm":3.28,"pe":38.62},{"date":"2020-12-24","close":131.97,"eps_ttm":3.28,"pe":40.23},{"date":"2020-12-31","close":132.69,"eps_ttm":3.28,"pe":40.45},{"date":"2021-01-08","close":132.05,"eps_ttm":3.28,"pe":40.26},{"date":"2021-01-15","close":127.14,"eps_ttm":3.28,"pe":38.76},{"date":"2021-01-22","close":139.07,"eps_ttm":3.28,"pe":42.4},{"date":"2021-01-29","close":131.96,"eps_ttm":3.28,"pe":40.23},{"date":"2021-02-05","close":136.76,"eps_ttm":3.28,"pe":41.7},{"date":"2021-02-12","close":135.37,"eps_ttm":3.28,"pe":41.27},{"date":"2021-02-19","close":129.87,"eps_ttm":3.28,"pe":39.59},{"date":"2021-02-26","close":121.26,"eps_ttm":3.28,"pe":36.97},{"date":"2021-03-05","close":121.42,"eps_ttm":3.28,"pe":37.02},{"date":"2021-03-12","close":121.03,"eps_ttm":3.28,"pe":36.9},{"date":"2021-03-19","close":119.99,"eps_ttm":3.28,"pe":36.58},{"date":"2021-03-26","close":121.21,"eps_ttm":3.28,"pe":36.95},{"date":"2021-04-01","close":123,"eps_ttm":3.28,"pe":37.5},{"date":"2021-04-09","close":133,"eps_ttm":3.28,"pe":40.55},{"date":"2021-04-16","close":134.16,"eps_ttm":3.28,"pe":40.9},{"date":"2021-04-23","close":134.32,"eps_ttm":3.28,"pe":40.95},{"date":"2021-04-30","close":131.46,"eps_ttm":3.28,"pe":40.08},{"date":"2021-05-07","close":130.21,"eps_ttm":3.28,"pe":39.7},{"date":"2021-05-14","close":127.45,"eps_ttm":3.28,"pe":38.86},{"date":"2021-05-21","close":125.43,"eps_ttm":3.28,"pe":38.24},{"date":"2021-05-28","close":124.61,"eps_ttm":3.28,"pe":37.99},{"date":"2021-06-04","close":125.89,"eps_ttm":3.28,"pe":38.38},{"date":"2021-06-11","close":127.35,"eps_ttm":3.28,"pe":38.83},{"date":"2021-06-18","close":130.46,"eps_ttm":3.28,"pe":39.77},{"date":"2021-06-25","close":133.11,"eps_ttm":3.28,"pe":40.58},{"date":"2021-07-02","close":139.96,"eps_ttm":3.28,"pe":42.67},{"date":"2021-07-09","close":145.11,"eps_ttm":3.28,"pe":44.24},{"date":"2021-07-16","close":146.39,"eps_ttm":3.28,"pe":44.63},{"date":"2021-07-23","close":148.56,"eps_ttm":3.28,"pe":45.29},{"date":"2021-07-30","close":145.86,"eps_ttm":3.28,"pe":44.47},{"date":"2021-08-06","close":146.14,"eps_ttm":3.28,"pe":44.55},{"date":"2021-08-13","close":149.1,"eps_ttm":3.28,"pe":45.46},{"date":"2021-08-20","close":148.19,"eps_ttm":3.28,"pe":45.18},{"date":"2021-08-27","close":148.6,"eps_ttm":3.28,"pe":45.3},{"date":"2021-09-03","close":154.3,"eps_ttm":3.28,"pe":47.04},{"date":"2021-09-10","close":148.97,"eps_ttm":3.28,"pe":45.42},{"date":"2021-09-17","close":146.06,"eps_ttm":3.28,"pe":44.53},{"date":"2021-09-24","close":146.92,"eps_ttm":3.28,"pe":44.79},{"date":"2021-10-01","close":142.65,"eps_ttm":3.28,"pe":43.49},{"date":"2021-10-08","close":142.9,"eps_ttm":3.28,"pe":43.57},{"date":"2021-10-15","close":144.84,"eps_ttm":3.28,"pe":44.16},{"date":"2021-10-22","close":148.69,"eps_ttm":3.28,"pe":45.33},{"date":"2021-10-29","close":149.8,"eps_ttm":5.61,"pe":26.7},{"date":"2021-11-05","close":151.28,"eps_ttm":5.61,"pe":26.97},{"date":"2021-11-12","close":149.99,"eps_ttm":5.61,"pe":26.74},{"date":"2021-11-19","close":160.55,"eps_ttm":5.61,"pe":28.62},{"date":"2021-11-26","close":156.81,"eps_ttm":5.61,"pe":27.95},{"date":"2021-12-03","close":161.84,"eps_ttm":5.61,"pe":28.85},{"date":"2021-12-10","close":179.45,"eps_ttm":5.61,"pe":31.99},{"date":"2021-12-17","close":171.14,"eps_ttm":5.61,"pe":30.51},{"date":"2021-12-23","close":176.28,"eps_ttm":5.61,"pe":31.42},{"date":"2021-12-31","close":177.57,"eps_ttm":5.61,"pe":31.65},{"date":"2022-01-07","close":172.17,"eps_ttm":5.61,"pe":30.69},{"date":"2022-01-14","close":173.07,"eps_ttm":5.61,"pe":30.85},{"date":"2022-01-21","close":162.41,"eps_ttm":5.61,"pe":28.95},{"date":"2022-01-28","close":170.33,"eps_ttm":5.61,"pe":30.36},{"date":"2022-02-04","close":172.39,"eps_ttm":5.61,"pe":30.73},{"date":"2022-02-11","close":168.64,"eps_ttm":5.61,"pe":30.06},{"date":"2022-02-18","close":167.3,"eps_ttm":5.61,"pe":29.82},{"date":"2022-02-25","close":164.85,"eps_ttm":5.61,"pe":29.39},{"date":"2022-03-04","close":163.17,"eps_ttm":5.61,"pe":29.09},{"date":"2022-03-11","close":154.73,"eps_ttm":5.61,"pe":27.58},{"date":"2022-03-18","close":163.98,"eps_ttm":5.61,"pe":29.23},{"date":"2022-03-25","close":174.72,"eps_ttm":5.61,"pe":31.14},{"date":"2022-04-01","close":174.31,"eps_ttm":5.61,"pe":31.07},{"date":"2022-04-08","close":170.09,"eps_ttm":5.61,"pe":30.32},{"date":"2022-04-14","close":165.29,"eps_ttm":5.61,"pe":29.46},{"date":"2022-04-22","close":161.79,"eps_ttm":5.61,"pe":28.84},{"date":"2022-04-29","close":157.65,"eps_ttm":5.61,"pe":28.1},{"date":"2022-05-06","close":157.28,"eps_ttm":5.61,"pe":28.04},{"date":"2022-05-13","close":147.11,"eps_ttm":5.61,"pe":26.22},{"date":"2022-05-20","close":137.59,"eps_ttm":5.61,"pe":24.53},{"date":"2022-05-27","close":149.64,"eps_ttm":5.61,"pe":26.67},{"date":"2022-06-03","close":145.38,"eps_ttm":5.61,"pe":25.91},{"date":"2022-06-10","close":137.13,"eps_ttm":5.61,"pe":24.44},{"date":"2022-06-17","close":131.56,"eps_ttm":5.61,"pe":23.45},{"date":"2022-06-24","close":141.66,"eps_ttm":5.61,"pe":25.25},{"date":"2022-07-01","close":138.93,"eps_ttm":5.61,"pe":24.76},{"date":"2022-07-08","close":147.04,"eps_ttm":5.61,"pe":26.21},{"date":"2022-07-15","close":150.17,"eps_ttm":5.61,"pe":26.77},{"date":"2022-07-22","close":154.09,"eps_ttm":5.61,"pe":27.47},{"date":"2022-07-29","close":162.51,"eps_ttm":5.61,"pe":28.97},{"date":"2022-08-05","close":165.35,"eps_ttm":5.61,"pe":29.47},{"date":"2022-08-12","close":172.1,"eps_ttm":5.61,"pe":30.68},{"date":"2022-08-19","close":171.52,"eps_ttm":5.61,"pe":30.57},{"date":"2022-08-26","close":163.62,"eps_ttm":5.61,"pe":29.17},{"date":"2022-09-02","close":155.81,"eps_ttm":5.61,"pe":27.77},{"date":"2022-09-09","close":157.37,"eps_ttm":5.61,"pe":28.05},{"date":"2022-09-16","close":150.7,"eps_ttm":5.61,"pe":26.86},{"date":"2022-09-23","close":150.43,"eps_ttm":5.61,"pe":26.81},{"date":"2022-09-30","close":138.2,"eps_ttm":5.61,"pe":24.63},{"date":"2022-10-07","close":140.09,"eps_ttm":5.61,"pe":24.97},{"date":"2022-10-14","close":138.38,"eps_ttm":5.61,"pe":24.67},{"date":"2022-10-21","close":147.27,"eps_ttm":5.61,"pe":26.25},{"date":"2022-10-28","close":155.74,"eps_ttm":6.11,"pe":25.49},{"date":"2022-11-04","close":138.38,"eps_ttm":6.11,"pe":22.65},{"date":"2022-11-11","close":149.7,"eps_ttm":6.11,"pe":24.5},{"date":"2022-11-18","close":151.29,"eps_ttm":6.11,"pe":24.76},{"date":"2022-11-25","close":148.11,"eps_ttm":6.11,"pe":24.24},{"date":"2022-12-02","close":147.81,"eps_ttm":6.11,"pe":24.19},{"date":"2022-12-09","close":142.16,"eps_ttm":6.11,"pe":23.27},{"date":"2022-12-16","close":134.51,"eps_ttm":6.11,"pe":22.01},{"date":"2022-12-23","close":131.86,"eps_ttm":6.11,"pe":21.58},{"date":"2022-12-30","close":129.93,"eps_ttm":6.11,"pe":21.27},{"date":"2023-01-06","close":129.62,"eps_ttm":6.11,"pe":21.21},{"date":"2023-01-13","close":134.76,"eps_ttm":6.11,"pe":22.06},{"date":"2023-01-20","close":137.87,"eps_ttm":6.11,"pe":22.56},{"date":"2023-01-27","close":145.93,"eps_ttm":6.11,"pe":23.88},{"date":"2023-02-03","close":154.5,"eps_ttm":6.11,"pe":25.29},{"date":"2023-02-10","close":151.01,"eps_ttm":6.11,"pe":24.72},{"date":"2023-02-17","close":152.55,"eps_ttm":6.11,"pe":24.97},{"date":"2023-02-24","close":146.71,"eps_ttm":6.11,"pe":24.01},{"date":"2023-03-03","close":151.03,"eps_ttm":6.11,"pe":24.72},{"date":"2023-03-10","close":148.5,"eps_ttm":6.11,"pe":24.3},{"date":"2023-03-17","close":155,"eps_ttm":6.11,"pe":25.37},{"date":"2023-03-24","close":160.25,"eps_ttm":6.11,"pe":26.23},{"date":"2023-03-31","close":164.9,"eps_ttm":6.11,"pe":26.99},{"date":"2023-04-06","close":164.66,"eps_ttm":6.11,"pe":26.95},{"date":"2023-04-14","close":165.21,"eps_ttm":6.11,"pe":27.04},{"date":"2023-04-21","close":165.02,"eps_ttm":6.11,"pe":27.01},{"date":"2023-04-28","close":169.68,"eps_ttm":6.11,"pe":27.77},{"date":"2023-05-05","close":173.57,"eps_ttm":6.11,"pe":28.41},{"date":"2023-05-12","close":172.57,"eps_ttm":6.11,"pe":28.24},{"date":"2023-05-19","close":175.16,"eps_ttm":6.11,"pe":28.67},{"date":"2023-05-26","close":175.43,"eps_ttm":6.11,"pe":28.71},{"date":"2023-06-02","close":180.95,"eps_ttm":6.11,"pe":29.62},{"date":"2023-06-09","close":180.96,"eps_ttm":6.11,"pe":29.62},{"date":"2023-06-16","close":184.92,"eps_ttm":6.11,"pe":30.27},{"date":"2023-06-23","close":186.68,"eps_ttm":6.11,"pe":30.55},{"date":"2023-06-30","close":193.97,"eps_ttm":6.11,"pe":31.75},{"date":"2023-07-07","close":190.68,"eps_ttm":6.11,"pe":31.21},{"date":"2023-07-14","close":190.69,"eps_ttm":6.11,"pe":31.21},{"date":"2023-07-21","close":191.94,"eps_ttm":6.11,"pe":31.41},{"date":"2023-07-28","close":195.83,"eps_ttm":6.11,"pe":32.05},{"date":"2023-08-04","close":181.99,"eps_ttm":6.11,"pe":29.79},{"date":"2023-08-11","close":177.79,"eps_ttm":6.11,"pe":29.1},{"date":"2023-08-18","close":174.49,"eps_ttm":6.11,"pe":28.56},{"date":"2023-08-25","close":178.61,"eps_ttm":6.11,"pe":29.23},{"date":"2023-09-01","close":189.46,"eps_ttm":6.11,"pe":31.01},{"date":"2023-09-08","close":178.18,"eps_ttm":6.11,"pe":29.16},{"date":"2023-09-15","close":175.01,"eps_ttm":6.11,"pe":28.64},{"date":"2023-09-22","close":174.79,"eps_ttm":6.11,"pe":28.61},{"date":"2023-09-29","close":171.21,"eps_ttm":6.11,"pe":28.02},{"date":"2023-10-06","close":177.49,"eps_ttm":6.11,"pe":29.05},{"date":"2023-10-13","close":178.85,"eps_ttm":6.11,"pe":29.27},{"date":"2023-10-20","close":172.88,"eps_ttm":6.11,"pe":28.29},{"date":"2023-10-27","close":168.22,"eps_ttm":6.11,"pe":27.53},{"date":"2023-11-03","close":176.65,"eps_ttm":6.13,"pe":28.82},{"date":"2023-11-10","close":186.4,"eps_ttm":6.13,"pe":30.41},{"date":"2023-11-17","close":189.69,"eps_ttm":6.13,"pe":30.94},{"date":"2023-11-24","close":189.97,"eps_ttm":6.13,"pe":30.99},{"date":"2023-12-01","close":191.24,"eps_ttm":6.13,"pe":31.2},{"date":"2023-12-08","close":195.71,"eps_ttm":6.13,"pe":31.93},{"date":"2023-12-15","close":197.57,"eps_ttm":6.13,"pe":32.23},{"date":"2023-12-22","close":193.6,"eps_ttm":6.13,"pe":31.58},{"date":"2023-12-29","close":192.53,"eps_ttm":6.13,"pe":31.41},{"date":"2024-01-05","close":181.18,"eps_ttm":6.13,"pe":29.56},{"date":"2024-01-12","close":185.92,"eps_ttm":6.13,"pe":30.33},{"date":"2024-01-19","close":191.56,"eps_ttm":6.13,"pe":31.25},{"date":"2024-01-26","close":192.42,"eps_ttm":6.13,"pe":31.39},{"date":"2024-02-02","close":185.85,"eps_ttm":6.13,"pe":30.32},{"date":"2024-02-09","close":188.85,"eps_ttm":6.13,"pe":30.81},{"date":"2024-02-16","close":182.31,"eps_ttm":6.13,"pe":29.74},{"date":"2024-02-23","close":182.52,"eps_ttm":6.13,"pe":29.77},{"date":"2024-03-01","close":179.66,"eps_ttm":6.13,"pe":29.31},{"date":"2024-03-08","close":170.73,"eps_ttm":6.13,"pe":27.85},{"date":"2024-03-15","close":172.62,"eps_ttm":6.13,"pe":28.16},{"date":"2024-03-22","close":172.28,"eps_ttm":6.13,"pe":28.1},{"date":"2024-03-28","close":171.48,"eps_ttm":6.13,"pe":27.97},{"date":"2024-04-05","close":169.58,"eps_ttm":6.13,"pe":27.66},{"date":"2024-04-12","close":176.55,"eps_ttm":6.13,"pe":28.8},{"date":"2024-04-19","close":165,"eps_ttm":6.13,"pe":26.92},{"date":"2024-04-26","close":169.3,"eps_ttm":6.13,"pe":27.62},{"date":"2024-05-03","close":183.38,"eps_ttm":6.13,"pe":29.92},{"date":"2024-05-10","close":183.05,"eps_ttm":6.13,"pe":29.86},{"date":"2024-05-17","close":189.87,"eps_ttm":6.13,"pe":30.97},{"date":"2024-05-24","close":189.98,"eps_ttm":6.13,"pe":30.99},{"date":"2024-05-31","close":192.25,"eps_ttm":6.13,"pe":31.36},{"date":"2024-06-07","close":196.89,"eps_ttm":6.13,"pe":32.12},{"date":"2024-06-14","close":212.49,"eps_ttm":6.13,"pe":34.66},{"date":"2024-06-21","close":207.49,"eps_ttm":6.13,"pe":33.85},{"date":"2024-06-28","close":210.62,"eps_ttm":6.13,"pe":34.36},{"date":"2024-07-05","close":226.34,"eps_ttm":6.13,"pe":36.92},{"date":"2024-07-12","close":230.54,"eps_ttm":6.13,"pe":37.61},{"date":"2024-07-19","close":224.31,"eps_ttm":6.13,"pe":36.59},{"date":"2024-07-26","close":217.96,"eps_ttm":6.13,"pe":35.56},{"date":"2024-08-02","close":219.86,"eps_ttm":6.13,"pe":35.87},{"date":"2024-08-09","close":216.24,"eps_ttm":6.13,"pe":35.28},{"date":"2024-08-16","close":226.05,"eps_ttm":6.13,"pe":36.88},{"date":"2024-08-23","close":226.84,"eps_ttm":6.13,"pe":37},{"date":"2024-08-30","close":229,"eps_ttm":6.13,"pe":37.36},{"date":"2024-09-06","close":220.82,"eps_ttm":6.13,"pe":36.02},{"date":"2024-09-13","close":222.5,"eps_ttm":6.13,"pe":36.3},{"date":"2024-09-20","close":228.2,"eps_ttm":6.13,"pe":37.23},{"date":"2024-09-27","close":227.79,"eps_ttm":6.13,"pe":37.16},{"date":"2024-10-04","close":226.8,"eps_ttm":6.13,"pe":37},{"date":"2024-10-11","close":227.55,"eps_ttm":6.13,"pe":37.12},{"date":"2024-10-18","close":235,"eps_ttm":6.13,"pe":38.34},{"date":"2024-10-25","close":231.41,"eps_ttm":6.13,"pe":37.75},{"date":"2024-11-01","close":222.91,"eps_ttm":6.08,"pe":36.66},{"date":"2024-11-08","close":226.96,"eps_ttm":6.08,"pe":37.33},{"date":"2024-11-15","close":225,"eps_ttm":6.08,"pe":37.01},{"date":"2024-11-22","close":229.87,"eps_ttm":6.08,"pe":37.81},{"date":"2024-11-29","close":237.33,"eps_ttm":6.08,"pe":39.03},{"date":"2024-12-06","close":242.84,"eps_ttm":6.08,"pe":39.94},{"date":"2024-12-13","close":248.13,"eps_ttm":6.08,"pe":40.81},{"date":"2024-12-20","close":254.49,"eps_ttm":6.08,"pe":41.86},{"date":"2024-12-27","close":255.59,"eps_ttm":6.08,"pe":42.04},{"date":"2025-01-03","close":243.36,"eps_ttm":6.08,"pe":40.03},{"date":"2025-01-10","close":236.85,"eps_ttm":6.08,"pe":38.96},{"date":"2025-01-17","close":229.98,"eps_ttm":6.08,"pe":37.83},{"date":"2025-01-24","close":222.78,"eps_ttm":6.08,"pe":36.64},{"date":"2025-01-31","close":236,"eps_ttm":6.08,"pe":38.82},{"date":"2025-02-07","close":227.63,"eps_ttm":6.08,"pe":37.44},{"date":"2025-02-14","close":244.6,"eps_ttm":6.08,"pe":40.23},{"date":"2025-02-21","close":245.55,"eps_ttm":6.08,"pe":40.39},{"date":"2025-02-28","close":241.84,"eps_ttm":6.08,"pe":39.78},{"date":"2025-03-07","close":239.07,"eps_ttm":6.08,"pe":39.32},{"date":"2025-03-14","close":213.49,"eps_ttm":6.08,"pe":35.11},{"date":"2025-03-21","close":218.27,"eps_ttm":6.08,"pe":35.9},{"date":"2025-03-28","close":217.9,"eps_ttm":6.08,"pe":35.84},{"date":"2025-04-04","close":188.38,"eps_ttm":6.08,"pe":30.98},{"date":"2025-04-11","close":198.15,"eps_ttm":6.08,"pe":32.59},{"date":"2025-04-17","close":196.98,"eps_ttm":6.08,"pe":32.4},{"date":"2025-04-25","close":209.28,"eps_ttm":6.08,"pe":34.42},{"date":"2025-05-02","close":205.35,"eps_ttm":6.08,"pe":33.77},{"date":"2025-05-09","close":198.53,"eps_ttm":6.08,"pe":32.65},{"date":"2025-05-16","close":211.26,"eps_ttm":6.08,"pe":34.75},{"date":"2025-05-23","close":195.27,"eps_ttm":6.08,"pe":32.12},{"date":"2025-05-30","close":200.85,"eps_ttm":6.08,"pe":33.03},{"date":"2025-06-06","close":203.92,"eps_ttm":6.08,"pe":33.54},{"date":"2025-06-13","close":196.45,"eps_ttm":6.08,"pe":32.31},{"date":"2025-06-20","close":201,"eps_ttm":6.08,"pe":33.06},{"date":"2025-06-27","close":201.08,"eps_ttm":6.08,"pe":33.07},{"date":"2025-07-03","close":213.55,"eps_ttm":6.08,"pe":35.12},{"date":"2025-07-11","close":211.16,"eps_ttm":6.08,"pe":34.73},{"date":"2025-07-18","close":211.18,"eps_ttm":6.08,"pe":34.73},{"date":"2025-07-25","close":213.88,"eps_ttm":6.08,"pe":35.18},{"date":"2025-08-01","close":202.38,"eps_ttm":6.08,"pe":33.29},{"date":"2025-08-08","close":229.35,"eps_ttm":6.08,"pe":37.72},{"date":"2025-08-15","close":231.59,"eps_ttm":6.08,"pe":38.09},{"date":"2025-08-22","close":227.76,"eps_ttm":6.08,"pe":37.46},{"date":"2025-08-29","close":232.14,"eps_ttm":6.08,"pe":38.18},{"date":"2025-09-05","close":239.69,"eps_ttm":6.08,"pe":39.42},{"date":"2025-09-12","close":234.07,"eps_ttm":6.08,"pe":38.5},{"date":"2025-09-19","close":245.5,"eps_ttm":6.08,"pe":40.38},{"date":"2025-09-26","close":255.46,"eps_ttm":6.08,"pe":42.02},{"date":"2025-10-03","close":258.02,"eps_ttm":6.08,"pe":42.44},{"date":"2025-10-10","close":245.27,"eps_ttm":6.08,"pe":40.34},{"date":"2025-10-17","close":252.29,"eps_ttm":6.08,"pe":41.5},{"date":"2025-10-24","close":262.82,"eps_ttm":6.08,"pe":43.23},{"date":"2025-10-31","close":270.37,"eps_ttm":7.46,"pe":36.24},{"date":"2025-11-07","close":268.47,"eps_ttm":7.46,"pe":35.99},{"date":"2025-11-14","close":272.41,"eps_ttm":7.46,"pe":36.52},{"date":"2025-11-21","close":271.49,"eps_ttm":7.46,"pe":36.39},{"date":"2025-11-28","close":278.85,"eps_ttm":7.46,"pe":37.38},{"date":"2025-12-05","close":278.78,"eps_ttm":7.46,"pe":37.37},{"date":"2025-12-12","close":278.28,"eps_ttm":7.46,"pe":37.3},{"date":"2025-12-19","close":273.67,"eps_ttm":7.46,"pe":36.68},{"date":"2025-12-26","close":273.4,"eps_ttm":7.46,"pe":36.65},{"date":"2026-01-02","close":271.01,"eps_ttm":7.46,"pe":36.33},{"date":"2026-01-09","close":259.37,"eps_ttm":7.46,"pe":34.77},{"date":"2026-01-16","close":255.53,"eps_ttm":7.46,"pe":34.25},{"date":"2026-01-23","close":248.04,"eps_ttm":7.46,"pe":33.25},{"date":"2026-01-30","close":259.48,"eps_ttm":7.46,"pe":34.78},{"date":"2026-02-06","close":278.12,"eps_ttm":7.46,"pe":37.28},{"date":"2026-02-13","close":255.78,"eps_ttm":7.46,"pe":34.29},{"date":"2026-02-20","close":264.58,"eps_ttm":7.46,"pe":35.47},{"date":"2026-02-27","close":264.18,"eps_ttm":7.46,"pe":35.41},{"date":"2026-03-06","close":257.46,"eps_ttm":7.46,"pe":34.51},{"date":"2026-03-13","close":250.12,"eps_ttm":7.46,"pe":33.53},{"date":"2026-03-20","close":247.99,"eps_ttm":7.46,"pe":33.24},{"date":"2026-03-27","close":248.8,"eps_ttm":7.46,"pe":33.35},{"date":"2026-04-02","close":255.92,"eps_ttm":7.46,"pe":34.31},{"date":"2026-04-10","close":260.48,"eps_ttm":7.46,"pe":34.92},{"date":"2026-04-17","close":270.23,"eps_ttm":7.46,"pe":36.22},{"date":"2026-04-24","close":271.06,"eps_ttm":7.46,"pe":36.34},{"date":"2026-05-01","close":280.14,"eps_ttm":7.46,"pe":37.55},{"date":"2026-05-08","close":293.32,"eps_ttm":7.46,"pe":39.32},{"date":"2026-05-15","close":300.23,"eps_ttm":7.46,"pe":40.25},{"date":"2026-05-22","close":308.82,"eps_ttm":7.46,"pe":41.4},{"date":"2026-05-29","close":312.06,"eps_ttm":7.46,"pe":41.83},{"date":"2026-06-05","close":307.34,"eps_ttm":7.46,"pe":41.2},{"date":"2026-06-12","close":291.13,"eps_ttm":7.46,"pe":39.03},{"date":"2026-06-18","close":298.01,"eps_ttm":7.46,"pe":39.95},{"date":"2026-06-26","close":283.78,"eps_ttm":7.46,"pe":38.04},{"date":"2026-07-02","close":308.63,"eps_ttm":7.46,"pe":41.37},{"date":"2026-07-10","close":315.32,"eps_ttm":7.46,"pe":42.27},{"date":"2026-07-17","close":333.74,"eps_ttm":7.46,"pe":44.74},{"date":"2026-07-24","close":333.02,"eps_ttm":7.46,"pe":44.64},{"date":"2026-07-31","close":308.91,"eps_ttm":7.46,"pe":41.41},{"date":"2026-08-07","close":313.33,"eps_ttm":7.46,"pe":42}]} | |
| \ No newline at end of file | ||
modified
hfmarketdata/web/package-lock.json
+295 −51
@@ -13,7 +13,8 @@ | ||
| 13 | 13 | "prism-react-renderer": "^2.4.0", |
| 14 | 14 | "react": "^18.3.1", |
| 15 | 15 | "react-dom": "^18.3.1", |
| 16 | − "react-router-dom": "^6.28.0" | |
| 16 | + "react-router-dom": "^6.28.0", | |
| 17 | + "remark-gfm": "^4.0.1" | |
| 17 | 18 | }, |
| 18 | 19 | "devDependencies": { |
| 19 | 20 | "@mdx-js/rollup": "^3.1.0", |
@@ -1391,7 +1392,6 @@ | ||
| 1391 | 1392 | "version": "4.1.13", |
| 1392 | 1393 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", |
| 1393 | 1394 | "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", |
| 1394 | − "dev": true, | |
| 1395 | 1395 | "license": "MIT", |
| 1396 | 1396 | "dependencies": { |
| 1397 | 1397 | "@types/ms": "*" |
@@ -1428,7 +1428,6 @@ | ||
| 1428 | 1428 | "version": "4.0.4", |
| 1429 | 1429 | "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", |
| 1430 | 1430 | "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", |
| 1431 | − "dev": true, | |
| 1432 | 1431 | "license": "MIT", |
| 1433 | 1432 | "dependencies": { |
| 1434 | 1433 | "@types/unist": "*" |
@@ -1444,7 +1443,6 @@ | ||
| 1444 | 1443 | "version": "2.1.0", |
| 1445 | 1444 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", |
| 1446 | 1445 | "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", |
| 1447 | − "dev": true, | |
| 1448 | 1446 | "license": "MIT" |
| 1449 | 1447 | }, |
| 1450 | 1448 | "node_modules/@types/prismjs": { |
@@ -1467,7 +1465,6 @@ | ||
| 1467 | 1465 | "version": "3.0.3", |
| 1468 | 1466 | "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", |
| 1469 | 1467 | "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", |
| 1470 | − "dev": true, | |
| 1471 | 1468 | "license": "MIT" |
| 1472 | 1469 | }, |
| 1473 | 1470 | "node_modules/@ungap/structured-clone": { |
@@ -1535,7 +1532,6 @@ | ||
| 1535 | 1532 | "version": "2.0.2", |
| 1536 | 1533 | "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", |
| 1537 | 1534 | "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", |
| 1538 | − "dev": true, | |
| 1539 | 1535 | "license": "MIT", |
| 1540 | 1536 | "funding": { |
| 1541 | 1537 | "type": "github", |
@@ -1614,7 +1610,6 @@ | ||
| 1614 | 1610 | "version": "2.0.1", |
| 1615 | 1611 | "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", |
| 1616 | 1612 | "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", |
| 1617 | − "dev": true, | |
| 1618 | 1613 | "license": "MIT", |
| 1619 | 1614 | "funding": { |
| 1620 | 1615 | "type": "github", |
@@ -1625,7 +1620,6 @@ | ||
| 1625 | 1620 | "version": "2.0.2", |
| 1626 | 1621 | "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", |
| 1627 | 1622 | "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", |
| 1628 | − "dev": true, | |
| 1629 | 1623 | "license": "MIT", |
| 1630 | 1624 | "funding": { |
| 1631 | 1625 | "type": "github", |
@@ -1714,7 +1708,6 @@ | ||
| 1714 | 1708 | "version": "4.4.3", |
| 1715 | 1709 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", |
| 1716 | 1710 | "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", |
| 1717 | − "dev": true, | |
| 1718 | 1711 | "license": "MIT", |
| 1719 | 1712 | "dependencies": { |
| 1720 | 1713 | "ms": "^2.1.3" |
@@ -1732,7 +1725,6 @@ | ||
| 1732 | 1725 | "version": "1.3.0", |
| 1733 | 1726 | "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", |
| 1734 | 1727 | "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", |
| 1735 | − "dev": true, | |
| 1736 | 1728 | "license": "MIT", |
| 1737 | 1729 | "dependencies": { |
| 1738 | 1730 | "character-entities": "^2.0.0" |
@@ -1746,7 +1738,6 @@ | ||
| 1746 | 1738 | "version": "2.0.3", |
| 1747 | 1739 | "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", |
| 1748 | 1740 | "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", |
| 1749 | − "dev": true, | |
| 1750 | 1741 | "license": "MIT", |
| 1751 | 1742 | "engines": { |
| 1752 | 1743 | "node": ">=6" |
@@ -1756,7 +1747,6 @@ | ||
| 1756 | 1747 | "version": "1.1.0", |
| 1757 | 1748 | "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", |
| 1758 | 1749 | "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", |
| 1759 | − "dev": true, | |
| 1760 | 1750 | "license": "MIT", |
| 1761 | 1751 | "dependencies": { |
| 1762 | 1752 | "dequal": "^2.0.0" |
@@ -1859,6 +1849,18 @@ | ||
| 1859 | 1849 | "node": ">=6" |
| 1860 | 1850 | } |
| 1861 | 1851 | }, |
| 1852 | + "node_modules/escape-string-regexp": { | |
| 1853 | + "version": "5.0.0", | |
| 1854 | + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", | |
| 1855 | + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", | |
| 1856 | + "license": "MIT", | |
| 1857 | + "engines": { | |
| 1858 | + "node": ">=12" | |
| 1859 | + }, | |
| 1860 | + "funding": { | |
| 1861 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 1862 | + } | |
| 1863 | + }, | |
| 1862 | 1864 | "node_modules/estree-util-attach-comments": { |
| 1863 | 1865 | "version": "3.0.0", |
| 1864 | 1866 | "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", |
@@ -1961,7 +1963,6 @@ | ||
| 1961 | 1963 | "version": "3.0.2", |
| 1962 | 1964 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", |
| 1963 | 1965 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", |
| 1964 | − "dev": true, | |
| 1965 | 1966 | "license": "MIT" |
| 1966 | 1967 | }, |
| 1967 | 1968 | "node_modules/fancy-canvas": { |
@@ -2143,7 +2144,6 @@ | ||
| 2143 | 2144 | "version": "4.1.0", |
| 2144 | 2145 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", |
| 2145 | 2146 | "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", |
| 2146 | − "dev": true, | |
| 2147 | 2147 | "license": "MIT", |
| 2148 | 2148 | "engines": { |
| 2149 | 2149 | "node": ">=12" |
@@ -2197,7 +2197,6 @@ | ||
| 2197 | 2197 | "version": "3.1.0", |
| 2198 | 2198 | "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", |
| 2199 | 2199 | "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", |
| 2200 | − "dev": true, | |
| 2201 | 2200 | "license": "MIT", |
| 2202 | 2201 | "funding": { |
| 2203 | 2202 | "type": "github", |
@@ -2239,11 +2238,36 @@ | ||
| 2239 | 2238 | "url": "https://github.com/sponsors/sindresorhus" |
| 2240 | 2239 | } |
| 2241 | 2240 | }, |
| 2241 | + "node_modules/markdown-table": { | |
| 2242 | + "version": "3.0.4", | |
| 2243 | + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", | |
| 2244 | + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", | |
| 2245 | + "license": "MIT", | |
| 2246 | + "funding": { | |
| 2247 | + "type": "github", | |
| 2248 | + "url": "https://github.com/sponsors/wooorm" | |
| 2249 | + } | |
| 2250 | + }, | |
| 2251 | + "node_modules/mdast-util-find-and-replace": { | |
| 2252 | + "version": "3.0.2", | |
| 2253 | + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", | |
| 2254 | + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", | |
| 2255 | + "license": "MIT", | |
| 2256 | + "dependencies": { | |
| 2257 | + "@types/mdast": "^4.0.0", | |
| 2258 | + "escape-string-regexp": "^5.0.0", | |
| 2259 | + "unist-util-is": "^6.0.0", | |
| 2260 | + "unist-util-visit-parents": "^6.0.0" | |
| 2261 | + }, | |
| 2262 | + "funding": { | |
| 2263 | + "type": "opencollective", | |
| 2264 | + "url": "https://opencollective.com/unified" | |
| 2265 | + } | |
| 2266 | + }, | |
| 2242 | 2267 | "node_modules/mdast-util-from-markdown": { |
| 2243 | 2268 | "version": "2.0.3", |
| 2244 | 2269 | "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", |
| 2245 | 2270 | "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", |
| 2246 | − "dev": true, | |
| 2247 | 2271 | "license": "MIT", |
| 2248 | 2272 | "dependencies": { |
| 2249 | 2273 | "@types/mdast": "^4.0.0", |
@@ -2264,6 +2288,107 @@ | ||
| 2264 | 2288 | "url": "https://opencollective.com/unified" |
| 2265 | 2289 | } |
| 2266 | 2290 | }, |
| 2291 | + "node_modules/mdast-util-gfm": { | |
| 2292 | + "version": "3.1.0", | |
| 2293 | + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", | |
| 2294 | + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", | |
| 2295 | + "license": "MIT", | |
| 2296 | + "dependencies": { | |
| 2297 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2298 | + "mdast-util-gfm-autolink-literal": "^2.0.0", | |
| 2299 | + "mdast-util-gfm-footnote": "^2.0.0", | |
| 2300 | + "mdast-util-gfm-strikethrough": "^2.0.0", | |
| 2301 | + "mdast-util-gfm-table": "^2.0.0", | |
| 2302 | + "mdast-util-gfm-task-list-item": "^2.0.0", | |
| 2303 | + "mdast-util-to-markdown": "^2.0.0" | |
| 2304 | + }, | |
| 2305 | + "funding": { | |
| 2306 | + "type": "opencollective", | |
| 2307 | + "url": "https://opencollective.com/unified" | |
| 2308 | + } | |
| 2309 | + }, | |
| 2310 | + "node_modules/mdast-util-gfm-autolink-literal": { | |
| 2311 | + "version": "2.0.1", | |
| 2312 | + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", | |
| 2313 | + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", | |
| 2314 | + "license": "MIT", | |
| 2315 | + "dependencies": { | |
| 2316 | + "@types/mdast": "^4.0.0", | |
| 2317 | + "ccount": "^2.0.0", | |
| 2318 | + "devlop": "^1.0.0", | |
| 2319 | + "mdast-util-find-and-replace": "^3.0.0", | |
| 2320 | + "micromark-util-character": "^2.0.0" | |
| 2321 | + }, | |
| 2322 | + "funding": { | |
| 2323 | + "type": "opencollective", | |
| 2324 | + "url": "https://opencollective.com/unified" | |
| 2325 | + } | |
| 2326 | + }, | |
| 2327 | + "node_modules/mdast-util-gfm-footnote": { | |
| 2328 | + "version": "2.1.0", | |
| 2329 | + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", | |
| 2330 | + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", | |
| 2331 | + "license": "MIT", | |
| 2332 | + "dependencies": { | |
| 2333 | + "@types/mdast": "^4.0.0", | |
| 2334 | + "devlop": "^1.1.0", | |
| 2335 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2336 | + "mdast-util-to-markdown": "^2.0.0", | |
| 2337 | + "micromark-util-normalize-identifier": "^2.0.0" | |
| 2338 | + }, | |
| 2339 | + "funding": { | |
| 2340 | + "type": "opencollective", | |
| 2341 | + "url": "https://opencollective.com/unified" | |
| 2342 | + } | |
| 2343 | + }, | |
| 2344 | + "node_modules/mdast-util-gfm-strikethrough": { | |
| 2345 | + "version": "2.0.0", | |
| 2346 | + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", | |
| 2347 | + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", | |
| 2348 | + "license": "MIT", | |
| 2349 | + "dependencies": { | |
| 2350 | + "@types/mdast": "^4.0.0", | |
| 2351 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2352 | + "mdast-util-to-markdown": "^2.0.0" | |
| 2353 | + }, | |
| 2354 | + "funding": { | |
| 2355 | + "type": "opencollective", | |
| 2356 | + "url": "https://opencollective.com/unified" | |
| 2357 | + } | |
| 2358 | + }, | |
| 2359 | + "node_modules/mdast-util-gfm-table": { | |
| 2360 | + "version": "2.0.0", | |
| 2361 | + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", | |
| 2362 | + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", | |
| 2363 | + "license": "MIT", | |
| 2364 | + "dependencies": { | |
| 2365 | + "@types/mdast": "^4.0.0", | |
| 2366 | + "devlop": "^1.0.0", | |
| 2367 | + "markdown-table": "^3.0.0", | |
| 2368 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2369 | + "mdast-util-to-markdown": "^2.0.0" | |
| 2370 | + }, | |
| 2371 | + "funding": { | |
| 2372 | + "type": "opencollective", | |
| 2373 | + "url": "https://opencollective.com/unified" | |
| 2374 | + } | |
| 2375 | + }, | |
| 2376 | + "node_modules/mdast-util-gfm-task-list-item": { | |
| 2377 | + "version": "2.0.0", | |
| 2378 | + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", | |
| 2379 | + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", | |
| 2380 | + "license": "MIT", | |
| 2381 | + "dependencies": { | |
| 2382 | + "@types/mdast": "^4.0.0", | |
| 2383 | + "devlop": "^1.0.0", | |
| 2384 | + "mdast-util-from-markdown": "^2.0.0", | |
| 2385 | + "mdast-util-to-markdown": "^2.0.0" | |
| 2386 | + }, | |
| 2387 | + "funding": { | |
| 2388 | + "type": "opencollective", | |
| 2389 | + "url": "https://opencollective.com/unified" | |
| 2390 | + } | |
| 2391 | + }, | |
| 2267 | 2392 | "node_modules/mdast-util-mdx": { |
| 2268 | 2393 | "version": "3.0.0", |
| 2269 | 2394 | "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", |
@@ -2349,7 +2474,6 @@ | ||
| 2349 | 2474 | "version": "4.1.0", |
| 2350 | 2475 | "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", |
| 2351 | 2476 | "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", |
| 2352 | − "dev": true, | |
| 2353 | 2477 | "license": "MIT", |
| 2354 | 2478 | "dependencies": { |
| 2355 | 2479 | "@types/mdast": "^4.0.0", |
@@ -2386,7 +2510,6 @@ | ||
| 2386 | 2510 | "version": "2.1.2", |
| 2387 | 2511 | "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", |
| 2388 | 2512 | "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", |
| 2389 | − "dev": true, | |
| 2390 | 2513 | "license": "MIT", |
| 2391 | 2514 | "dependencies": { |
| 2392 | 2515 | "@types/mdast": "^4.0.0", |
@@ -2408,7 +2531,6 @@ | ||
| 2408 | 2531 | "version": "4.0.0", |
| 2409 | 2532 | "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", |
| 2410 | 2533 | "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", |
| 2411 | − "dev": true, | |
| 2412 | 2534 | "license": "MIT", |
| 2413 | 2535 | "dependencies": { |
| 2414 | 2536 | "@types/mdast": "^4.0.0" |
@@ -2422,7 +2544,6 @@ | ||
| 2422 | 2544 | "version": "4.0.2", |
| 2423 | 2545 | "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", |
| 2424 | 2546 | "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", |
| 2425 | − "dev": true, | |
| 2426 | 2547 | "funding": [ |
| 2427 | 2548 | { |
| 2428 | 2549 | "type": "GitHub Sponsors", |
@@ -2458,7 +2579,6 @@ | ||
| 2458 | 2579 | "version": "2.0.3", |
| 2459 | 2580 | "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", |
| 2460 | 2581 | "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", |
| 2461 | − "dev": true, | |
| 2462 | 2582 | "funding": [ |
| 2463 | 2583 | { |
| 2464 | 2584 | "type": "GitHub Sponsors", |
@@ -2489,6 +2609,127 @@ | ||
| 2489 | 2609 | "micromark-util-types": "^2.0.0" |
| 2490 | 2610 | } |
| 2491 | 2611 | }, |
| 2612 | + "node_modules/micromark-extension-gfm": { | |
| 2613 | + "version": "3.0.0", | |
| 2614 | + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", | |
| 2615 | + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", | |
| 2616 | + "license": "MIT", | |
| 2617 | + "dependencies": { | |
| 2618 | + "micromark-extension-gfm-autolink-literal": "^2.0.0", | |
| 2619 | + "micromark-extension-gfm-footnote": "^2.0.0", | |
| 2620 | + "micromark-extension-gfm-strikethrough": "^2.0.0", | |
| 2621 | + "micromark-extension-gfm-table": "^2.0.0", | |
| 2622 | + "micromark-extension-gfm-tagfilter": "^2.0.0", | |
| 2623 | + "micromark-extension-gfm-task-list-item": "^2.0.0", | |
| 2624 | + "micromark-util-combine-extensions": "^2.0.0", | |
| 2625 | + "micromark-util-types": "^2.0.0" | |
| 2626 | + }, | |
| 2627 | + "funding": { | |
| 2628 | + "type": "opencollective", | |
| 2629 | + "url": "https://opencollective.com/unified" | |
| 2630 | + } | |
| 2631 | + }, | |
| 2632 | + "node_modules/micromark-extension-gfm-autolink-literal": { | |
| 2633 | + "version": "2.1.0", | |
| 2634 | + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", | |
| 2635 | + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", | |
| 2636 | + "license": "MIT", | |
| 2637 | + "dependencies": { | |
| 2638 | + "micromark-util-character": "^2.0.0", | |
| 2639 | + "micromark-util-sanitize-uri": "^2.0.0", | |
| 2640 | + "micromark-util-symbol": "^2.0.0", | |
| 2641 | + "micromark-util-types": "^2.0.0" | |
| 2642 | + }, | |
| 2643 | + "funding": { | |
| 2644 | + "type": "opencollective", | |
| 2645 | + "url": "https://opencollective.com/unified" | |
| 2646 | + } | |
| 2647 | + }, | |
| 2648 | + "node_modules/micromark-extension-gfm-footnote": { | |
| 2649 | + "version": "2.1.0", | |
| 2650 | + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", | |
| 2651 | + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", | |
| 2652 | + "license": "MIT", | |
| 2653 | + "dependencies": { | |
| 2654 | + "devlop": "^1.0.0", | |
| 2655 | + "micromark-core-commonmark": "^2.0.0", | |
| 2656 | + "micromark-factory-space": "^2.0.0", | |
| 2657 | + "micromark-util-character": "^2.0.0", | |
| 2658 | + "micromark-util-normalize-identifier": "^2.0.0", | |
| 2659 | + "micromark-util-sanitize-uri": "^2.0.0", | |
| 2660 | + "micromark-util-symbol": "^2.0.0", | |
| 2661 | + "micromark-util-types": "^2.0.0" | |
| 2662 | + }, | |
| 2663 | + "funding": { | |
| 2664 | + "type": "opencollective", | |
| 2665 | + "url": "https://opencollective.com/unified" | |
| 2666 | + } | |
| 2667 | + }, | |
| 2668 | + "node_modules/micromark-extension-gfm-strikethrough": { | |
| 2669 | + "version": "2.1.0", | |
| 2670 | + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", | |
| 2671 | + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", | |
| 2672 | + "license": "MIT", | |
| 2673 | + "dependencies": { | |
| 2674 | + "devlop": "^1.0.0", | |
| 2675 | + "micromark-util-chunked": "^2.0.0", | |
| 2676 | + "micromark-util-classify-character": "^2.0.0", | |
| 2677 | + "micromark-util-resolve-all": "^2.0.0", | |
| 2678 | + "micromark-util-symbol": "^2.0.0", | |
| 2679 | + "micromark-util-types": "^2.0.0" | |
| 2680 | + }, | |
| 2681 | + "funding": { | |
| 2682 | + "type": "opencollective", | |
| 2683 | + "url": "https://opencollective.com/unified" | |
| 2684 | + } | |
| 2685 | + }, | |
| 2686 | + "node_modules/micromark-extension-gfm-table": { | |
| 2687 | + "version": "2.1.1", | |
| 2688 | + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", | |
| 2689 | + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", | |
| 2690 | + "license": "MIT", | |
| 2691 | + "dependencies": { | |
| 2692 | + "devlop": "^1.0.0", | |
| 2693 | + "micromark-factory-space": "^2.0.0", | |
| 2694 | + "micromark-util-character": "^2.0.0", | |
| 2695 | + "micromark-util-symbol": "^2.0.0", | |
| 2696 | + "micromark-util-types": "^2.0.0" | |
| 2697 | + }, | |
| 2698 | + "funding": { | |
| 2699 | + "type": "opencollective", | |
| 2700 | + "url": "https://opencollective.com/unified" | |
| 2701 | + } | |
| 2702 | + }, | |
| 2703 | + "node_modules/micromark-extension-gfm-tagfilter": { | |
| 2704 | + "version": "2.0.0", | |
| 2705 | + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", | |
| 2706 | + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", | |
| 2707 | + "license": "MIT", | |
| 2708 | + "dependencies": { | |
| 2709 | + "micromark-util-types": "^2.0.0" | |
| 2710 | + }, | |
| 2711 | + "funding": { | |
| 2712 | + "type": "opencollective", | |
| 2713 | + "url": "https://opencollective.com/unified" | |
| 2714 | + } | |
| 2715 | + }, | |
| 2716 | + "node_modules/micromark-extension-gfm-task-list-item": { | |
| 2717 | + "version": "2.1.0", | |
| 2718 | + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", | |
| 2719 | + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", | |
| 2720 | + "license": "MIT", | |
| 2721 | + "dependencies": { | |
| 2722 | + "devlop": "^1.0.0", | |
| 2723 | + "micromark-factory-space": "^2.0.0", | |
| 2724 | + "micromark-util-character": "^2.0.0", | |
| 2725 | + "micromark-util-symbol": "^2.0.0", | |
| 2726 | + "micromark-util-types": "^2.0.0" | |
| 2727 | + }, | |
| 2728 | + "funding": { | |
| 2729 | + "type": "opencollective", | |
| 2730 | + "url": "https://opencollective.com/unified" | |
| 2731 | + } | |
| 2732 | + }, | |
| 2492 | 2733 | "node_modules/micromark-extension-mdx-expression": { |
| 2493 | 2734 | "version": "3.0.1", |
| 2494 | 2735 | "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", |
@@ -2600,7 +2841,6 @@ | ||
| 2600 | 2841 | "version": "2.0.1", |
| 2601 | 2842 | "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", |
| 2602 | 2843 | "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", |
| 2603 | − "dev": true, | |
| 2604 | 2844 | "funding": [ |
| 2605 | 2845 | { |
| 2606 | 2846 | "type": "GitHub Sponsors", |
@@ -2622,7 +2862,6 @@ | ||
| 2622 | 2862 | "version": "2.0.1", |
| 2623 | 2863 | "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", |
| 2624 | 2864 | "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", |
| 2625 | − "dev": true, | |
| 2626 | 2865 | "funding": [ |
| 2627 | 2866 | { |
| 2628 | 2867 | "type": "GitHub Sponsors", |
@@ -2673,7 +2912,6 @@ | ||
| 2673 | 2912 | "version": "2.0.1", |
| 2674 | 2913 | "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", |
| 2675 | 2914 | "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", |
| 2676 | − "dev": true, | |
| 2677 | 2915 | "funding": [ |
| 2678 | 2916 | { |
| 2679 | 2917 | "type": "GitHub Sponsors", |
@@ -2694,7 +2932,6 @@ | ||
| 2694 | 2932 | "version": "2.0.1", |
| 2695 | 2933 | "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", |
| 2696 | 2934 | "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", |
| 2697 | − "dev": true, | |
| 2698 | 2935 | "funding": [ |
| 2699 | 2936 | { |
| 2700 | 2937 | "type": "GitHub Sponsors", |
@@ -2717,7 +2954,6 @@ | ||
| 2717 | 2954 | "version": "2.0.1", |
| 2718 | 2955 | "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", |
| 2719 | 2956 | "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", |
| 2720 | − "dev": true, | |
| 2721 | 2957 | "funding": [ |
| 2722 | 2958 | { |
| 2723 | 2959 | "type": "GitHub Sponsors", |
@@ -2740,7 +2976,6 @@ | ||
| 2740 | 2976 | "version": "2.1.1", |
| 2741 | 2977 | "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", |
| 2742 | 2978 | "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", |
| 2743 | − "dev": true, | |
| 2744 | 2979 | "funding": [ |
| 2745 | 2980 | { |
| 2746 | 2981 | "type": "GitHub Sponsors", |
@@ -2761,7 +2996,6 @@ | ||
| 2761 | 2996 | "version": "2.0.1", |
| 2762 | 2997 | "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", |
| 2763 | 2998 | "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", |
| 2764 | − "dev": true, | |
| 2765 | 2999 | "funding": [ |
| 2766 | 3000 | { |
| 2767 | 3001 | "type": "GitHub Sponsors", |
@@ -2781,7 +3015,6 @@ | ||
| 2781 | 3015 | "version": "2.0.1", |
| 2782 | 3016 | "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", |
| 2783 | 3017 | "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", |
| 2784 | − "dev": true, | |
| 2785 | 3018 | "funding": [ |
| 2786 | 3019 | { |
| 2787 | 3020 | "type": "GitHub Sponsors", |
@@ -2803,7 +3036,6 @@ | ||
| 2803 | 3036 | "version": "2.0.1", |
| 2804 | 3037 | "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", |
| 2805 | 3038 | "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", |
| 2806 | − "dev": true, | |
| 2807 | 3039 | "funding": [ |
| 2808 | 3040 | { |
| 2809 | 3041 | "type": "GitHub Sponsors", |
@@ -2824,7 +3056,6 @@ | ||
| 2824 | 3056 | "version": "2.0.2", |
| 2825 | 3057 | "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", |
| 2826 | 3058 | "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", |
| 2827 | − "dev": true, | |
| 2828 | 3059 | "funding": [ |
| 2829 | 3060 | { |
| 2830 | 3061 | "type": "GitHub Sponsors", |
@@ -2844,7 +3075,6 @@ | ||
| 2844 | 3075 | "version": "2.0.1", |
| 2845 | 3076 | "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", |
| 2846 | 3077 | "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", |
| 2847 | − "dev": true, | |
| 2848 | 3078 | "funding": [ |
| 2849 | 3079 | { |
| 2850 | 3080 | "type": "GitHub Sponsors", |
@@ -2867,7 +3097,6 @@ | ||
| 2867 | 3097 | "version": "2.0.1", |
| 2868 | 3098 | "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", |
| 2869 | 3099 | "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", |
| 2870 | − "dev": true, | |
| 2871 | 3100 | "funding": [ |
| 2872 | 3101 | { |
| 2873 | 3102 | "type": "GitHub Sponsors", |
@@ -2910,7 +3139,6 @@ | ||
| 2910 | 3139 | "version": "2.0.1", |
| 2911 | 3140 | "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", |
| 2912 | 3141 | "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", |
| 2913 | − "dev": true, | |
| 2914 | 3142 | "funding": [ |
| 2915 | 3143 | { |
| 2916 | 3144 | "type": "GitHub Sponsors", |
@@ -2927,7 +3155,6 @@ | ||
| 2927 | 3155 | "version": "2.0.1", |
| 2928 | 3156 | "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", |
| 2929 | 3157 | "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", |
| 2930 | − "dev": true, | |
| 2931 | 3158 | "funding": [ |
| 2932 | 3159 | { |
| 2933 | 3160 | "type": "GitHub Sponsors", |
@@ -2947,7 +3174,6 @@ | ||
| 2947 | 3174 | "version": "2.0.1", |
| 2948 | 3175 | "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", |
| 2949 | 3176 | "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", |
| 2950 | − "dev": true, | |
| 2951 | 3177 | "funding": [ |
| 2952 | 3178 | { |
| 2953 | 3179 | "type": "GitHub Sponsors", |
@@ -2967,7 +3193,6 @@ | ||
| 2967 | 3193 | "version": "2.0.1", |
| 2968 | 3194 | "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", |
| 2969 | 3195 | "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", |
| 2970 | − "dev": true, | |
| 2971 | 3196 | "funding": [ |
| 2972 | 3197 | { |
| 2973 | 3198 | "type": "GitHub Sponsors", |
@@ -2989,7 +3214,6 @@ | ||
| 2989 | 3214 | "version": "2.1.0", |
| 2990 | 3215 | "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", |
| 2991 | 3216 | "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", |
| 2992 | − "dev": true, | |
| 2993 | 3217 | "funding": [ |
| 2994 | 3218 | { |
| 2995 | 3219 | "type": "GitHub Sponsors", |
@@ -3012,7 +3236,6 @@ | ||
| 3012 | 3236 | "version": "2.0.1", |
| 3013 | 3237 | "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", |
| 3014 | 3238 | "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", |
| 3015 | − "dev": true, | |
| 3016 | 3239 | "funding": [ |
| 3017 | 3240 | { |
| 3018 | 3241 | "type": "GitHub Sponsors", |
@@ -3029,7 +3252,6 @@ | ||
| 3029 | 3252 | "version": "2.0.2", |
| 3030 | 3253 | "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", |
| 3031 | 3254 | "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", |
| 3032 | − "dev": true, | |
| 3033 | 3255 | "funding": [ |
| 3034 | 3256 | { |
| 3035 | 3257 | "type": "GitHub Sponsors", |
@@ -3046,7 +3268,6 @@ | ||
| 3046 | 3268 | "version": "2.1.3", |
| 3047 | 3269 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", |
| 3048 | 3270 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", |
| 3049 | − "dev": true, | |
| 3050 | 3271 | "license": "MIT" |
| 3051 | 3272 | }, |
| 3052 | 3273 | "node_modules/nanoid": { |
@@ -3361,6 +3582,24 @@ | ||
| 3361 | 3582 | "url": "https://opencollective.com/unified" |
| 3362 | 3583 | } |
| 3363 | 3584 | }, |
| 3585 | + "node_modules/remark-gfm": { | |
| 3586 | + "version": "4.0.1", | |
| 3587 | + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", | |
| 3588 | + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", | |
| 3589 | + "license": "MIT", | |
| 3590 | + "dependencies": { | |
| 3591 | + "@types/mdast": "^4.0.0", | |
| 3592 | + "mdast-util-gfm": "^3.0.0", | |
| 3593 | + "micromark-extension-gfm": "^3.0.0", | |
| 3594 | + "remark-parse": "^11.0.0", | |
| 3595 | + "remark-stringify": "^11.0.0", | |
| 3596 | + "unified": "^11.0.0" | |
| 3597 | + }, | |
| 3598 | + "funding": { | |
| 3599 | + "type": "opencollective", | |
| 3600 | + "url": "https://opencollective.com/unified" | |
| 3601 | + } | |
| 3602 | + }, | |
| 3364 | 3603 | "node_modules/remark-mdx": { |
| 3365 | 3604 | "version": "3.1.1", |
| 3366 | 3605 | "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", |
@@ -3380,7 +3619,6 @@ | ||
| 3380 | 3619 | "version": "11.0.0", |
| 3381 | 3620 | "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", |
| 3382 | 3621 | "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", |
| 3383 | − "dev": true, | |
| 3384 | 3622 | "license": "MIT", |
| 3385 | 3623 | "dependencies": { |
| 3386 | 3624 | "@types/mdast": "^4.0.0", |
@@ -3411,6 +3649,21 @@ | ||
| 3411 | 3649 | "url": "https://opencollective.com/unified" |
| 3412 | 3650 | } |
| 3413 | 3651 | }, |
| 3652 | + "node_modules/remark-stringify": { | |
| 3653 | + "version": "11.0.0", | |
| 3654 | + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", | |
| 3655 | + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", | |
| 3656 | + "license": "MIT", | |
| 3657 | + "dependencies": { | |
| 3658 | + "@types/mdast": "^4.0.0", | |
| 3659 | + "mdast-util-to-markdown": "^2.0.0", | |
| 3660 | + "unified": "^11.0.0" | |
| 3661 | + }, | |
| 3662 | + "funding": { | |
| 3663 | + "type": "opencollective", | |
| 3664 | + "url": "https://opencollective.com/unified" | |
| 3665 | + } | |
| 3666 | + }, | |
| 3414 | 3667 | "node_modules/rollup": { |
| 3415 | 3668 | "version": "4.62.4", |
| 3416 | 3669 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", |
@@ -3574,7 +3827,6 @@ | ||
| 3574 | 3827 | "version": "2.2.0", |
| 3575 | 3828 | "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", |
| 3576 | 3829 | "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", |
| 3577 | − "dev": true, | |
| 3578 | 3830 | "license": "MIT", |
| 3579 | 3831 | "funding": { |
| 3580 | 3832 | "type": "github", |
@@ -3585,7 +3837,6 @@ | ||
| 3585 | 3837 | "version": "11.0.5", |
| 3586 | 3838 | "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", |
| 3587 | 3839 | "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", |
| 3588 | − "dev": true, | |
| 3589 | 3840 | "license": "MIT", |
| 3590 | 3841 | "dependencies": { |
| 3591 | 3842 | "@types/unist": "^3.0.0", |
@@ -3605,7 +3856,6 @@ | ||
| 3605 | 3856 | "version": "6.0.1", |
| 3606 | 3857 | "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", |
| 3607 | 3858 | "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", |
| 3608 | − "dev": true, | |
| 3609 | 3859 | "license": "MIT", |
| 3610 | 3860 | "dependencies": { |
| 3611 | 3861 | "@types/unist": "^3.0.0" |
@@ -3647,7 +3897,6 @@ | ||
| 3647 | 3897 | "version": "4.0.0", |
| 3648 | 3898 | "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", |
| 3649 | 3899 | "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", |
| 3650 | − "dev": true, | |
| 3651 | 3900 | "license": "MIT", |
| 3652 | 3901 | "dependencies": { |
| 3653 | 3902 | "@types/unist": "^3.0.0" |
@@ -3661,7 +3910,6 @@ | ||
| 3661 | 3910 | "version": "5.1.0", |
| 3662 | 3911 | "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", |
| 3663 | 3912 | "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", |
| 3664 | − "dev": true, | |
| 3665 | 3913 | "license": "MIT", |
| 3666 | 3914 | "dependencies": { |
| 3667 | 3915 | "@types/unist": "^3.0.0", |
@@ -3677,7 +3925,6 @@ | ||
| 3677 | 3925 | "version": "6.0.2", |
| 3678 | 3926 | "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", |
| 3679 | 3927 | "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", |
| 3680 | − "dev": true, | |
| 3681 | 3928 | "license": "MIT", |
| 3682 | 3929 | "dependencies": { |
| 3683 | 3930 | "@types/unist": "^3.0.0", |
@@ -3723,7 +3970,6 @@ | ||
| 3723 | 3970 | "version": "6.0.3", |
| 3724 | 3971 | "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", |
| 3725 | 3972 | "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", |
| 3726 | − "dev": true, | |
| 3727 | 3973 | "license": "MIT", |
| 3728 | 3974 | "dependencies": { |
| 3729 | 3975 | "@types/unist": "^3.0.0", |
@@ -3738,7 +3984,6 @@ | ||
| 3738 | 3984 | "version": "4.0.3", |
| 3739 | 3985 | "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", |
| 3740 | 3986 | "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", |
| 3741 | − "dev": true, | |
| 3742 | 3987 | "license": "MIT", |
| 3743 | 3988 | "dependencies": { |
| 3744 | 3989 | "@types/unist": "^3.0.0", |
@@ -3835,7 +4080,6 @@ | ||
| 3835 | 4080 | "version": "2.0.4", |
| 3836 | 4081 | "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", |
| 3837 | 4082 | "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", |
| 3838 | − "dev": true, | |
| 3839 | 4083 | "license": "MIT", |
| 3840 | 4084 | "funding": { |
| 3841 | 4085 | "type": "github", |
modified
hfmarketdata/web/package.json
+9 −3
@@ -7,9 +7,14 @@ | ||
| 7 | 7 | "type": "module", |
| 8 | 8 | "scripts": { |
| 9 | 9 | "dev": "vite", |
| 10 | − "build": "vite build", | |
| 10 | + "build": "vite build && node scripts/prerender.mjs", | |
| 11 | + "build:spa": "vite build", | |
| 11 | 12 | "preview": "vite preview", |
| 12 | − "test:e2e": "playwright test" | |
| 13 | + "examples": "node scripts/fetch-examples.mjs", | |
| 14 | + "sample:aapl-pe": "node scripts/make-aapl-pe-sample.mjs", | |
| 15 | + "bench:formats": "node scripts/bench-formats.mjs", | |
| 16 | + "test:e2e": "playwright test", | |
| 17 | + "test:e2e:ui": "playwright test --ui" | |
| 13 | 18 | }, |
| 14 | 19 | "dependencies": { |
| 15 | 20 | "@mdx-js/react": "^3.1.1", |
@@ -17,7 +22,8 @@ | ||
| 17 | 22 | "prism-react-renderer": "^2.4.0", |
| 18 | 23 | "react": "^18.3.1", |
| 19 | 24 | "react-dom": "^18.3.1", |
| 20 | − "react-router-dom": "^6.28.0" | |
| 25 | + "react-router-dom": "^6.28.0", | |
| 26 | + "remark-gfm": "^4.0.1" | |
| 21 | 27 | }, |
| 22 | 28 | "devDependencies": { |
| 23 | 29 | "@mdx-js/rollup": "^3.1.0", |
added
hfmarketdata/web/scripts/bench-formats.mjs
+26 −0
@@ -0,0 +1,26 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// Measure real response sizes (JSON vs CSV, raw and gzip) for the Data formats guide. Writes CSVs to a temp dir so | |
| 3 | +// scripts/bench-parquet.py can measure the Parquet equivalent. Usage: node scripts/bench-formats.mjs [outdir] | |
| 4 | +import { gzipSync } from 'node:zlib' | |
| 5 | +import { mkdirSync, writeFileSync } from 'node:fs' | |
| 6 | +import { join } from 'node:path' | |
| 7 | + | |
| 8 | +const BASE = (process.env.HFMD_BASE || 'https://www.hfmarketdata.io').replace(/\/$/, '') | |
| 9 | +const OUT = process.argv[2] || '/tmp/hfmd-bench' | |
| 10 | +const CASES = [ | |
| 11 | + ['aapl_1min_week', '/v1/bars/stock/AAPL?timeframe=1min&start=2024-06-03&end=2024-06-08&limit=50000'], | |
| 12 | + ['aapl_1day_all', '/v1/bars/stock/AAPL?timeframe=1day&limit=50000'], | |
| 13 | + ['aapl_chain_day', '/v1/options/chain/AAPL?trade_date=2024-06-21&limit=20000'], | |
| 14 | +] | |
| 15 | +mkdirSync(OUT, { recursive: true }) | |
| 16 | +for (const [name, path] of CASES) { | |
| 17 | + const t0 = Date.now() | |
| 18 | + const json = await (await fetch(BASE + path)).text() | |
| 19 | + const tj = Date.now() - t0 | |
| 20 | + const t1 = Date.now() | |
| 21 | + const csv = await (await fetch(`${BASE + path}&format=csv`)).text() | |
| 22 | + const tc = Date.now() - t1 | |
| 23 | + writeFileSync(join(OUT, `${name}.csv`), csv) | |
| 24 | + const rows = JSON.parse(json).count | |
| 25 | + console.log(JSON.stringify({ name, rows, json: json.length, json_gz: gzipSync(json).length, csv: csv.length, csv_gz: gzipSync(csv).length, ms_json: tj, ms_csv: tc })) | |
| 26 | +} | |
added
hfmarketdata/web/scripts/bench-parquet.py
+18 −0
@@ -0,0 +1,18 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +"""Convert the CSVs written by bench-formats.mjs to Parquet (zstd, the API's codec) and print sizes. | |
| 3 | + | |
| 4 | +Usage: python3 scripts/bench-parquet.py [/tmp/hfmd-bench] | |
| 5 | +""" | |
| 6 | +import glob | |
| 7 | +import io | |
| 8 | +import os | |
| 9 | +import sys | |
| 10 | + | |
| 11 | +import pandas as pd | |
| 12 | + | |
| 13 | +out = sys.argv[1] if len(sys.argv) > 1 else "/tmp/hfmd-bench" | |
| 14 | +for path in sorted(glob.glob(os.path.join(out, "*.csv"))): | |
| 15 | + df = pd.read_csv(path) | |
| 16 | + buf = io.BytesIO() | |
| 17 | + df.to_parquet(buf, compression="zstd", index=False) | |
| 18 | + print(f"{os.path.basename(path)[:-4]} rows={len(df)} parquet_zstd={buf.getbuffer().nbytes}") | |
added
hfmarketdata/web/scripts/fetch-examples.mjs
+141 −0
@@ -0,0 +1,141 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// Capture one real response per public GET operation whose spec has no example, into content/examples/<operationId>.json. | |
| 3 | +// Also refreshes content/openapi.snapshot.json (bundled fallback when /openapi.json is unreachable at runtime). | |
| 4 | +// Usage: node scripts/fetch-examples.mjs [--base https://www.hfmarketdata.io] [--only opId,opId] [--force] | |
| 5 | +// Idempotent and network-tolerant: failures are reported, never fatal. Commit the resulting files. | |
| 6 | +import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs' | |
| 7 | +import { dirname, join, resolve } from 'node:path' | |
| 8 | +import { fileURLToPath } from 'node:url' | |
| 9 | + | |
| 10 | +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') | |
| 11 | +const OUT = join(ROOT, 'content', 'examples') | |
| 12 | +const args = process.argv.slice(2) | |
| 13 | +const flag = (name, def) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : def } | |
| 14 | +const BASE = (flag('--base', process.env.HFMD_BASE || 'https://www.hfmarketdata.io')).replace(/\/$/, '') | |
| 15 | +const ONLY = flag('--only', '')?.split(',').filter(Boolean) | |
| 16 | +const FORCE = args.includes('--force') | |
| 17 | +const MAX_ROWS = 5 | |
| 18 | +const PUBLIC_TAGS = new Set(['meta', 'bars', 'futures', 'options', 'fundamentals']) | |
| 19 | + | |
| 20 | +// Keep in sync with src/docs/enrich.js (duplicated on purpose: this script must not import app code). | |
| 21 | +const NAME_DEFAULTS = { | |
| 22 | + asset: 'stock', ticker: 'AAPL', tickers: 'AAPL,MSFT,NVDA', root: 'ES', symbol: 'ESZ25', concept: 'Assets', | |
| 23 | + timeframe: '1day', interval: '1d', start: '2024-06-03', end: '2024-06-07', from: '2024-01-01', to: '2024-06-30', | |
| 24 | + as_of: '2024-06-03', at: '2024-06-03 10:35:00', trade_date: '2024-06-21', expiry: '2024-12-20', strike: '200', call_put: 'c', | |
| 25 | + quarter: '2024_q4', year: '2024', roll: 'volume', adjust: 'back_adjusted', depth: '1', session: 'all', format: 'json', | |
| 26 | + limit: '5', order: 'asc', search: 'AAP', period: 'quarterly', fiscal_year: '2024', fiscal_period: 'Q2', statement: 'balance_sheet', | |
| 27 | + form: '10-Q', range: '24h', | |
| 28 | +} | |
| 29 | +const PATH_OVERRIDES = { | |
| 30 | + 'GET /v1/bars/{asset}/{ticker}': { asset: 'stock', ticker: 'AAPL', timeframe: '1day', start: '2024-06-03', limit: '5' }, | |
| 31 | + 'GET /v1/bars/{asset}': { asset: 'stock', tickers: 'AAPL,MSFT,NVDA', timeframe: '1min', start: '2024-06-03 09:30:00', end: '2024-06-03 09:35:00', limit: '3' }, | |
| 32 | + 'GET /v1/snapshot/{asset}': { asset: 'stock', tickers: 'AAPL,MSFT,NVDA', at: '2024-06-03 10:35:00', timeframe: '1min' }, | |
| 33 | + 'GET /v1/{asset}/tickers': { asset: 'stock', timeframe: '1day', search: 'AAP', limit: '10' }, | |
| 34 | + 'GET /v1/options/chain/{ticker}': { ticker: 'AAPL', trade_date: '2024-06-21', call_put: 'c', strike_min: '200', strike_max: '210', limit: '5' }, | |
| 35 | + 'GET /v1/options/expirations/{ticker}': { ticker: 'AAPL', trade_date: '2024-06-21' }, | |
| 36 | + 'GET /v1/options/history/{ticker}': { ticker: 'AAPL', strike: '200', expiry: '2024-12-20', call_put: 'c', limit: '5' }, | |
| 37 | + 'GET /v1/options/tickers': { search: 'TSL', limit: '10' }, | |
| 38 | + 'GET /v1/futures/{root}/contracts': { root: 'ES' }, | |
| 39 | + 'GET /v1/futures/contract/{symbol}/bars': { symbol: 'ESZ25', interval: '1d', from: '2025-06-01', to: '2025-06-30', limit: '5' }, | |
| 40 | + 'GET /v1/futures/contract/{symbol}/coverage': { symbol: 'ESZ25' }, | |
| 41 | + 'GET /v1/futures/{root}/chain': { root: 'CL', as_of: '2024-06-03' }, | |
| 42 | + 'GET /v1/futures/{root}/continuous': { root: 'ES', roll: 'volume', adjust: 'back_adjusted', depth: '1', interval: '1d', from: '2024-01-01', to: '2024-03-31', limit: '5' }, | |
| 43 | + 'GET /v1/futures/{root}/term-structure': { root: 'CL', as_of: '2024-06-03' }, | |
| 44 | + 'GET /v1/fundamentals/{ticker}/statements': { ticker: 'AAPL', statement: 'balance_sheet', period: 'quarterly', from: '2024-01-01', to: '2024-06-30' }, | |
| 45 | + 'GET /v1/fundamentals/{ticker}/facts/{concept}': { ticker: 'AAPL', concept: 'Assets', from: '2023-01-01' }, | |
| 46 | + 'GET /v1/fundamentals/{ticker}/ratios': { ticker: 'AAPL', period: 'quarterly', from: '2024-01-01' }, | |
| 47 | + 'GET /v1/fundamentals/{ticker}/ratios/daily': { ticker: 'AAPL', from: '2024-01-01', to: '2024-06-30', limit: '5' }, | |
| 48 | + 'GET /v1/fundamentals/{ticker}/filings': { ticker: 'AAPL', form: '10-Q', from: '2024-01-01' }, | |
| 49 | + 'GET /v1/fundamentals/{ticker}/coverage': { ticker: 'AAPL' }, | |
| 50 | + 'GET /v1/fundamentals/screener': { limit: '10' }, | |
| 51 | + 'GET /v1/fundamentals/frames/{concept}': { concept: 'Assets', fiscal_year: '2024', fiscal_period: 'Q2' }, | |
| 52 | +} | |
| 53 | +const SHOW_OPTIONAL = new Set(['timeframe', 'interval', 'start', 'end', 'from', 'to', 'limit', 'as_of', 'roll', 'adjust', 'trade_date', 'search', 'period', 'statement', 'form', 'fiscal_year', 'fiscal_period', 'call_put', 'strike_min', 'strike_max', 'at', 'tickers', 'depth']) | |
| 54 | + | |
| 55 | +function exampleParams(op, path, method) { | |
| 56 | + const over = PATH_OVERRIDES[`${method} ${path}`] || {} | |
| 57 | + const values = {} | |
| 58 | + for (const p of op.parameters || []) { | |
| 59 | + if (p.in === 'header' || p.in === 'cookie' || p.name === 'api_key') continue | |
| 60 | + const schema = p.schema || {} | |
| 61 | + let v = over[p.name] | |
| 62 | + if (v === undefined && (p.required || SHOW_OPTIONAL.has(p.name))) v = NAME_DEFAULTS[p.name] ?? schema.default ?? (schema.enum ? schema.enum[0] : undefined) | |
| 63 | + if (v === undefined || v === '' || v === null) continue | |
| 64 | + values[p.name] = String(v) | |
| 65 | + } | |
| 66 | + return values | |
| 67 | +} | |
| 68 | + | |
| 69 | +function buildUrl(path, values, params) { | |
| 70 | + let p = path | |
| 71 | + const pathParams = new Set((params || []).filter(x => x.in === 'path').map(x => x.name)) | |
| 72 | + for (const n of pathParams) p = p.replace(`{${n}}`, encodeURIComponent(values[n] ?? '')) | |
| 73 | + const q = Object.entries(values).filter(([k]) => !pathParams.has(k)).map(([k, v]) => `${k}=${encodeURIComponent(v).replace(/%2C/g, ',').replace(/%3A/g, ':')}`) | |
| 74 | + return BASE + p + (q.length ? `?${q.join('&')}` : '') | |
| 75 | +} | |
| 76 | + | |
| 77 | +function hasExample(op) { | |
| 78 | + for (const [code, r] of Object.entries(op.responses || {})) { | |
| 79 | + if (!code.startsWith('2')) continue | |
| 80 | + const media = r.content?.['application/json'] | |
| 81 | + if (!media) continue | |
| 82 | + if (media.example !== undefined || media.examples || media.schema?.example !== undefined || media.schema?.examples?.length) return true | |
| 83 | + } | |
| 84 | + return false | |
| 85 | +} | |
| 86 | + | |
| 87 | +function trim(body) { | |
| 88 | + if (body && typeof body === 'object' && Array.isArray(body.data) && body.data.length > MAX_ROWS) { | |
| 89 | + return { ...body, data: body.data.slice(0, MAX_ROWS), _truncated: `${body.data.length - MAX_ROWS} more rows omitted in this example` } | |
| 90 | + } | |
| 91 | + for (const k of ['tickers', 'quarters', 'expirations', 'contracts', 'roots']) { | |
| 92 | + if (body && Array.isArray(body[k]) && body[k].length > 12) return { ...body, [k]: [...body[k].slice(0, 12), `… ${body[k].length - 12} more`] } | |
| 93 | + } | |
| 94 | + return body | |
| 95 | +} | |
| 96 | + | |
| 97 | +async function main() { | |
| 98 | + mkdirSync(OUT, { recursive: true }) | |
| 99 | + const res = await fetch(`${BASE}/openapi.json`, { headers: { Accept: 'application/json' } }) | |
| 100 | + if (!res.ok) throw new Error(`openapi.json → ${res.status}`) | |
| 101 | + const spec = await res.json() | |
| 102 | + const snapPath = join(ROOT, 'content', 'openapi.snapshot.json') | |
| 103 | + // Only replace the snapshot when the live spec is at least as rich (has the Error schema) — the local | |
| 104 | + // enriched build may be ahead of production during the v2 rollout. | |
| 105 | + if (spec.components?.schemas?.Error || !existsSync(snapPath)) writeFileSync(snapPath, JSON.stringify(spec, null, 1)) | |
| 106 | + else console.log('snapshot kept (live spec lacks components.schemas.Error — production still v1)') | |
| 107 | + | |
| 108 | + let ok = 0, skipped = 0, failed = 0 | |
| 109 | + for (const [path, item] of Object.entries(spec.paths)) { | |
| 110 | + const op = item.get | |
| 111 | + if (!op) continue | |
| 112 | + const id = op.operationId | |
| 113 | + const tag = op.tags?.[0] | |
| 114 | + if (!PUBLIC_TAGS.has(tag) || /stream/.test(path)) { skipped++; continue } | |
| 115 | + if (ONLY.length && !ONLY.includes(id)) continue | |
| 116 | + const file = join(OUT, `${id}.json`) | |
| 117 | + if (hasExample(op) && !FORCE) { skipped++; continue } | |
| 118 | + if (existsSync(file) && !FORCE) { | |
| 119 | + const prev = JSON.parse(readFileSync(file, 'utf8')) | |
| 120 | + if (prev.status === 200) { skipped++; continue } | |
| 121 | + } | |
| 122 | + const values = exampleParams(op, path, 'GET') | |
| 123 | + const url = buildUrl(path, values, op.parameters) | |
| 124 | + try { | |
| 125 | + const r = await fetch(url, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(30000) }) | |
| 126 | + const ct = r.headers.get('content-type') || '' | |
| 127 | + const body = ct.includes('json') ? await r.json() : await r.text() | |
| 128 | + const headers = {} | |
| 129 | + for (const h of ['x-ratelimit-limit-requests', 'x-ratelimit-remaining-requests', 'x-ratelimit-limit-rows', 'x-ratelimit-remaining-rows', 'x-ratelimit-reset', 'x-row-count', 'etag', 'content-type']) if (r.headers.get(h)) headers[h] = r.headers.get(h) | |
| 130 | + if (!r.ok) { console.log(`✗ ${id} ${r.status} ${url}`); failed++; continue } | |
| 131 | + writeFileSync(file, JSON.stringify({ operationId: id, method: 'GET', path, params: values, url, status: r.status, headers, captured_at: new Date().toISOString(), body: trim(body) }, null, 2)) | |
| 132 | + console.log(`✓ ${id} ${r.status} ${url}`) | |
| 133 | + ok++ | |
| 134 | + } catch (e) { | |
| 135 | + console.log(`✗ ${id} ${e.message} ${url}`); failed++ | |
| 136 | + } | |
| 137 | + } | |
| 138 | + console.log(`\n${ok} captured · ${skipped} skipped · ${failed} failed → ${OUT}`) | |
| 139 | +} | |
| 140 | + | |
| 141 | +main().catch(e => { console.error(e); process.exit(1) }) | |
added
hfmarketdata/web/scripts/make-aapl-pe-sample.mjs
+51 −0
@@ -0,0 +1,51 @@ | ||
| 1 | +#!/usr/bin/env node | |
| 2 | +// Build content/samples/aapl-pe.json — the static fallback for the homepage P/E chart when | |
| 3 | +// /v1/fundamentals/AAPL/ratios/daily is not live. Prices are REAL (split-adjusted daily closes from the API); | |
| 4 | +// the denominator is Apple's reported fiscal-year diluted EPS (10-K, split-adjusted), stepped at each 10-K release. | |
| 5 | +// The output carries a `note` describing exactly this so nothing is presented as a computed API ratio. | |
| 6 | +import { mkdirSync, writeFileSync } from 'node:fs' | |
| 7 | +import { dirname, join, resolve } from 'node:path' | |
| 8 | +import { fileURLToPath } from 'node:url' | |
| 9 | + | |
| 10 | +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') | |
| 11 | +const BASE = (process.env.HFMD_BASE || 'https://www.hfmarketdata.io').replace(/\/$/, '') | |
| 12 | + | |
| 13 | +// Fiscal-year diluted EPS as reported in Apple's Form 10-K (USD, adjusted for the 4-for-1 split of 2020-08-31), | |
| 14 | +// applied from the release date of that 10-K's earnings. | |
| 15 | +const EPS_STEPS = [ | |
| 16 | + ['2014-10-20', 1.61], ['2015-10-27', 2.31], ['2016-10-25', 2.08], ['2017-11-02', 2.30], ['2018-11-01', 2.98], | |
| 17 | + ['2019-10-30', 2.97], ['2020-10-29', 3.28], ['2021-10-28', 5.61], ['2022-10-27', 6.11], ['2023-11-02', 6.13], | |
| 18 | + ['2024-10-31', 6.08], ['2025-10-30', 7.46], | |
| 19 | +] | |
| 20 | + | |
| 21 | +function epsAt(date) { | |
| 22 | + let eps = EPS_STEPS[0][1] | |
| 23 | + for (const [d, v] of EPS_STEPS) if (d <= date) eps = v | |
| 24 | + return eps | |
| 25 | +} | |
| 26 | + | |
| 27 | +async function main() { | |
| 28 | + const url = `${BASE}/v1/bars/stock/AAPL?timeframe=1day&adjustment=adj_split&start=2015-01-01&limit=50000` | |
| 29 | + const res = await fetch(url, { headers: { Accept: 'application/json' } }) | |
| 30 | + if (!res.ok) throw new Error(`${res.status} ${url}`) | |
| 31 | + const { data } = await res.json() | |
| 32 | + // Weekly sampling (Fridays, else last bar of the week) keeps the file small (~600 points). | |
| 33 | + const byWeek = new Map() | |
| 34 | + for (const row of data) { | |
| 35 | + const d = new Date(row.datetime + 'T00:00:00Z') | |
| 36 | + const monday = new Date(d); monday.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7)) | |
| 37 | + byWeek.set(monday.toISOString().slice(0, 10), row) | |
| 38 | + } | |
| 39 | + const points = [...byWeek.values()].map(r => ({ date: r.datetime.slice(0, 10), close: r.close, eps_ttm: epsAt(r.datetime.slice(0, 10)), pe: +(r.close / epsAt(r.datetime.slice(0, 10))).toFixed(2) })) | |
| 40 | + const out = { | |
| 41 | + ticker: 'AAPL', metric: 'pe', | |
| 42 | + note: 'Illustrative fallback used only when /v1/fundamentals/AAPL/ratios/daily is not live: split-adjusted weekly close (HF Market Data, adj_split) divided by the diluted EPS reported in Apple\'s latest Form 10-K at that date (10-K EPS stepped at each release; not a trailing-twelve-month series).', | |
| 43 | + source: { prices: url, eps: 'Apple Inc. Form 10-K filings FY2014–FY2025 (diluted EPS, split-adjusted)' }, | |
| 44 | + generated_at: new Date().toISOString(), points, | |
| 45 | + } | |
| 46 | + mkdirSync(join(ROOT, 'content', 'samples'), { recursive: true }) | |
| 47 | + writeFileSync(join(ROOT, 'content', 'samples', 'aapl-pe.json'), JSON.stringify(out)) | |
| 48 | + console.log(`${points.length} weekly points → content/samples/aapl-pe.json (${points[0].date} → ${points.at(-1).date})`) | |
| 49 | +} | |
| 50 | + | |
| 51 | +main().catch(e => { console.error(e); process.exit(1) }) | |
modified
hfmarketdata/web/src/components/components.css
+7 −5
@@ -1,10 +1,11 @@ | ||
| 1 | 1 | /* Shared components: Code, Tabs, Callout, Badge, LangSelector, SearchDialog, Markdown prose. Owner: web-core. */ |
| 2 | 2 | |
| 3 | 3 | /* Code */ |
| 4 | −.code { border: 1px solid var(--line); border-radius: var(--radius); background: var(--code-bg); overflow: hidden; margin: 0 0 16px; } | |
| 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; } | |
| 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; } | |
| 6 | 6 | .code-title { font-family: var(--mono); font-size: 12px; color: var(--fg-2); letter-spacing: .02em; } |
| 7 | −.code-pre { margin: 0; border: 0; border-radius: 0; background: transparent; padding: 12px 14px; font-size: 13px; line-height: 1.55; tab-size: 2; } | |
| 7 | +/* contain: inline-size → the (scrolling) pre never inflates the intrinsic width of grid/flex ancestors on narrow screens */ | |
| 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; } | |
| 8 | 9 | .code-line { display: block; white-space: pre; } |
| 9 | 10 | .code-ln { display: inline-block; width: 2.4em; color: var(--fg-3); user-select: none; text-align: right; padding-right: 1.2em; } |
| 10 | 11 | .copy-btn { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 8px; border-radius: 6px; border: 1px solid transparent; background: transparent; color: var(--fg-2); cursor: pointer; font-size: 12px; } |
@@ -27,14 +28,15 @@ | ||
| 27 | 28 | .lang-btn { border: 0; background: transparent; color: var(--fg-2); font-size: 12.5px; padding: 3px 9px; border-radius: 5px; cursor: pointer; font-weight: 500; } |
| 28 | 29 | .lang-btn:hover { color: var(--fg); } |
| 29 | 30 | .lang-btn.active { background: var(--bg); color: var(--fg); box-shadow: 0 1px 2px rgba(0, 0, 0, .25); } |
| 30 | −.codetabs { margin: 0 0 16px; } | |
| 31 | −.codetabs-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; } | |
| 31 | +.codetabs { margin: 0 0 16px; min-width: 0; max-width: 100%; } | |
| 32 | +.codetabs-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; } | |
| 32 | 33 | .codetabs .code { margin-bottom: 0; } |
| 33 | 34 | |
| 34 | 35 | /* Callout */ |
| 35 | 36 | .callout { display: flex; gap: 12px; padding: 14px 16px; border-radius: var(--radius); border: 1px solid var(--line); background: var(--bg-1); margin: 0 0 18px; border-left-width: 3px; } |
| 36 | 37 | .callout-icon { flex: none; display: inline-grid; place-items: center; width: 22px; height: 22px; margin-top: 1px; } |
| 37 | 38 | .callout-icon svg { width: 18px; height: 18px; } |
| 39 | +.callout-body { min-width: 0; overflow-wrap: anywhere; } | |
| 38 | 40 | .callout-title { font-weight: 650; margin-bottom: 3px; } |
| 39 | 41 | .callout-body p:last-child { margin-bottom: 0; } |
| 40 | 42 | .callout-info { border-left-color: var(--accent-2); } .callout-info .callout-icon { color: var(--accent-2); } |
modified
hfmarketdata/web/src/docs/spec.js
+2 −2
@@ -128,9 +128,9 @@ export function paramInfo(spec, p) { | ||
| 128 | 128 | if (max !== undefined) constraints.push(`≤ ${max}`) |
| 129 | 129 | if (minLen !== undefined) constraints.push(`min length ${minLen}`) |
| 130 | 130 | if (maxLen !== undefined) constraints.push(`max length ${maxLen}`) |
| 131 | − if (pattern) constraints.push(`pattern ${pattern}`) | |
| 132 | − if (format) constraints.push(format) | |
| 133 | 131 | const en = pick(schema, 'enum') || enumFromPattern(pattern) |
| 132 | + if (pattern && !(en && !pick(schema, 'enum'))) constraints.push(`pattern ${pattern}`) // hide patterns shown as choices | |
| 133 | + if (format) constraints.push(format) | |
| 134 | 134 | return { |
| 135 | 135 | name: p.name, in: p.in || 'query', required: !!p.required, |
| 136 | 136 | type: schemaType(schema), default: pick(schema, 'default'), enum: en || null, |
modified
hfmarketdata/web/src/pages/docs/docs.css
+2 −2
@@ -27,7 +27,7 @@ | ||
| 27 | 27 | .docs-crumbs a { color: var(--fg-2); } |
| 28 | 28 | .docs-crumbs svg { width: 12px; height: 12px; } |
| 29 | 29 | .docs-panel { border-left: 1px solid var(--line); background: var(--bg-1); padding: 26px 22px 60px; } |
| 30 | −.docs-panel-inner { position: sticky; top: calc(var(--topbar-h) + 18px); display: grid; gap: 18px; max-height: calc(100dvh - var(--topbar-h) - 36px); overflow-y: auto; scrollbar-width: thin; padding-right: 2px; } | |
| 30 | +.docs-panel-inner { position: sticky; top: calc(var(--topbar-h) + 18px); display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; gap: 18px; max-height: calc(100dvh - var(--topbar-h) - 36px); overflow-y: auto; scrollbar-width: thin; padding-right: 2px; } | |
| 31 | 31 | .docs-panel .code { background: var(--code-bg); } |
| 32 | 32 | .docs-panel h4 { font-size: 11.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-3); margin: 0 0 8px; } |
| 33 | 33 | .panel-block { display: grid; gap: 6px; } |
@@ -41,7 +41,7 @@ | ||
| 41 | 41 | .docs-prevnext .next { text-align: right; } |
| 42 | 42 | |
| 43 | 43 | /* prose (MDX + markdown descriptions) */ |
| 44 | −.prose { font-size: 15.5px; line-height: 1.7; color: var(--fg); } | |
| 44 | +.prose { font-size: 15.5px; line-height: 1.7; color: var(--fg); overflow-wrap: anywhere; min-width: 0; } | |
| 45 | 45 | .prose > p:first-of-type { color: var(--fg-1); } |
| 46 | 46 | .prose h2 { margin-top: 2.2em; padding-top: .3em; font-size: 22px; } |
| 47 | 47 | .prose h3 { margin-top: 1.8em; font-size: 17px; } |
modified
hfmarketdata/web/vite.config.js
+3 −1
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path' | ||
| 3 | 3 | import { defineConfig } from 'vite' |
| 4 | 4 | import react from '@vitejs/plugin-react' |
| 5 | 5 | import mdx from '@mdx-js/rollup' |
| 6 | +import remarkGfm from 'remark-gfm' | |
| 6 | 7 | |
| 7 | 8 | const CONTENT = resolve(__dirname, 'content') |
| 8 | 9 | |
@@ -53,10 +54,11 @@ function guidesIndexPlugin() { | ||
| 53 | 54 | |
| 54 | 55 | // Dev proxy → local API (uvicorn on :8090). In production the API serves dist/ itself (SPA fallback in main.py). |
| 55 | 56 | export default defineConfig({ |
| 56 | − plugins: [guidesIndexPlugin(), { enforce: 'pre', ...mdx({ providerImportSource: '@mdx-js/react' }) }, react()], | |
| 57 | + plugins: [guidesIndexPlugin(), { enforce: 'pre', ...mdx({ providerImportSource: '@mdx-js/react', remarkPlugins: [remarkGfm] }) }, react()], | |
| 57 | 58 | build: { |
| 58 | 59 | target: 'es2020', |
| 59 | 60 | cssCodeSplit: true, |
| 61 | + manifest: true, // read by scripts/prerender.mjs to preload each route's chunk graph | |
| 60 | 62 | rollupOptions: { |
| 61 | 63 | output: { |
| 62 | 64 | manualChunks(id) { |
| 63 | 65 | |