SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

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>
Simon-Pierre Boucher committed 20 days ago (Sep 5, 2026) parent 3d8729d

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