SPB Git

spb/hfmarketdata

Public

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

web: refonte design — étape 1 (tokens, composants unifiés, home, docs, playground, status, légal, 404, prerender, e2e)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 6, 2026) parent 02b779a Browse files

81 changed files +2,909 −2,351

81 changed files
modified hfmarketdata/web/README.md +124 −73
@@ -1,8 +1,8 @@
1 1 # hfmarketdata/web — site, docs, playground, dashboard
2 2
3 3 Vite 6 + React 18 + react-router 6. Dark by default, light theme persisted. No CSS framework, no external
4 −requests at runtime (system font stacks, inline SVG, all JS bundled). Served by the FastAPI app from `dist/`
5 −(SPA fallback in `api/main.py`), or by `vite preview` for E2E.
4 +requests at runtime (system sans stack, self-hosted JetBrains Mono, inline SVG, all JS bundled). Served by the FastAPI
5 +app from `dist/` (SPA fallback in `api/main.py`), or by `vite preview` for E2E.
6 6
7 7 ```
8 8 npm install
@@ -10,57 +10,103 @@ npm run dev # http://localhost:5173 — proxies /v1, /health, /openapi.
10 10 npm run build # vite build + scripts/prerender.mjs → dist/
11 11 npm run test:e2e # Playwright against `vite preview` (build first)
12 12 npm run examples # refresh content/examples/*.json + openapi snapshot from production
13 +node scripts/make-brand-assets.mjs # regenerate public/og.png + PNG icons (Playwright)
13 14 ```
14 15
16 +## Rule: the site never spends the visitor's quota by itself
17 +
18 +Keyless access is 30 requests / hour per IP. The homepage makes **one** call on load (`GET /v1/status`, quota-exempt)
19 +and falls back silently to `content/examples/status_v1_status_get.json`. Docs pages show the captured example and
20 +only fetch on an explicit **Run live**. The playground's empty state is the captured example of the selected
21 +endpoint; only **Run** (or a valid deep link, once) hits the API. A 429 is always shown as *rate limited — not down*,
22 +with a countdown and a free-account CTA (`ErrorState`).
23 +
15 24 ## Architecture
16 25
17 26 ```
18 −index.html theme applied before first paint (inline script reads localStorage hfmd.theme)
27 +index.html theme applied before first paint (inline script reads localStorage hfmd.theme); og.png, icons, manifest
19 28 src/main.jsx BrowserRouter › AuthProvider › LangProvider › App
20 29 src/App.jsx route table, one lazy chunk per area (owner comments inside)
21 −src/app/ Layout (topbar · drawer · search · footer), theme.css (tokens), theme.js, api.js, auth.jsx
30 +src/app/ Layout (topbar · drawer · search · footer), theme.css (design tokens + utilities), theme.js, api.js, auth.jsx
22 31 src/components/ shared UI (below) + components.css
23 32 src/docs/ docs engine: spec.js (OpenAPI loader), enrich.js (example values), snippets.js (code gen),
24 33 guides.js (registry), search.js (index), mdx-components.jsx, useTitle.js
25 34 src/pages/home|limits|status marketing / limits / live status pages (web-core)
26 −src/pages/docs DocsLayout (3 columns), DocsHome, ReferenceIndex, OperationPage, GuidePage, ErrorsPage
35 +src/pages/legal /terms, /privacy, /data-license (MDX in the same folder)
36 +src/pages/NotFound.jsx 404 with search + links
37 +src/pages/docs DocsLayout (nav · content · panel), DocsHome, ReferenceIndex, OperationPage, GuidePage, ErrorsPage
27 38 src/pages/playground|auth|dashboard|admin web-app agent · src/pages/integrations mcp agent
39 +src/playground/ Playground (app), EndpointPicker (combobox), RequestForm, catalog.js, codegen.js, views/
28 40 content/guides/*.mdx guides (MDX) · content/changelog.mdx
29 41 content/examples/<opId>.json captured real responses (scripts/fetch-examples.mjs) — commit them
42 +content/examples/home-aapl-daily.json one year of daily AAPL bars for the homepage chart (compact rows)
30 43 content/openapi.snapshot.json bundled spec fallback when /openapi.json is unreachable (e.g. vite preview)
31 −content/samples/aapl-pe.json homepage chart fallback (scripts/make-aapl-pe-sample.mjs)
32 −scripts/ prerender.mjs · fetch-examples.mjs · make-aapl-pe-sample.mjs · bench-formats.mjs · bench-parquet.py
33 −e2e/ Playwright specs (site, docs, mobile) · playwright.config.js
34 −public/ favicon.svg, logo.svg, og.svg, robots.txt (copied verbatim to dist/)
44 +content/samples/aapl-pe.json homepage fundamentals chart (scripts/make-aapl-pe-sample.mjs)
45 +public/ favicon.svg, logo.svg, og.png, icon-{32,192,512}.png, apple-touch-icon.png, manifest.webmanifest,
46 + fonts/jetbrains-mono-latin.woff2 (31 KB, variable weight), robots.txt, downloads/
47 +scripts/ prerender.mjs · fetch-examples.mjs · make-aapl-pe-sample.mjs · make-brand-assets.mjs · bench-*
48 +e2e/ Playwright specs (site, docs, mobile, playground, auth, dashboard) · playwright.config.js
35 49 ```
36 50
37 −Chunks (`vite.config.js › manualChunks`): `vendor` (react, router), `prism`, `charts` (lightweight-charts, only
38 −loaded by the homepage chart / playground), `mdx`, one chunk per page and per guide.
51 +Chunks (`vite.config.js › manualChunks`): `vendor` (react, router), `prism` (lazy — `CodeHighlight.jsx` is loaded on
52 +demand by `CodeBlock`, so a page paints its code as plain text first), `charts` (lightweight-charts, lazy: homepage
53 +charts load when scrolled into view, playground Chart tab), `mdx`, one chunk per page, guide and captured example.
54 +
55 +## Design system (`src/app/theme.css`)
39 56
40 −### Theme
57 +Tokens live in `:root` (dark) with `[data-theme="light"]` overrides. Breakpoints: **640 / 960 / 1200 px**.
41 58
42 −Tokens live in `src/app/theme.css` (`:root` dark, `[data-theme="light"]` overrides). `src/app/theme.js` exposes
43 −`useTheme()` → `{ theme, toggle, set }` and `usePersisted(key, initial)` (localStorage-backed `useState`). Colours for
44 −code blocks are CSS variables too (`--code-*`) so Prism follows the theme without a second stylesheet. HTTP verb colours:
45 −`--get --post --put --patch --delete`.
59 +| Group | Tokens |
60 +| --- | --- |
61 +| Surfaces | `--bg --bg-1 --bg-2 --bg-3`, lines `--line --line-2 --line-3` |
62 +| Text | `--fg --fg-1 --fg-2 --fg-3` (all ≥ 4.5:1 on `--bg`/`--bg-1`/`--bg-2`), `--fg-muted-deco` (decorative only, never for text) |
63 +| Accent | `--accent --accent-ink --accent-soft --accent-strong`, `--accent-2 --accent-2-soft` |
64 +| Market | `--up --down --neutral` (candles, deltas — never raw hex in charts) |
65 +| States | `--warn --danger --info --success` + `*-soft` |
66 +| Verbs | `--get --post --put --patch --delete` |
67 +| Type | `--sans`, `--mono` (JetBrains Mono self-hosted), `--fs-0…8` = 12/13/14/15/17/20/24/32/44, `--lh-tight/snug/body/prose` |
68 +| Space / radius / shadow | `--sp-1…10` (4·n), `--r-sm --r-md --r-lg` (6/10/16; `--radius`/`--radius-lg` are legacy aliases), `--shadow-1 --shadow-2` |
69 +| Layers | `--z-sticky 10 · --z-drawer 60 · --z-modal 80` |
70 +| Controls | `--tap` = 36 px desktop, 44 px under 960 px — every button/link/input uses it as min-height |
71 +
72 +Utility classes (usable in any page, including the dashboard): `.btn .btn-primary .btn-secondary .btn-ghost .btn-danger
73 +.btn-sm .btn-lg .btn-block`, `.icon-btn`, `.card .card-sm`, `.grid .grid-2/3/4`, `.stack .row`, `.eyebrow .lead .muted
74 +.small .mono .num .up .down .neutral`, `.table-wrap` (+ `.dense`, `.sticky-first`, `.table-stack` for label/value rows
75 +under 640 px), **`.tabs-nav`** (horizontal scrolling tab bar for links/buttons, `.active` or `aria-current`),
76 +**`.stat .stat-label .stat-value .stat-hint .stat-delta`**, **`.skeleton .skeleton-block .skeleton-text`**,
77 +**`.empty-state`**, **`.error-state`** (+ `.is-429`, `.countdown`), `.sr-only`, `.skip-link`.
78 +
79 +Accessibility: single `:focus-visible` ring (no double ring on inputs), `prefers-reduced-motion`, skip link, roles on
80 +tabs / dialog / combobox / listbox, contrast AA on every text token.
46 81
47 82 ## Shared components (`src/components/`, barrel `index.js`)
48 83
49 84 | Component | Props | Notes |
50 85 | --- | --- | --- |
51 −| `Code` | `code, language='text', title?, copy=true, maxHeight?, lineNumbers=false` | Prism (prism-react-renderer) + bundled `bash`/`r` grammars added in `Code.jsx`; copy button; scrolls inside when `maxHeight` set. `CopyButton({ text })`, `useCopy()` exported. |
52 −| `CodeTabs` | `snippets, title?, maxHeight?` | `snippets` = `{ curl, python, javascript, r }` (any subset) **or** `[{ id, label, language, code }]`. Uses the global persisted language (`hfmd.lang`). |
53 −| `Tabs` | `items:[{id,label,content?,icon?}], value?, onChange?, defaultValue?, size='md'|'sm', children?(activeId)` | Accessible tablist (roving focus, arrows/Home/End). |
54 −| `Callout` | `type='info'|'tip'|'warning'|'danger', title?` | Aside with icon; `role=alert` for warning/danger. |
55 −| `Table` | `columns:[{key,label,align?,render?,width?}], rows, keyField?, caption?, dense?` | Responsive wrapper `.table-wrap`. |
56 −| `Badge` / `MethodBadge` / `StatusBadge` | `tone='neutral'|'success'|'info'|'warn'|'danger'` / `method` / `code` | |
86 +| `CodeBlock` (alias `Code`) | `code, language='text', title?, copy=true, wrapToggle=true, wrap?, maxHeight?, lineNumbers=false, actions?, footer?` | Prism (lazy chunk, theme via `--code-*` vars, bash/R/toml/http grammars added). Header: title, extra `actions`, wrap toggle (persisted `hfmd.codewrap`), copy. `footer` = caption under the code. |
87 +| `CopyButton` / `useCopy()` | `text (string \| () => string), label='Copy', size='sm'\|'md', iconOnly, variant='ghost'\|'secondary'` | The only clipboard helper. |
88 +| `CodeTabs` | `snippets, title?, maxHeight?, footer?, lineNumbers?` | `snippets` = `{ curl, python, javascript, r }` (any subset) **or** `[{ id, label, language, code }]`. Uses the global persisted language (`hfmd.lang`). |
89 +| `Button` / `IconButton` | `variant='primary'\|'secondary'\|'ghost'\|'danger', size='sm'\|'md'\|'lg', to?, href?, block, loading, disabled` / `label (required), to?, href?` | Renders `<Link>` / `<a>` when `to` / `href` is set. |
90 +| `Tabs` | `items:[{id,label,content?,icon?,badge?,disabled?}], value?, onChange?, defaultValue?, size='md'\|'sm', right?, ariaLabel, panel=true, children?(activeId)` | Accessible tablist (roving focus, arrows/Home/End, skips disabled). `right` = slot at the end of the bar. |
91 +| `Callout` | `type='info'\|'tip'\|'success'\|'warning'\|'danger', title?, action?, compact` | `kind` accepted as alias (`warn` → `warning`) — `PgCallout.jsx` re-exports it for the dashboard/auth pages. `role=alert` for warning/danger. |
92 +| `Card` | `as='div', to?, size='md'\|'sm'` | Link card when `to` is set. |
93 +| `Table` | `columns:[{key,label,align?,render?,width?,mono?}], rows, keyField?, caption?, dense, stickyFirst, stack, maxHeight?, emptyText` | Sticky header inside `.table-wrap`; `stack` turns rows into label/value pairs under 640 px (cells carry `data-label`). |
94 +| `Field` / `Input` / `Select` / `Textarea` | `Field: label, hint, error, required, id?, inline, labelExtra` — wires `id`, `aria-describedby`, `aria-invalid` on its single child. `Select` accepts `options` (strings or `{value,label}`). Controls accept `invalid`, `Input` accepts `mono`. |
95 +| `Skeleton` | `width, height, lines?, block` | Shimmer placeholder. |
96 +| `EmptyState` | `icon?, title, children, actions?` | Dashed box, centred. |
97 +| `ErrorState` | `status, code?, message?, title?, retryUntil? (epoch s), authenticated, docs?, onRetry?, actions?` | Network (status 0) / 4xx / **429 with live countdown + free-account CTA** / 5xx. Also exports `useCountdown(epochSec)`, `fmtDuration(s)`. |
98 +| `Stat` | `label, value, hint?, delta?, loading` | Number tile (`delta` coloured with `--up`/`--down`). |
99 +| `Badge` / `MethodBadge` / `StatusBadge` / `TierBadge` | `tone='neutral'\|'success'\|'info'\|'warn'\|'danger', dot` / `method` / `code` / `tier` | |
100 +| `BrandLogo` | `name='claude'\|'cursor'\|'codex'\|'mcp'\|'skills'\|'python'\|'r'\|'javascript', size=40` | One source for the homepage and Integrations logos. |
57 101 | `LangSelector` | `value?, onChange?, size` | Radio group over `LANGS` (`curl, python, javascript, r`); `useLang()` → `[lang, setLang]`; wrap app in `LangProvider`. |
58 −| `Markdown` / `InlineMd` | `text` or children, `headingOffset?` | Safe Markdown → React (no innerHTML): headings, lists, fenced code, tables, blockquotes, inline code/bold/italic/links (unsafe schemes dropped; site links become router `Link`s). |
59 −| `SearchDialog` | `open, onClose` | ⌘K/Ctrl+K and `/` are bound in `Layout`; index built lazily from spec + guides. |
60 −| `Icons` | — | Stroke icons (`SunIcon`, `CopyIcon`, `PlayIcon`, …), 18px, currentColor. |
102 +| `Markdown` / `InlineMd` | `text` or children, `headingOffset?` | Safe Markdown → React (no innerHTML). |
103 +| `SearchDialog` | `open, onClose, initialQuery?` | ⌘K/Ctrl+K and `/` are bound in `Layout`; any page can open it with a query: `window.dispatchEvent(new CustomEvent('hfmd:search', { detail: 'rate limits' }))`. |
104 +| `useMediaQuery(query)` | → boolean | Used by the docs layout to move the code panel inline under 1200 px. |
105 +| `Icons` | — | Stroke icons (`SunIcon`, `CopyIcon`, `PlayIcon`, `WrapIcon`, `CheckCircleIcon`, `InboxIcon`, `WifiOffIcon`, …), 18px, currentColor. |
61 106
62 −CSS classes reusable anywhere: `.btn .btn-primary .btn-ghost .btn-sm .btn-lg`, `.card`, `.grid .grid-2/3/4`, `.eyebrow`,
63 −`.lead`, `.muted`, `.small`, `.mono`, `.num`, `.table-wrap`, `.sr-only`, `.skeleton` (home.css).
107 +Compatibility aliases kept for pages owned by other agents: `components/PgCallout.jsx` (→ `Callout`, `kind` prop) and
108 +`components/PgCopyButton.jsx` (→ `CopyButton`, `{ text, label, small }`). `PgTabs`, `PgCodeBlock`, `IntCopyBlock`,
109 +`Code.jsx`, `src/styles.css` and `src/legacy-spec.js` were removed.
64 110
65 111 ## Docs engine
66 112
@@ -73,38 +119,46 @@ CSS classes reusable anywhere: `.btn .btn-primary .btn-ghost .btn-sm .btn-lg`, `
73 119 (resolved, with type/default/enum/constraints), responses }`; `groupByTag` orders by `spec.tags`.
74 120 FastAPI's `^(a|b)$` patterns are surfaced as enums.
75 121 3. `enrich.js › exampleParams(op)` derives realistic values: spec `example` → `PATH_OVERRIDES["METHOD /path"]` →
76 − `NAME_DEFAULTS[paramName]` (AAPL, ES, ESZ25, CL, 2024-06-03…) → schema default/enum. `buildRequest(op, values)`
77 − → `{ url, rel, query, format }`; `playgroundLink(op, values)`.
78 −4. `snippets.js › snippetsFor({ method, url, format, auth, stream })` → `{ curl, python, javascript, r }`
79 − (requests+pandas, fetch, httr2; Parquet/CSV parsing lines vary with `format`; `$HFMD_API_KEY` env var, never a key).
80 −5. Response example: spec example (`responseExample`) → `content/examples/<operationId>.json` (captured at build by
81 − `npm run examples`) → live fetch for public GET operations (cached in memory). Errors: `errorResponses()` parses the
82 − `\`CODE\`` markers that `api/openapi.py` writes into 4xx/5xx descriptions; every code links to `/docs/errors#<code>`.
83 −6. Routes: `/docs/reference` (index), `/docs/reference/tag/<tag>`, `/docs/reference/<operationId>`, `/docs/errors`.
122 + `NAME_DEFAULTS[paramName]` → schema default/enum. `buildRequest(op, values)` → `{ url, rel, query, format }`;
123 + `playgroundLink(op, values)`.
124 +4. `snippets.js › snippetsFor({ method, url, format, auth, stream })` → `{ curl, python, javascript, r }`.
125 +5. Response example: spec example → `content/examples/<operationId>.json` (captured by `npm run examples`) →
126 + **nothing automatic**. Public GET pages show a *Run live* button in the response header (one request of the
127 + visitor's quota; 429 → "rate limited — not down" + countdown).
128 +6. Layout: `DocPage({ intro, children, after, panel })`. Above 1200 px the panel (On this page → request → response)
129 + is a sticky aside; below, `useMediaQuery` renders it **inside the article right after `intro`** (before
130 + Parameters) — DOM order = visual order, no CSS reordering. Content column ≤ 720 px + 420 px panel.
131 +7. Headings: the `#` anchor is an empty `<a class="anchor">` drawn with `::before`, so `textContent` stays clean.
132 +8. Routes: `/docs/reference` (index), `/docs/reference/tag/<tag>`, `/docs/reference/<operationId>`, `/docs/errors`.
84 133
85 134 **Guides** are MDX in `content/guides/`. The `virtual:guides-index` Vite plugin (in `vite.config.js`) extracts `##`/`###`
86 135 headings of every guide at build time → search index + "On this page" outline without loading the guide chunks.
87 −Heading ids use the same `slugify` in the plugin and in `mdx-components.jsx`.
88 136
89 137 ### Add a guide
90 138
91 −1. Create `content/guides/<file>.mdx`. Optional exports:
92 − ```js
93 − export const meta = { title, description }
94 − export const snippets = [{ title, curl, python, javascript, r }] // → right-hand code panel
95 − ```
96 − Components available without import: `Callout`, `CodeTabs`, `Tabs`, `Table`, `Badge`, `MethodBadge`, `Code`,
97 − `Endpoint method path id?` (chip linking to the reference), `TryIt ep params`, `Url path`, `Figure caption`, `Link`.
98 − MDX caveats: no bare `<` or `{` in prose (use code spans), SVG attributes in camelCase.
99 −2. Register it in `src/docs/guides.js › GUIDES` (`slug`, `title`, `section`, `file`, `summary`). Sections:
100 − `start · guides · recipes · fundamentals · more`. Nav, search, prev/next and the sitemap pick it up automatically.
139 +1. Create `content/guides/<file>.mdx`. Optional exports: `meta = { title, description }`,
140 + `snippets = [{ title, curl, python, javascript, r }]` (→ right-hand code panel). Components available without
141 + import: `Callout`, `CodeTabs`, `Tabs`, `Table`, `Badge`, `MethodBadge`, `Code`, `Endpoint method path id?`,
142 + `TryIt ep params`, `Url path`, `Figure caption`, `Link`. MDX caveats: no bare `<` or `{` in prose, SVG attributes
143 + in camelCase.
144 +2. Register it in `src/docs/guides.js › GUIDES` (`slug`, `title`, `section`, `file`, `summary`). Nav, search,
145 + prev/next, sitemap **and the prerendered shell** pick it up automatically (`scripts/prerender.mjs` parses `GUIDES`).
101 146
102 147 ### Errors page
103 148
104 149 `/docs/errors` lists every code of `components.schemas.Error.properties.code.enum` (plus curated meaning/fix in
105 150 `ErrorsPage.jsx › ERROR_INFO`) with anchors `#<code_lower>` — the API's `error.docs` URLs point there.
106 151
107 −## Playground deep-link contract (`/playground?ep=…`)
152 +## Playground
153 +
154 +Layout: **endpoint picker** (`EndpointPicker.jsx`, a command-palette combobox: search by title/path/group, grouped
155 +listbox, ↑ ↓ Enter Esc) + preset chips → **request card** (parameters grid built from `catalog.js`, URL bar, Run —
156 +sticky at the bottom on mobile) → **results** full width (tabs JSON / Table / Chart / Code / Download) → rate-limit
157 +panel. Empty state = the captured production example of the endpoint (`content/examples/<operationId>.json`) with
158 +"Edit a parameter and run". `catalog.js › normalizeValues()` recomputes dependent enums (e.g. `adjustment` after
159 +`asset` arrives from a deep link) so Run is never blocked by "Not an allowed value".
160 +
161 +### Deep-link contract (`/playground?ep=…`)
108 162
109 163 Every reference page's **Try it** button and the MDX `<TryIt>` component produce:
110 164
@@ -112,44 +166,41 @@ Every reference page's **Try it** button and the MDX `<TryIt>` component produce
112 166 /playground?ep=<operationId>&<paramName>=<value>&<paramName>=<value>…
113 167 ```
114 168
115 −- `ep` — the OpenAPI `operationId` (e.g. `bars_v1_bars__asset___ticker__get`). The playground should resolve it via
116 − `listOperations(spec)` from `src/docs/spec.js` and select that endpoint.
117 −- Every other query key is a **parameter name of that operation** (path or query), URL-encoded, already validated
118 − against the spec's enums/patterns by construction. Unknown keys should be ignored.
119 −- `format` may be present (`json|csv|parquet`); `api_key` is never included.
120 −- Helpers to reuse: `exampleParams(op)`, `buildRequest(op, values)`, `snippetsFor(...)`, `rateHeaders(res)` (`api.js`).
169 +- `ep` — the OpenAPI `operationId` (e.g. `bars_v1_bars__asset___ticker__get`) or a catalog id (`stock-bars`). The
170 + playground resolves it via `catalog.js › findEntry` (fixed params like `asset=futures` pick the matching entry), then
171 + `/openapi.json` for unknown ids (generic form).
172 +- Every other query key is a **parameter name of that operation**, URL-encoded. Unknown keys are ignored. `format` may be
173 + present (`json|csv|parquet`); `api_key` is never included.
174 +- A deep link with `ep` **auto-runs once** when the form validates (one request). `/playground` alone never calls the API.
121 175
122 176 ## Prerender (LCP)
123 177
124 178 `npm run build` runs `vite build` (with `build.manifest`) then `scripts/prerender.mjs`, which writes static shells for
125 −`/`, `/limits`, `/docs`, `/docs/quickstart`, `/status` — as both `dist/<route>/index.html` and `dist/<route>.html` so
126 −every static server finds them (`/` in place) — plus `dist/sitemap.xml`:
127 −
128 −- route-specific `<title>`, description, OpenGraph, canonical;
129 −- `<link rel="modulepreload">` for the route's whole chunk graph and `<link rel="stylesheet">` for its CSS — the
130 − lazy-route waterfall disappears, so the first paint of the real page is one round-trip after HTML;
131 −- above-the-fold markup (header, h1, lead) inside `#root`, styled by the already-linked CSS, so LCP happens before
132 − React mounts. `createRoot().render()` then swaps it for the live tree.
133 −
134 −Trade-off: this is *not* SSR — dynamic content (counters, spec-driven reference) is not in the HTML. Full SSR would
135 −require a second SSR bundle and a Node render per route around lazy routes, MDX and browser-only libraries
136 −(lightweight-charts), for pages whose content is mostly fetched live anyway. The shell approach costs ~60 lines and no
137 −runtime dependency. **Backend note**: the FastAPI SPA fallback serves `dist/index.html` for directory paths; to serve the
138 −shells it should try, in order, `candidate`, `candidate.with_suffix(".html")` and `candidate / "index.html"` before
139 −falling back to `index.html`.
179 +`/`, `/limits`, `/status`, `/playground`, `/integrations`, `/terms`, `/privacy`, `/data-license`, `/docs`,
180 +`/docs/reference`, `/docs/errors` and **every guide in `GUIDES`** — as both `dist/<route>/index.html` and
181 +`dist/<route>.html` — plus `dist/sitemap.xml`. Each shell has route-specific `<title>`, description, OpenGraph,
182 +canonical, `<link rel="modulepreload">` for the route's chunk graph (guide chunk included), its CSS, and the h1 + lead
183 +inside `#root`. Trade-off: not SSR — dynamic content is not in the HTML (see git history for the reasoning).
184 +**Backend note**: the SPA fallback should try `candidate`, `candidate.html` and `candidate/index.html` before `index.html`,
185 +and return 404 for unknown paths (the client renders `NotFound`).
140 186
141 187 ## Scripts
142 188
143 189 | Script | Purpose |
144 190 | --- | --- |
145 −| `scripts/fetch-examples.mjs` | For each public GET operation without a spec example, call production with realistic params and store `content/examples/<operationId>.json` (`{ url, params, status, headers, captured_at, body }`, `data` trimmed to 5 rows). Refreshes `content/openapi.snapshot.json` when the live spec has the `Error` schema. Flags: `--base`, `--only a,b`, `--force`. |
146 −| `scripts/make-aapl-pe-sample.mjs` | Builds `content/samples/aapl-pe.json` (weekly AAPL split-adjusted close ÷ 10-K diluted EPS, labelled as such) — homepage chart fallback until `/v1/fundamentals/AAPL/ratios/daily` is live. |
191 +| `scripts/fetch-examples.mjs` | For each public GET operation without a spec example, call production with realistic params and store `content/examples/<operationId>.json`. Flags: `--base`, `--only a,b`, `--force`. |
192 +| `scripts/make-brand-assets.mjs` | Renders `public/og.png` (1200×630), `apple-touch-icon.png` (180) and `icon-{32,192,512}.png` from inline HTML with Playwright. |
193 +| `scripts/make-aapl-pe-sample.mjs` | Builds `content/samples/aapl-pe.json` (weekly AAPL split-adjusted close ÷ 10-K diluted EPS) — homepage fundamentals chart. |
147 194 | `scripts/bench-formats.mjs` + `bench-parquet.py` | Real JSON/CSV/Parquet size measurements used in the Data formats guide. |
148 195 | `scripts/prerender.mjs` | See above. |
149 196
150 197 ## E2E
151 198
152 199 `playwright.config.js` starts `vite preview` on :4173 (Chromium desktop + Pixel 7 for `mobile.spec.js`). Without the
153 −API, docs render from the bundled snapshot and the homepage/status pages exercise their fallbacks — which is exactly
154 −what the tests assert (docs navigation, ⌘K search with keyboard, Try-it link format, limits content and CTAs, theme
155 −toggle persistence, prerendered shells, mobile drawer/docs nav). Browsers: `npx playwright install chromium`.
200 +API, docs render from the bundled snapshot and the homepage/status pages exercise their fallbacks. Specs cover: home
201 +(single `/v1/status` call, captured hero, footer, no overflow), limits, theme persistence + light-mode code blocks,
202 +status (down vs **429 = rate limited**, last dates), 404 (search seeds the ⌘K dialog), legal pages, prerendered
203 +shells for every guide, tap targets; docs (outline order, Run live explicit, wrap toggle, clean heading text, search,
204 +anchors); playground (endpoint picker, captured empty state, deep link auto-run, dependent enums, 429 countdown, CSV,
205 +WebSocket gating); mobile (drawer, inline docs panel order, playground form + 44 px Run, no overflow on every public
206 +page). Browsers: `npx playwright install chromium`.
added hfmarketdata/web/content/examples/home-aapl-daily.json +263 −0
@@ -0,0 +1,263 @@
1 +{
2 + "note": "Real response captured from production on 2026-09-06 for the homepage chart (GET /v1/bars/stock/AAPL?timeframe=1day&start=2025-09-02&end=2026-09-03, adj_splitdiv), OHLC rounded to 2 decimals, volume dropped. Columns: datetime, open, high, low, close. Refresh with npm run examples.",
3 + "url": "https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day&start=2025-09-02&end=2026-09-03",
4 + "captured_at": "2026-09-06T14:20:00Z",
5 + "ticker": "AAPL",
6 + "columns": ["datetime", "open", "high", "low", "close"],
7 + "rows": [
8 + ["2025-09-02",228.41,230.0,226.13,228.87],
9 + ["2025-09-03",236.34,237.97,233.5,237.59],
10 + ["2025-09-04",237.57,239.02,235.87,238.9],
11 + ["2025-09-05",239.11,240.43,237.61,238.81],
12 + ["2025-09-08",238.42,239.27,235.47,237.0],
13 + ["2025-09-09",236.13,237.9,232.5,233.49],
14 + ["2025-09-10",231.33,231.56,225.12,225.96],
15 + ["2025-09-11",226.04,229.6,225.82,229.18],
16 + ["2025-09-12",228.38,233.65,228.18,233.21],
17 + ["2025-09-15",236.13,237.31,234.16,235.83],
18 + ["2025-09-16",236.3,240.33,235.45,237.27],
19 + ["2025-09-17",238.09,239.22,236.86,238.11],
20 + ["2025-09-18",239.09,240.31,235.78,237.0],
21 + ["2025-09-19",240.34,245.39,239.33,244.6],
22 + ["2025-09-22",247.39,255.7,247.21,255.14],
23 + ["2025-09-23",254.93,256.39,252.65,253.49],
24 + ["2025-09-24",254.28,254.8,250.12,251.38],
25 + ["2025-09-25",252.27,256.22,250.79,255.92],
26 + ["2025-09-26",253.16,256.65,252.85,254.52],
27 + ["2025-09-29",253.62,254.06,252.08,253.49],
28 + ["2025-09-30",253.92,254.98,252.18,253.69],
29 + ["2025-10-01",254.1,257.84,253.99,254.51],
30 + ["2025-10-02",255.63,257.23,253.21,256.18],
31 + ["2025-10-03",253.73,258.29,253.02,257.07],
32 + ["2025-10-06",257.04,258.12,254.11,255.75],
33 + ["2025-10-07",255.86,256.45,254.49,255.54],
34 + ["2025-10-08",255.58,257.57,255.17,257.11],
35 + ["2025-10-09",256.86,257.05,252.21,253.1],
36 + ["2025-10-10",254.0,255.44,243.1,244.37],
37 + ["2025-10-13",248.46,248.77,244.66,246.75],
38 + ["2025-10-14",245.69,247.93,243.8,246.86],
39 + ["2025-10-15",248.57,250.89,246.56,248.42],
40 + ["2025-10-16",247.34,248.12,244.23,246.54],
41 + ["2025-10-17",247.11,252.45,246.36,251.36],
42 + ["2025-10-20",254.94,263.4,254.69,261.27],
43 + ["2025-10-21",260.92,264.31,260.87,261.8],
44 + ["2025-10-22",261.68,261.88,254.49,257.5],
45 + ["2025-10-23",258.98,259.66,257.06,258.62],
46 + ["2025-10-24",260.23,263.16,258.23,261.85],
47 + ["2025-10-27",263.91,268.13,263.68,267.82],
48 + ["2025-10-28",268.0,268.9,267.16,268.01],
49 + ["2025-10-29",268.28,270.41,266.13,268.71],
50 + ["2025-10-30",270.99,273.13,267.49,270.4],
51 + ["2025-10-31",275.97,276.3,268.17,269.37],
52 + ["2025-11-03",269.42,269.85,265.27,268.06],
53 + ["2025-11-04",267.34,270.49,266.63,269.05],
54 + ["2025-11-05",267.62,270.7,265.95,269.15],
55 + ["2025-11-06",266.9,272.39,266.9,268.78],
56 + ["2025-11-07",268.8,271.29,265.79,267.48],
57 + ["2025-11-10",268.23,272.99,266.73,268.7],
58 + ["2025-11-11",269.08,275.16,269.07,274.5],
59 + ["2025-11-12",274.25,274.98,270.96,272.73],
60 + ["2025-11-13",273.37,275.95,271.35,272.21],
61 + ["2025-11-14",270.31,275.21,268.87,271.67],
62 + ["2025-11-17",268.09,269.76,265.01,266.73],
63 + ["2025-11-18",269.26,269.98,264.6,266.71],
64 + ["2025-11-19",264.8,271.47,264.78,267.83],
65 + ["2025-11-20",270.09,274.68,265.2,265.53],
66 + ["2025-11-21",265.23,272.59,264.95,270.75],
67 + ["2025-11-24",270.16,276.25,270.16,275.17],
68 + ["2025-11-25",274.52,279.62,274.5,276.22],
69 + ["2025-11-26",276.21,278.77,275.88,276.8],
70 + ["2025-11-28",276.51,278.24,275.24,278.09],
71 + ["2025-12-01",277.26,282.65,275.39,282.33],
72 + ["2025-12-02",282.23,286.62,281.86,285.41],
73 + ["2025-12-03",285.42,287.84,282.53,283.38],
74 + ["2025-12-04",283.32,283.96,277.83,279.94],
75 + ["2025-12-05",279.78,280.38,277.3,278.02],
76 + ["2025-12-08",277.38,278.91,275.4,277.14],
77 + ["2025-12-09",277.4,279.27,276.17,276.43],
78 + ["2025-12-10",277.0,278.99,275.69,278.02],
79 + ["2025-12-11",278.34,278.83,273.07,277.28],
80 + ["2025-12-12",277.15,278.46,276.07,277.52],
81 + ["2025-12-15",279.39,279.39,272.1,273.37],
82 + ["2025-12-16",272.08,274.75,271.05,273.86],
83 + ["2025-12-17",274.26,275.41,270.9,271.1],
84 + ["2025-12-18",272.86,272.89,266.23,271.45],
85 + ["2025-12-19",271.41,273.85,269.17,272.93],
86 + ["2025-12-22",272.12,273.14,269.77,270.23],
87 + ["2025-12-23",270.1,271.76,268.83,271.62],
88 + ["2025-12-24",271.6,274.68,271.46,273.07],
89 + ["2025-12-26",273.42,274.62,272.12,272.66],
90 + ["2025-12-29",271.95,273.62,271.61,273.02],
91 + ["2025-12-30",272.07,273.34,271.54,272.34],
92 + ["2025-12-31",272.32,272.94,271.01,271.12],
93 + ["2026-01-02",271.52,277.09,268.27,270.27],
94 + ["2026-01-05",269.91,270.77,265.42,266.53],
95 + ["2026-01-06",266.28,266.82,261.41,261.65],
96 + ["2026-01-07",262.49,262.96,259.1,259.62],
97 + ["2026-01-08",256.32,258.59,255.01,258.34],
98 + ["2026-01-09",258.37,259.5,255.52,258.67],
99 + ["2026-01-12",258.46,260.59,256.1,259.54],
100 + ["2026-01-13",258.02,261.1,257.69,260.34],
101 + ["2026-01-14",258.79,261.11,256.01,259.25],
102 + ["2026-01-15",259.94,260.33,256.35,257.51],
103 + ["2026-01-16",257.2,258.2,254.24,254.84],
104 + ["2026-01-20",252.04,254.1,242.76,246.03],
105 + ["2026-01-21",248.02,250.88,244.51,246.98],
106 + ["2026-01-22",248.52,250.32,247.48,247.68],
107 + ["2026-01-23",246.65,248.73,244.02,247.37],
108 + ["2026-01-26",250.8,255.86,249.12,254.72],
109 + ["2026-01-27",258.47,261.24,257.51,257.57],
110 + ["2026-01-28",256.95,258.15,253.82,255.74],
111 + ["2026-01-29",257.3,258.95,253.72,257.58],
112 + ["2026-01-30",254.47,261.19,251.5,258.78],
113 + ["2026-02-02",259.32,269.76,258.5,269.28],
114 + ["2026-02-03",268.47,271.14,266.88,268.75],
115 + ["2026-02-04",271.55,278.19,271.55,275.74],
116 + ["2026-02-05",277.38,278.74,272.49,275.16],
117 + ["2026-02-06",276.37,280.14,276.17,277.37],
118 + ["2026-02-09",277.41,277.7,271.22,274.13],
119 + ["2026-02-10",274.4,274.88,272.45,273.19],
120 + ["2026-02-11",274.21,279.68,273.96,275.01],
121 + ["2026-02-12",275.1,275.23,259.72,261.26],
122 + ["2026-02-13",261.54,261.76,254.99,255.32],
123 + ["2026-02-17",257.59,265.82,255.08,263.41],
124 + ["2026-02-18",263.13,266.34,261.98,263.88],
125 + ["2026-02-19",262.13,264.01,259.59,260.12],
126 + ["2026-02-20",258.51,264.28,257.7,264.11],
127 + ["2026-02-23",263.02,268.95,262.91,265.71],
128 + ["2026-02-24",267.38,274.4,267.23,271.66],
129 + ["2026-02-25",271.3,274.45,270.57,273.74],
130 + ["2026-02-26",274.46,275.62,270.31,272.46],
131 + ["2026-02-27",272.32,272.32,262.42,263.71],
132 + ["2026-03-02",261.94,266.06,259.74,264.25],
133 + ["2026-03-03",263.01,265.09,259.67,263.28],
134 + ["2026-03-04",264.18,265.68,260.96,262.05],
135 + ["2026-03-05",260.33,261.09,256.79,259.83],
136 + ["2026-03-06",258.17,258.31,253.92,257.0],
137 + ["2026-03-09",255.23,260.68,253.23,259.42],
138 + ["2026-03-10",257.19,262.01,256.49,260.37],
139 + ["2026-03-11",260.62,261.66,259.09,260.35],
140 + ["2026-03-12",258.2,258.49,253.73,255.3],
141 + ["2026-03-13",255.02,255.87,249.11,249.67],
142 + ["2026-03-16",251.66,253.43,249.43,252.37],
143 + ["2026-03-17",252.5,254.68,251.73,253.78],
144 + ["2026-03-18",252.18,254.49,248.56,249.49],
145 + ["2026-03-19",248.96,251.38,246.86,248.52],
146 + ["2026-03-20",247.53,248.76,245.56,247.55],
147 + ["2026-03-23",253.52,254.15,249.83,251.04],
148 + ["2026-03-24",249.9,254.37,249.11,251.19],
149 + ["2026-03-25",253.64,254.55,251.15,252.17],
150 + ["2026-03-26",251.67,256.54,250.32,252.44],
151 + ["2026-03-27",253.45,255.04,247.63,248.36],
152 + ["2026-03-30",249.62,250.42,245.6,245.87],
153 + ["2026-03-31",247.47,255.02,246.66,253.34],
154 + ["2026-04-01",253.63,255.72,252.88,255.17],
155 + ["2026-04-02",253.75,255.67,250.2,255.46],
156 + ["2026-04-06",256.05,261.69,256.0,258.4],
157 + ["2026-04-07",255.7,255.74,245.26,253.05],
158 + ["2026-04-08",257.99,259.29,256.07,258.44],
159 + ["2026-04-09",258.54,260.65,255.61,260.03],
160 + ["2026-04-10",259.52,261.72,258.56,260.02],
161 + ["2026-04-13",259.27,259.72,256.2,258.74],
162 + ["2026-04-14",258.78,261.46,256.73,258.37],
163 + ["2026-04-15",257.7,266.09,257.35,265.96],
164 + ["2026-04-16",266.32,266.68,260.8,262.93],
165 + ["2026-04-17",266.48,271.81,266.24,269.75],
166 + ["2026-04-20",269.85,273.79,269.81,272.56],
167 + ["2026-04-21",271.02,272.31,264.93,265.7],
168 + ["2026-04-22",267.34,273.25,266.39,272.68],
169 + ["2026-04-23",274.56,275.28,271.17,272.94],
170 + ["2026-04-24",272.27,272.57,269.17,270.58],
171 + ["2026-04-27",265.62,267.88,264.6,267.13],
172 + ["2026-04-28",271.85,272.74,268.18,270.23],
173 + ["2026-04-29",267.07,270.56,266.56,269.69],
174 + ["2026-04-30",270.02,275.51,267.66,270.87],
175 + ["2026-05-01",278.36,286.71,277.87,279.64],
176 + ["2026-05-04",279.16,280.13,274.37,276.34],
177 + ["2026-05-05",276.43,284.06,276.01,283.67],
178 + ["2026-05-06",281.41,287.52,280.57,287.0],
179 + ["2026-05-07",288.75,291.61,285.27,286.93],
180 + ["2026-05-08",289.49,294.23,289.48,292.8],
181 + ["2026-05-11",291.73,293.63,289.98,292.43],
182 + ["2026-05-12",292.31,295.02,292.31,294.55],
183 + ["2026-05-13",293.25,300.66,293.25,298.61],
184 + ["2026-05-14",299.56,300.19,295.13,297.95],
185 + ["2026-05-15",297.64,302.94,296.26,299.97],
186 + ["2026-05-18",299.98,300.4,294.66,297.58],
187 + ["2026-05-19",296.71,300.25,296.09,298.71],
188 + ["2026-05-20",297.92,302.54,297.82,301.99],
189 + ["2026-05-21",300.8,305.28,300.14,304.73],
190 + ["2026-05-22",305.86,311.13,305.58,308.55],
191 + ["2026-05-26",309.29,311.55,307.4,308.06],
192 + ["2026-05-27",308.06,312.99,308.03,310.58],
193 + ["2026-05-28",310.41,312.53,309.3,312.24],
194 + ["2026-05-29",311.51,314.73,309.26,311.79],
195 + ["2026-06-01",309.36,310.67,304.76,306.05],
196 + ["2026-06-02",307.2,315.18,306.42,314.93],
197 + ["2026-06-03",313.9,316.67,308.58,309.99],
198 + ["2026-06-04",312.96,313.27,309.38,310.96],
199 + ["2026-06-05",312.59,314.9,306.89,307.08],
200 + ["2026-06-08",308.47,317.13,300.91,301.28],
201 + ["2026-06-09",300.02,300.49,287.53,290.3],
202 + ["2026-06-10",290.49,294.5,287.13,291.33],
203 + ["2026-06-11",293.47,296.74,289.34,295.38],
204 + ["2026-06-12",295.77,296.88,289.37,290.88],
205 + ["2026-06-15",293.87,297.52,291.45,296.16],
206 + ["2026-06-16",294.99,300.22,293.72,298.98],
207 + ["2026-06-17",300.59,301.81,294.11,295.69],
208 + ["2026-06-18",297.85,300.31,295.37,297.75],
209 + ["2026-06-22",297.05,302.16,296.5,296.75],
210 + ["2026-06-23",297.28,301.38,293.93,294.05],
211 + ["2026-06-24",295.1,299.44,292.69,292.83],
212 + ["2026-06-25",287.15,288.55,273.51,274.91],
213 + ["2026-06-26",274.76,285.7,273.97,283.54],
214 + ["2026-06-29",286.48,288.12,279.61,281.5],
215 + ["2026-06-30",280.93,289.69,280.45,289.11],
216 + ["2026-07-01",293.19,296.33,288.95,294.13],
217 + ["2026-07-02",293.87,309.15,293.43,308.36],
218 + ["2026-07-06",307.1,313.93,306.74,312.39],
219 + ["2026-07-07",315.02,315.21,309.88,310.39],
220 + ["2026-07-08",311.64,314.55,306.79,313.12],
221 + ["2026-07-09",310.24,316.26,307.89,315.95],
222 + ["2026-07-10",314.45,316.64,311.9,315.05],
223 + ["2026-07-13",316.74,323.17,315.51,317.04],
224 + ["2026-07-14",313.49,315.92,311.64,314.59],
225 + ["2026-07-15",317.34,328.45,317.05,327.22],
226 + ["2026-07-16",327.72,334.39,326.51,332.97],
227 + ["2026-07-17",331.69,334.7,328.72,333.45],
228 + ["2026-07-20",333.22,333.42,323.4,326.31],
229 + ["2026-07-21",322.85,329.32,321.94,327.46],
230 + ["2026-07-22",327.59,328.72,323.06,325.61],
231 + ["2026-07-23",321.45,323.02,319.07,321.38],
232 + ["2026-07-24",321.51,334.08,321.34,332.73],
233 + ["2026-07-27",334.25,339.28,333.73,336.62],
234 + ["2026-07-28",339.74,342.59,335.31,339.79],
235 + ["2026-07-29",339.44,344.27,337.06,337.9],
236 + ["2026-07-30",332.81,334.46,329.31,333.14],
237 + ["2026-07-31",304.55,310.42,299.74,308.64],
238 + ["2026-08-03",309.31,311.53,302.3,303.16],
239 + ["2026-08-04",302.46,310.15,301.06,309.11],
240 + ["2026-08-05",309.09,311.44,305.41,310.73],
241 + ["2026-08-06",314.07,316.02,308.96,312.14],
242 + ["2026-08-07",311.18,314.54,310.47,313.06],
243 + ["2026-08-10",306.83,308.26,304.61,308.26],
244 + ["2026-08-11",307.75,309.97,302.79,304.91],
245 + ["2026-08-12",305.1,305.66,300.57,302.25],
246 + ["2026-08-13",304.21,306,302.05,305.26],
247 + ["2026-08-14",306,307.49,304.3,305.93],
248 + ["2026-08-17",306.21,307.66,302.94,305.59],
249 + ["2026-08-18",307.58,311.49,305.74,310.03],
250 + ["2026-08-19",310.14,319.28,309.6,316.83],
251 + ["2026-08-20",317.45,320.28,310.65,311.3],
252 + ["2026-08-21",312.05,312.38,307.01,309.35],
253 + ["2026-08-24",311.47,313.36,309.97,310.34],
254 + ["2026-08-25",310.79,313.59,308.21,309.9],
255 + ["2026-08-26",310.3,315.43,308.8,313.45],
256 + ["2026-08-27",310.55,315.4,309.4,314.58],
257 + ["2026-08-28",316.85,322.37,315.45,319.7],
258 + ["2026-08-31",319.6,321.24,312.8,316.85],
259 + ["2026-09-01",316.98,327.3,314.73,325.13],
260 + ["2026-09-02",326.87,328.4,323.53,324.96],
261 + ["2026-09-03",324.87,330.81,324.11,328.21]
262 + ]
263 +}
modified hfmarketdata/web/content/samples/aapl-pe.json +1 −1
@@ -1 +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
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\u2013FY2025 (diluted EPS, split-adjusted)"},"generated_at":"2026-09-04T23:38:04.919Z","points":[{"date":"2015-01-02","close":27.33,"eps_ttm":1.61,"pe":16.98},{"date":"2015-01-09","close":28.0,"eps_ttm":1.61,"pe":17.39},{"date":"2015-01-16","close":26.5,"eps_ttm":1.61,"pe":16.46},{"date":"2015-01-23","close":28.25,"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.73,"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.38,"eps_ttm":1.61,"pe":20.11},{"date":"2015-02-27","close":32.12,"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.9,"eps_ttm":1.61,"pe":19.19},{"date":"2015-03-20","close":31.48,"eps_ttm":1.61,"pe":19.55},{"date":"2015-03-27","close":30.81,"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.77,"eps_ttm":1.61,"pe":19.74},{"date":"2015-04-17","close":31.19,"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.24,"eps_ttm":1.61,"pe":20.02},{"date":"2015-05-08","close":31.91,"eps_ttm":1.61,"pe":19.82},{"date":"2015-05-15","close":32.19,"eps_ttm":1.61,"pe":20},{"date":"2015-05-22","close":33.13,"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.16,"eps_ttm":1.61,"pe":19.98},{"date":"2015-06-12","close":31.79,"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.69,"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.41,"eps_ttm":1.61,"pe":20.13},{"date":"2015-07-24","close":31.12,"eps_ttm":1.61,"pe":19.33},{"date":"2015-07-31","close":30.32,"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.32,"eps_ttm":1.61,"pe":17.59},{"date":"2015-09-04","close":27.32,"eps_ttm":1.61,"pe":16.97},{"date":"2015-09-11","close":28.55,"eps_ttm":1.61,"pe":17.73},{"date":"2015-09-18","close":28.36,"eps_ttm":1.61,"pe":17.62},{"date":"2015-09-25","close":28.68,"eps_ttm":1.61,"pe":17.81},{"date":"2015-10-02","close":27.59,"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.88,"eps_ttm":2.31,"pe":12.93},{"date":"2015-11-06","close":30.27,"eps_ttm":2.31,"pe":13.1},{"date":"2015-11-13","close":28.09,"eps_ttm":2.31,"pe":12.16},{"date":"2015-11-20","close":29.82,"eps_ttm":2.31,"pe":12.91},{"date":"2015-11-27","close":29.45,"eps_ttm":2.31,"pe":12.75},{"date":"2015-12-04","close":29.76,"eps_ttm":2.31,"pe":12.88},{"date":"2015-12-11","close":28.3,"eps_ttm":2.31,"pe":12.25},{"date":"2015-12-18","close":26.51,"eps_ttm":2.31,"pe":11.48},{"date":"2015-12-24","close":27.01,"eps_ttm":2.31,"pe":11.69},{"date":"2015-12-31","close":26.32,"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.28,"eps_ttm":2.31,"pe":10.51},{"date":"2016-01-22","close":25.36,"eps_ttm":2.31,"pe":10.98},{"date":"2016-01-29","close":24.34,"eps_ttm":2.31,"pe":10.53},{"date":"2016-02-05","close":23.5,"eps_ttm":2.31,"pe":10.18},{"date":"2016-02-12","close":23.5,"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.23,"eps_ttm":2.31,"pe":10.49},{"date":"2016-03-04","close":25.75,"eps_ttm":2.31,"pe":11.15},{"date":"2016-03-11","close":25.57,"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.42,"eps_ttm":2.31,"pe":11.44},{"date":"2016-04-01","close":27.5,"eps_ttm":2.31,"pe":11.9},{"date":"2016-04-08","close":27.16,"eps_ttm":2.31,"pe":11.76},{"date":"2016-04-15","close":27.46,"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.43,"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.8,"eps_ttm":2.31,"pe":10.31},{"date":"2016-05-27","close":25.09,"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.71,"eps_ttm":2.31,"pe":10.7},{"date":"2016-06-17","close":23.83,"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.97,"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.7,"eps_ttm":2.31,"pe":10.69},{"date":"2016-07-22","close":24.66,"eps_ttm":2.31,"pe":10.68},{"date":"2016-07-29","close":26.05,"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.05,"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.73,"eps_ttm":2.31,"pe":11.57},{"date":"2016-09-02","close":26.93,"eps_ttm":2.31,"pe":11.66},{"date":"2016-09-09","close":25.78,"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.18,"eps_ttm":2.31,"pe":12.2},{"date":"2016-09-30","close":28.26,"eps_ttm":2.31,"pe":12.23},{"date":"2016-10-07","close":28.52,"eps_ttm":2.31,"pe":12.34},{"date":"2016-10-14","close":29.41,"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.11,"eps_ttm":2.08,"pe":13.03},{"date":"2016-11-18","close":27.52,"eps_ttm":2.08,"pe":13.23},{"date":"2016-11-25","close":27.95,"eps_ttm":2.08,"pe":13.44},{"date":"2016-12-02","close":27.48,"eps_ttm":2.08,"pe":13.21},{"date":"2016-12-09","close":28.49,"eps_ttm":2.08,"pe":13.7},{"date":"2016-12-16","close":28.99,"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.95,"eps_ttm":2.08,"pe":13.92},{"date":"2017-01-06","close":29.48,"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.49,"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.16,"eps_ttm":2.08,"pe":16.43},{"date":"2017-03-03","close":34.95,"eps_ttm":2.08,"pe":16.8},{"date":"2017-03-10","close":34.78,"eps_ttm":2.08,"pe":16.72},{"date":"2017-03-17","close":35.0,"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.91,"eps_ttm":2.08,"pe":17.27},{"date":"2017-04-07","close":35.84,"eps_ttm":2.08,"pe":17.23},{"date":"2017-04-13","close":35.26,"eps_ttm":2.08,"pe":16.95},{"date":"2017-04-21","close":35.57,"eps_ttm":2.08,"pe":17.1},{"date":"2017-04-28","close":35.91,"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.02,"eps_ttm":2.08,"pe":18.76},{"date":"2017-05-19","close":38.27,"eps_ttm":2.08,"pe":18.4},{"date":"2017-05-26","close":38.4,"eps_ttm":2.08,"pe":18.46},{"date":"2017-06-02","close":38.86,"eps_ttm":2.08,"pe":18.68},{"date":"2017-06-09","close":37.24,"eps_ttm":2.08,"pe":17.91},{"date":"2017-06-16","close":35.57,"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.01,"eps_ttm":2.08,"pe":17.31},{"date":"2017-07-07","close":36.05,"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.57,"eps_ttm":2.08,"pe":18.06},{"date":"2017-07-28","close":37.38,"eps_ttm":2.08,"pe":17.97},{"date":"2017-08-04","close":39.1,"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.38,"eps_ttm":2.08,"pe":18.93},{"date":"2017-08-25","close":39.97,"eps_ttm":2.08,"pe":19.21},{"date":"2017-09-01","close":41.01,"eps_ttm":2.08,"pe":19.72},{"date":"2017-09-08","close":39.66,"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.97,"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.83,"eps_ttm":2.08,"pe":18.67},{"date":"2017-10-13","close":39.25,"eps_ttm":2.08,"pe":18.87},{"date":"2017-10-20","close":39.06,"eps_ttm":2.08,"pe":18.78},{"date":"2017-10-27","close":40.76,"eps_ttm":2.08,"pe":19.6},{"date":"2017-11-03","close":43.12,"eps_ttm":2.3,"pe":18.75},{"date":"2017-11-10","close":43.67,"eps_ttm":2.3,"pe":18.99},{"date":"2017-11-17","close":42.54,"eps_ttm":2.3,"pe":18.49},{"date":"2017-11-24","close":43.74,"eps_ttm":2.3,"pe":19.02},{"date":"2017-12-01","close":42.76,"eps_ttm":2.3,"pe":18.59},{"date":"2017-12-08","close":42.34,"eps_ttm":2.3,"pe":18.41},{"date":"2017-12-15","close":43.49,"eps_ttm":2.3,"pe":18.91},{"date":"2017-12-22","close":43.75,"eps_ttm":2.3,"pe":19.02},{"date":"2017-12-29","close":42.31,"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.27,"eps_ttm":2.3,"pe":19.25},{"date":"2018-01-19","close":44.62,"eps_ttm":2.3,"pe":19.4},{"date":"2018-01-26","close":42.88,"eps_ttm":2.3,"pe":18.64},{"date":"2018-02-02","close":40.12,"eps_ttm":2.3,"pe":17.45},{"date":"2018-02-09","close":39.1,"eps_ttm":2.3,"pe":17},{"date":"2018-02-16","close":43.11,"eps_ttm":2.3,"pe":18.74},{"date":"2018-02-23","close":43.88,"eps_ttm":2.3,"pe":19.08},{"date":"2018-03-02","close":44.05,"eps_ttm":2.3,"pe":19.15},{"date":"2018-03-09","close":44.99,"eps_ttm":2.3,"pe":19.56},{"date":"2018-03-16","close":44.51,"eps_ttm":2.3,"pe":19.35},{"date":"2018-03-23","close":41.23,"eps_ttm":2.3,"pe":17.93},{"date":"2018-03-29","close":41.95,"eps_ttm":2.3,"pe":18.24},{"date":"2018-04-06","close":42.09,"eps_ttm":2.3,"pe":18.3},{"date":"2018-04-13","close":43.68,"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.96,"eps_ttm":2.3,"pe":19.98},{"date":"2018-05-11","close":47.15,"eps_ttm":2.3,"pe":20.5},{"date":"2018-05-18","close":46.58,"eps_ttm":2.3,"pe":20.25},{"date":"2018-05-25","close":47.15,"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.92,"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.28,"eps_ttm":2.3,"pe":20.12},{"date":"2018-07-06","close":46.99,"eps_ttm":2.3,"pe":20.43},{"date":"2018-07-13","close":47.83,"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.74,"eps_ttm":2.3,"pe":20.76},{"date":"2018-08-03","close":52.0,"eps_ttm":2.3,"pe":22.61},{"date":"2018-08-10","close":51.88,"eps_ttm":2.3,"pe":22.56},{"date":"2018-08-17","close":54.4,"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.91,"eps_ttm":2.3,"pe":24.74},{"date":"2018-09-07","close":55.33,"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.41,"eps_ttm":2.3,"pe":23.66},{"date":"2018-09-28","close":56.44,"eps_ttm":2.3,"pe":24.54},{"date":"2018-10-05","close":56.07,"eps_ttm":2.3,"pe":24.38},{"date":"2018-10-12","close":55.53,"eps_ttm":2.3,"pe":24.14},{"date":"2018-10-19","close":54.83,"eps_ttm":2.3,"pe":23.84},{"date":"2018-10-26","close":54.08,"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.12,"eps_ttm":2.98,"pe":17.15},{"date":"2018-11-16","close":48.38,"eps_ttm":2.98,"pe":16.24},{"date":"2018-11-23","close":43.07,"eps_ttm":2.98,"pe":14.45},{"date":"2018-11-30","close":44.65,"eps_ttm":2.98,"pe":14.98},{"date":"2018-12-07","close":42.12,"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.68,"eps_ttm":2.98,"pe":12.65},{"date":"2018-12-28","close":39.06,"eps_ttm":2.98,"pe":13.11},{"date":"2019-01-04","close":37.06,"eps_ttm":2.98,"pe":12.44},{"date":"2019-01-11","close":38.07,"eps_ttm":2.98,"pe":12.78},{"date":"2019-01-18","close":39.2,"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.6,"eps_ttm":2.98,"pe":14.3},{"date":"2019-02-15","close":42.6,"eps_ttm":2.98,"pe":14.3},{"date":"2019-02-22","close":43.24,"eps_ttm":2.98,"pe":14.51},{"date":"2019-03-01","close":43.74,"eps_ttm":2.98,"pe":14.68},{"date":"2019-03-08","close":43.23,"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.76,"eps_ttm":2.98,"pe":16.03},{"date":"2019-03-29","close":47.49,"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.72,"eps_ttm":2.98,"pe":16.68},{"date":"2019-04-18","close":50.97,"eps_ttm":2.98,"pe":17.1},{"date":"2019-04-26","close":51.08,"eps_ttm":2.98,"pe":17.14},{"date":"2019-05-03","close":52.94,"eps_ttm":2.98,"pe":17.76},{"date":"2019-05-10","close":49.3,"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.74,"eps_ttm":2.98,"pe":15.01},{"date":"2019-05-31","close":43.77,"eps_ttm":2.98,"pe":14.69},{"date":"2019-06-07","close":47.54,"eps_ttm":2.98,"pe":15.95},{"date":"2019-06-14","close":48.19,"eps_ttm":2.98,"pe":16.17},{"date":"2019-06-21","close":49.7,"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.06,"eps_ttm":2.98,"pe":17.13},{"date":"2019-07-12","close":50.83,"eps_ttm":2.98,"pe":17.06},{"date":"2019-07-19","close":50.65,"eps_ttm":2.98,"pe":17},{"date":"2019-07-26","close":51.94,"eps_ttm":2.98,"pe":17.43},{"date":"2019-08-02","close":51.01,"eps_ttm":2.98,"pe":17.12},{"date":"2019-08-09","close":50.25,"eps_ttm":2.98,"pe":16.86},{"date":"2019-08-16","close":51.62,"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.19,"eps_ttm":2.98,"pe":17.51},{"date":"2019-09-06","close":53.31,"eps_ttm":2.98,"pe":17.89},{"date":"2019-09-13","close":54.69,"eps_ttm":2.98,"pe":18.35},{"date":"2019-09-20","close":54.43,"eps_ttm":2.98,"pe":18.27},{"date":"2019-09-27","close":54.7,"eps_ttm":2.98,"pe":18.36},{"date":"2019-10-04","close":56.75,"eps_ttm":2.98,"pe":19.04},{"date":"2019-10-11","close":59.05,"eps_ttm":2.98,"pe":19.82},{"date":"2019-10-18","close":59.1,"eps_ttm":2.98,"pe":19.83},{"date":"2019-10-25","close":61.65,"eps_ttm":2.98,"pe":20.69},{"date":"2019-11-01","close":63.95,"eps_ttm":2.97,"pe":21.53},{"date":"2019-11-08","close":65.03,"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.44,"eps_ttm":2.97,"pe":22.04},{"date":"2019-11-29","close":66.81,"eps_ttm":2.97,"pe":22.5},{"date":"2019-12-06","close":67.68,"eps_ttm":2.97,"pe":22.79},{"date":"2019-12-13","close":68.79,"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.36,"eps_ttm":2.97,"pe":25.04},{"date":"2020-01-10","close":77.58,"eps_ttm":2.97,"pe":26.12},{"date":"2020-01-17","close":79.68,"eps_ttm":2.97,"pe":26.83},{"date":"2020-01-24","close":79.58,"eps_ttm":2.97,"pe":26.79},{"date":"2020-01-31","close":77.38,"eps_ttm":2.97,"pe":26.05},{"date":"2020-02-07","close":80.01,"eps_ttm":2.97,"pe":26.94},{"date":"2020-02-14","close":81.24,"eps_ttm":2.97,"pe":27.35},{"date":"2020-02-21","close":78.26,"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.26,"eps_ttm":2.97,"pe":24.33},{"date":"2020-03-13","close":69.49,"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.94,"eps_ttm":2.97,"pe":20.85},{"date":"2020-04-03","close":60.35,"eps_ttm":2.97,"pe":20.32},{"date":"2020-04-09","close":67.0,"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.74,"eps_ttm":2.97,"pe":23.82},{"date":"2020-05-01","close":72.27,"eps_ttm":2.97,"pe":24.33},{"date":"2020-05-08","close":77.53,"eps_ttm":2.97,"pe":26.11},{"date":"2020-05-15","close":76.93,"eps_ttm":2.97,"pe":25.9},{"date":"2020-05-22","close":79.72,"eps_ttm":2.97,"pe":26.84},{"date":"2020-05-29","close":79.48,"eps_ttm":2.97,"pe":26.76},{"date":"2020-06-05","close":82.88,"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.41,"eps_ttm":2.97,"pe":29.77},{"date":"2020-07-02","close":91.03,"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.33,"eps_ttm":2.97,"pe":32.43},{"date":"2020-07-24","close":92.61,"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.11,"eps_ttm":2.97,"pe":37.41},{"date":"2020-08-14","close":114.91,"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.81,"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/e2e/docs.spec.js +48 −13
@@ -1,8 +1,10 @@
1 −// Docs: navigation, generated reference, Try-it contract, search dialog, errors anchors, language persistence.
1 +// Docs: navigation, generated reference, Try-it contract, search dialog, errors anchors, language persistence,
2 +// no automatic live calls (Run live is explicit), wrap toggle, heading anchors without "#" in textContent.
2 3 import { expect, test } from '@playwright/test'
4 +import { json, mockAnon } from './mocks.js'
3 5
4 6 test.describe('docs', () => {
5 − test('overview renders nav and quickstart guide loads with code panel', async ({ page }) => {
7 + test('overview renders nav and quickstart guide loads with code panel (outline first)', async ({ page }) => {
6 8 await page.goto('/docs')
7 9 await expect(page.getByTestId('docs-home')).toBeVisible()
8 10 await expect(page.getByRole('navigation', { name: 'Documentation' })).toBeVisible()
@@ -10,10 +12,12 @@ test.describe('docs', () => {
10 12 await expect(page).toHaveURL(/\/docs\/quickstart$/)
11 13 await expect(page.getByRole('heading', { level: 1, name: 'Quickstart' })).toBeVisible()
12 14 await expect(page.getByTestId('guide-page')).toBeVisible()
13 − // right panel: code tabs with the 4 languages
14 15 const panel = page.getByRole('complementary', { name: 'Code and outline' })
15 16 await expect(panel.getByRole('radio', { name: 'Python' }).first()).toBeVisible()
16 17 await expect(panel.getByRole('radio', { name: 'R', exact: true }).first()).toBeVisible()
18 + // "On this page" comes before the code blocks
19 + const [toc, code] = await Promise.all([panel.getByRole('navigation', { name: 'On this page' }).boundingBox(), panel.locator('.code').first().boundingBox()])
20 + expect(toc.y).toBeLessThan(code.y)
17 21 await expect(page).toHaveTitle(/Quickstart/)
18 22 })
19 23
@@ -37,20 +41,29 @@ test.describe('docs', () => {
37 41 await expect(page).toHaveURL(/\/docs\/reference\/bars_v1_bars__asset___ticker__get$/)
38 42 })
39 43
40 − test('operation page: parameters, samples, response example and Try-it deep link', async ({ page }) => {
44 + test('operation page: parameters, samples, captured example, Try-it deep link, no automatic API call', async ({ page }) => {
45 + const calls = []
46 + await mockAnon(page, url => { calls.push(url.pathname); return null })
41 47 await page.goto('/docs/reference/bars_v1_bars__asset___ticker__get')
42 48 await expect(page.getByTestId('operation-page')).toBeVisible()
43 49 await expect(page.getByRole('heading', { level: 1 })).toHaveText(/Bars/)
44 50 await expect(page.locator('.op-path')).toContainText('/v1/bars/{asset}/{ticker}')
45 − // parameters table lists the documented params
46 51 for (const p of ['asset', 'ticker', 'timeframe', 'limit', 'format']) await expect(page.locator(`#param-${p}`)).toBeVisible()
47 − // curl sample uses the production URL with realistic values, no key
48 52 const panel = page.getByRole('complementary', { name: 'Code and outline' })
49 53 await panel.getByRole('radio', { name: 'curl' }).first().click()
50 54 await expect(panel.locator('.code').first()).toContainText('https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day')
51 55 await expect(panel.locator('.code').first()).not.toContainText('hfmd_live_')
52 − // captured response example present (content/examples)
53 − await expect(panel.locator('.code').nth(1)).toContainText('"ticker": "AAPL"')
56 + // captured response example present (content/examples), nothing fetched
57 + await expect(page.getByTestId('response-example')).toContainText('"ticker": "AAPL"')
58 + await expect(page.getByTestId('response-example')).toContainText('captured')
59 + await page.waitForTimeout(400)
60 + expect(calls.filter(p => p !== '/v1/me')).toEqual([])
61 + // Run live is explicit
62 + await page.unroute('**/v1/**')
63 + await mockAnon(page, url => (url.pathname === '/v1/bars/stock/AAPL' ? json({ count: 1, data: [{ ticker: 'AAPL', datetime: '2024-06-03', close: 1 }] }) : null))
64 + await page.getByTestId('run-live').click()
65 + await expect(page.getByTestId('response-example')).toContainText('live')
66 + await expect(page.getByTestId('response-example')).toContainText('keyless requests left')
54 67 // Try it → /playground?ep=<operationId>&<params>
55 68 const href = await page.getByTestId('try-it').getAttribute('href')
56 69 expect(href).toMatch(/^\/playground\?ep=bars_v1_bars__asset___ticker__get&/)
@@ -58,11 +71,34 @@ test.describe('docs', () => {
58 71 expect(q.get('asset')).toBe('stock')
59 72 expect(q.get('ticker')).toBe('AAPL')
60 73 expect(q.get('timeframe')).toBe('1day')
61 − // errors section links to anchors on the errors page
62 74 await expect(page.locator('#errors')).toBeVisible()
63 75 await expect(page.getByRole('link', { name: 'RATE_LIMIT_EXCEEDED' }).first()).toHaveAttribute('href', '/docs/errors#rate_limit_exceeded')
64 76 })
65 77
78 + test('operation page: Run live on a 429 says rate limited, not down', async ({ page }) => {
79 + await mockAnon(page, url => (url.pathname === '/v1/bars/stock/AAPL'
80 + ? json({ error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests' } }, 429, { 'Retry-After': '90' }) : null))
81 + await page.goto('/docs/reference/bars_v1_bars__asset___ticker__get')
82 + await page.getByTestId('run-live').click()
83 + await expect(page.getByTestId('response-example')).toContainText('Rate limited — not down')
84 + await expect(page.getByTestId('response-example').getByRole('link', { name: /free account/ })).toBeVisible()
85 + await expect(page.getByTestId('run-live')).toBeDisabled()
86 + })
87 +
88 + test('code blocks: wrap toggle persists; heading anchors carry no "#" text', async ({ page }) => {
89 + await page.goto('/docs/quickstart')
90 + const block = page.getByRole('complementary', { name: 'Code and outline' }).locator('.code').first()
91 + await expect(block).not.toHaveClass(/code-wrap/)
92 + await block.getByRole('button', { name: 'Wrap long lines' }).click()
93 + await expect(block).toHaveClass(/code-wrap/)
94 + expect(await page.evaluate(() => localStorage.getItem('hfmd.codewrap'))).toBe('1')
95 + await page.reload()
96 + await expect(page.getByRole('complementary', { name: 'Code and outline' }).locator('.code').first()).toHaveClass(/code-wrap/)
97 + const texts = await page.locator('.docs-content h2').evaluateAll(els => els.map(e => e.textContent))
98 + for (const t of texts) expect(t.trim().startsWith('#')).toBe(false)
99 + await page.evaluate(() => localStorage.removeItem('hfmd.codewrap'))
100 + })
101 +
66 102 test('errors page has an anchor per code', async ({ page }) => {
67 103 await page.goto('/docs/errors#rate_limit_exceeded')
68 104 await expect(page.getByTestId('errors-page')).toBeVisible()
@@ -70,6 +106,7 @@ test.describe('docs', () => {
70 106 await expect(page.locator(`#${id}`)).toBeAttached()
71 107 }
72 108 await expect(page.locator('#rate_limit_exceeded')).toContainText('RATE_LIMIT_EXCEEDED')
109 + expect((await page.locator('#rate_limit_exceeded h3').textContent()).trim().endsWith('#')).toBe(false)
73 110 })
74 111
75 112 test('Cmd+K search: open, type, keyboard navigate, Enter opens the page, Esc closes', async ({ page }) => {
@@ -86,7 +123,6 @@ test.describe('docs', () => {
86 123 await page.keyboard.press('Enter')
87 124 await expect(dialog).toBeHidden()
88 125 await expect(page).toHaveURL(/\/docs\/futures-contracts/)
89 − // slash shortcut + Escape
90 126 await page.keyboard.press('/')
91 127 await expect(page.getByTestId('search-dialog')).toBeVisible()
92 128 await expect(page.getByTestId('search-dialog').getByRole('combobox')).toBeFocused()
@@ -94,7 +130,6 @@ test.describe('docs', () => {
94 130 await expect(page.getByTestId('search-dialog').getByRole('option').first()).toContainText(/Opt Chain|chain/i)
95 131 await page.keyboard.press('Escape')
96 132 await expect(page.getByTestId('search-dialog')).toBeHidden()
97 − // trigger button also opens it
98 133 await page.getByTestId('search-trigger').click()
99 134 await expect(page.getByTestId('search-dialog')).toBeVisible()
100 135 })
@@ -106,9 +141,7 @@ test.describe('docs', () => {
106 141 await expect(page.locator('.callout').first()).toBeVisible()
107 142 await expect(page.locator('.figure svg').first()).toBeVisible()
108 143 await expect(page.locator('.docs-content table').first()).toBeVisible()
109 − // On this page outline lists the headings
110 144 await expect(page.getByRole('navigation', { name: 'On this page' }).getByRole('link', { name: /Adjustment methods/ })).toBeVisible()
111 − // Edit / report link is a mailto with the page URL
112 145 const mail = await page.getByRole('link', { name: /Edit \/ Report an issue/ }).getAttribute('href')
113 146 expect(mail).toMatch(/^mailto:contact@spboucher\.ai\?subject=/)
114 147 expect(decodeURIComponent(mail)).toContain('/docs/futures-contracts')
@@ -117,5 +150,7 @@ test.describe('docs', () => {
117 150 test('changelog has the v2.0.0 entry dated 2026-09-04', async ({ page }) => {
118 151 await page.goto('/docs/changelog')
119 152 await expect(page.getByRole('heading', { level: 2, name: /2\.0\.0 — 2026-09-04/ })).toBeVisible()
153 + await page.goto('/changelog')
154 + await expect(page).toHaveURL(/\/docs\/changelog$/)
120 155 })
121 156 })
modified hfmarketdata/web/e2e/mobile.spec.js +45 −7
@@ -1,29 +1,67 @@
1 −// Mobile (Pixel 7): drawer navigation, docs nav toggle, code panel below content (DOM order = visual order).
1 +// Mobile (Pixel 7): drawer navigation, docs nav toggle + inline panel order, playground form + sticky Run,
2 +// tap targets ≥ 44 px, no horizontal overflow anywhere.
2 3 import { expect, test } from '@playwright/test'
4 +import { mockAnon } from './mocks.js'
5 +
6 +const noOverflow = page => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
3 7
4 8 test.describe('mobile', () => {
5 − test('drawer opens from the menu button and navigates', async ({ page }) => {
9 + test('drawer opens from the menu button and navigates; header tap targets ≥ 44 px', async ({ page }) => {
6 10 await page.goto('/')
7 11 await expect(page.getByRole('navigation', { name: 'Main' })).toBeHidden()
12 + const small = await page.evaluate(() => [...document.querySelectorAll('header a, header button')].filter(e => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && (r.height < 44 || r.width < 44) }).length)
13 + expect(small).toBe(0)
8 14 await page.getByRole('button', { name: 'Open menu' }).click()
9 15 const drawer = page.getByRole('navigation', { name: 'Mobile' })
10 16 await expect(drawer).toBeVisible()
17 + await expect(drawer.getByRole('link', { name: 'Changelog' })).toBeVisible()
11 18 await drawer.getByRole('link', { name: 'Limits' }).click()
12 19 await expect(page).toHaveURL(/\/limits$/)
13 20 await expect(drawer).toBeHidden()
14 21 })
15 22
16 − test('docs: nav toggle, code panel after content', async ({ page }) => {
23 + test('no horizontal overflow on the public pages', async ({ page }) => {
24 + await mockAnon(page)
25 + for (const path of ['/', '/docs', '/docs/quickstart', '/docs/reference/bars_v1_bars__asset___ticker__get', '/playground', '/integrations', '/limits', '/status', '/terms', '/nope']) {
26 + await page.goto(path)
27 + await page.waitForTimeout(300)
28 + expect(await noOverflow(page), path).toBe(true)
29 + }
30 + })
31 +
32 + test('docs: nav toggle; request/response panel right after the intro, before Parameters', async ({ page }) => {
17 33 await page.goto('/docs/quickstart')
18 34 await expect(page.getByRole('navigation', { name: 'Documentation' })).toBeHidden()
19 35 await page.getByRole('button', { name: /Docs menu/ }).click()
20 36 await expect(page.getByRole('navigation', { name: 'Documentation' })).toBeVisible()
21 37 await page.getByRole('navigation', { name: 'Documentation' }).getByRole('link', { name: 'Rate limits' }).click()
22 38 await expect(page).toHaveURL(/\/docs\/rate-limits$/)
23 − const content = page.locator('.docs-content')
24 − const panel = page.getByRole('complementary', { name: 'Code and outline' })
25 − const [c, p] = await Promise.all([content.boundingBox(), panel.boundingBox()])
26 − expect(p.y).toBeGreaterThan(c.y) // panel rendered below the article on small screens
39 + await expect(page.getByRole('complementary', { name: 'Code and outline' })).toHaveCount(0)
40 + await expect(page.getByTestId('docs-panel-inline')).toBeVisible()
41 + // operation page: inline panel between the header actions and the Parameters heading
42 + await page.goto('/docs/reference/bars_v1_bars__asset___ticker__get')
43 + const [tryIt, panel, params, prevNext] = await Promise.all([
44 + page.getByTestId('try-it').boundingBox(), page.getByTestId('docs-panel-inline').boundingBox(),
45 + page.locator('#parameters').boundingBox(), page.getByRole('navigation', { name: 'Previous / next' }).boundingBox(),
46 + ])
47 + expect(panel.y).toBeGreaterThan(tryIt.y)
48 + expect(params.y).toBeGreaterThan(panel.y)
49 + expect(prevNext.y).toBeGreaterThan(params.y)
50 + // parameters table is readable: no cell narrower than its label column, no horizontal overflow
51 + expect(await noOverflow(page)).toBe(true)
52 + })
53 +
54 + test('playground: form visible near the top, Run reachable, results below', async ({ page }) => {
55 + await mockAnon(page)
56 + await page.goto('/playground')
57 + const form = await page.getByTestId('pg-form').boundingBox()
58 + expect(form.y).toBeLessThan(900)
59 + const run = page.getByTestId('pg-run')
60 + await run.scrollIntoViewIfNeeded()
61 + const box = await run.boundingBox()
62 + expect(box.height).toBeGreaterThanOrEqual(44)
63 + await expect(page.getByTestId('pg-sample')).toBeVisible()
64 + expect(await noOverflow(page)).toBe(true)
27 65 })
28 66
29 67 test('search dialog usable on touch viewport', async ({ page }) => {
modified hfmarketdata/web/e2e/playground.spec.js +80 −27
@@ -1,3 +1,5 @@
1 +// Playground: endpoint picker, form, deep links (incl. auto-run and dependent enums), captured example empty state,
2 +// results views, code tab, 429 countdown, CSV download, WebSocket gating.
1 3 import { expect, test } from '@playwright/test'
2 4 import { bars, json, mockAnon } from './mocks.js'
3 5
@@ -16,13 +18,28 @@ test.beforeEach(async ({ page }) => {
16 18 await page.route('**/openapi.json', r => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(SPEC) }))
17 19 })
18 20
19 −test('keyless banner, default request and live URL preview', async ({ page }) => {
20 − await mockAnon(page)
21 +async function pickEndpoint(page, name) {
22 + await page.getByTestId('pg-endpoint').click()
23 + await page.getByTestId('pg-endpoint-search').fill(name)
24 + await page.getByRole('listbox', { name: 'Endpoints' }).getByRole('option', { name: new RegExp(name.replace(/[()]/g, '\\$&'), 'i') }).first().click()
25 +}
26 +
27 +test('keyless banner, default request, captured example as empty state, live URL preview, form above the fold', async ({ page }) => {
28 + const calls = []
29 + await mockAnon(page, url => { calls.push(url.pathname); return null })
21 30 await page.goto('/playground')
22 31 await expect(page.getByText('Keyless mode:')).toBeVisible()
23 32 await expect(page.getByRole('link', { name: 'Create free account' }).first()).toBeVisible()
33 + // parameters form is visible without scrolling on desktop
34 + const form = await page.getByTestId('pg-form').boundingBox()
35 + expect(form.y + 100).toBeLessThan(page.viewportSize().height)
24 36 const url = page.getByTestId('pg-url')
25 37 await expect(url).toContainText('https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1day')
38 + // empty state = captured production example, no API call made
39 + await expect(page.getByTestId('pg-sample')).toContainText('Edit a parameter and run')
40 + await expect(page.getByRole('tab', { name: /^Table/ })).toHaveAttribute('aria-selected', 'true')
41 + await expect(page.getByRole('region', { name: 'Results table' })).toContainText('AAPL')
42 + expect(calls.filter(p => p !== '/v1/me')).toEqual([])
26 43 await page.getByLabel(/^ticker/).fill('MSFT')
27 44 await expect(url).toContainText('/v1/bars/stock/MSFT')
28 45 await expect(page).toHaveURL(/ep=stock-bars/)
@@ -31,16 +48,31 @@ test('keyless banner, default request and live URL preview', async ({ page }) =>
31 48 await expect(url).toContainText('/v1/bars/etf/MSFT')
32 49 })
33 50
34 −test('run → status, latency, rows, table / chart / json views and rate-limit panel', async ({ page }) => {
51 +test('endpoint picker: search, keyboard, selection', async ({ page }) => {
52 + await mockAnon(page)
53 + await page.goto('/playground')
54 + await page.getByTestId('pg-endpoint').click()
55 + const search = page.getByTestId('pg-endpoint-search')
56 + await expect(search).toBeFocused()
57 + await search.fill('term')
58 + await expect(page.getByRole('listbox', { name: 'Endpoints' }).getByRole('option')).toHaveCount(1)
59 + await page.keyboard.press('Enter')
60 + await expect(page.getByTestId('pg-endpoint')).toContainText('Term structure')
61 + await expect(page.getByTestId('pg-url')).toContainText('/v1/futures/NG/term-structure')
62 + await expect(page).toHaveURL(/ep=futures-term-structure/)
63 +})
64 +
65 +test('run → status, latency, rows, table / chart / json / code views and rate-limit panel', async ({ page }) => {
35 66 const rows = bars(120)
36 67 await mockAnon(page, url => (url.pathname === '/v1/bars/stock/AAPL' ? json({ count: rows.length, data: rows }) : null))
37 − await page.goto('/playground?ep=stock-bars&timeframe=1min')
68 + await page.goto('/playground')
69 + await page.getByLabel(/^timeframe/).selectOption('1min')
38 70 await page.getByTestId('pg-run').click()
39 71 const status = page.getByTestId('pg-status')
40 72 await expect(status).toContainText('200 OK')
41 73 await expect(status).toContainText('120 rows')
42 74 await expect(status).toContainText(/\d+ ms/)
43 − // bars → chart by default; table + json available
75 + await expect(page.getByTestId('pg-sample')).toHaveCount(0)
44 76 await expect(page.getByRole('tab', { name: 'Chart' })).toHaveAttribute('aria-selected', 'true')
45 77 await expect(page.getByRole('img', { name: /Candlestick chart of 120 rows/ })).toBeVisible()
46 78 await page.getByRole('tab', { name: /^Table/ }).click()
@@ -50,33 +82,49 @@ test('run → status, latency, rows, table / chart / json views and rate-limit p
50 82 await expect(page.getByText(/sorted by close asc/)).toBeVisible()
51 83 await page.getByRole('tab', { name: 'JSON' }).click()
52 84 await expect(page.getByText('120 rows in data[]')).toBeVisible()
53 − // rate-limit panel from headers
54 85 const rl = page.getByRole('region', { name: 'Rate limit' })
55 86 await expect(rl).toContainText('29 / 30 left')
56 87 await expect(rl).toContainText('99,500 / 100,000 left')
57 88 await expect(rl).toContainText('Window resets in')
58 − // code export without key → no Authorization header
59 − await expect(page.locator('.pg-codex')).toContainText('curl "https://www.hfmarketdata.io/v1/bars/stock/AAPL')
60 − await expect(page.locator('.pg-codex')).not.toContainText('Authorization')
61 − await page.getByRole('tab', { name: /Python/ }).click()
62 − await expect(page.locator('.pg-codex')).toContainText('pd.DataFrame(payload["data"])')
89 + // code tab without key → no Authorization header
90 + await page.getByRole('tab', { name: 'Code' }).click()
91 + const code = page.getByTestId('pg-code')
92 + await code.getByRole('radio', { name: 'curl' }).click()
93 + await expect(code).toContainText('curl "https://www.hfmarketdata.io/v1/bars/stock/AAPL')
94 + await expect(code).not.toContainText('Authorization')
95 + await code.getByRole('radio', { name: 'Python' }).click()
96 + await expect(code).toContainText('pd.DataFrame(payload["data"])')
63 97 })
64 98
65 −test('deep link by operationId selects the catalog entry and fills params', async ({ page }) => {
66 − await mockAnon(page)
99 +test('deep link by operationId selects the catalog entry, fills params and auto-runs once', async ({ page }) => {
100 + let hits = 0
101 + await mockAnon(page, url => { if (url.pathname === '/v1/options/chain/TSLA') { hits++; return json({ count: 1, data: [{ strike: 200, delta: 0.5 }] }) } return null })
67 102 await page.goto('/playground?ep=opt_chain_v1_options_chain__ticker__get&ticker=TSLA&trade_date=2024-06-21&call_put=p')
68 − await expect(page.getByRole('button', { name: /Options chain \(Greeks\)/ })).toHaveAttribute('aria-current', 'true')
103 + await expect(page.getByTestId('pg-endpoint')).toContainText('Options chain (Greeks)')
69 104 await expect(page.getByTestId('pg-url')).toContainText('/v1/options/chain/TSLA?trade_date=2024-06-21')
70 105 await expect(page.getByTestId('pg-url')).toContainText('call_put=p')
71 − // URL is normalised to the catalog id
72 106 await expect(page).toHaveURL(/ep=options-chain/)
107 + await expect(page.getByTestId('pg-status')).toContainText('200 OK')
108 + await page.waitForTimeout(300)
109 + expect(hits).toBe(1)
73 110 })
74 111
75 −test('deep link with fixed asset picks the crypto entry', async ({ page }) => {
112 +test('deep link with fixed asset picks the crypto entry; futures deep link from the home is runnable', async ({ page }) => {
76 113 await mockAnon(page)
77 114 await page.goto('/playground?ep=bars_v1_bars__asset___ticker__get&asset=crypto&ticker=ETHUSD')
78 − await expect(page.getByRole('button', { name: /Crypto bars/ })).toHaveAttribute('aria-current', 'true')
115 + await expect(page.getByTestId('pg-endpoint')).toContainText('Crypto bars')
79 116 await expect(page.getByTestId('pg-url')).toContainText('/v1/bars/crypto/ETHUSD')
117 + // asset=futures resolves to the futures entry, dependent enums are recomputed, Run is enabled (regression: "Not an allowed value")
118 + await page.goto('/playground?ep=bars_v1_bars__asset___ticker__get&asset=futures&ticker=ES&timeframe=1day&limit=5')
119 + await expect(page.getByTestId('pg-endpoint')).toContainText('Futures continuous')
120 + await expect(page.getByTestId('pg-url')).toContainText('/v1/bars/futures/ES?adjustment=contin_adj_ratio&timeframe=1day')
121 + await expect(page.getByText('Not an allowed value')).toHaveCount(0)
122 + await expect(page.getByTestId('pg-run')).toBeEnabled()
123 + // multi-ticker with asset=futures from the URL: adjustment enum switches to futures values
124 + await page.goto('/playground?ep=bars-multi&asset=futures&tickers=ES,CL&adjustment=adj_split')
125 + await expect(page.getByText('Not an allowed value')).toHaveCount(0)
126 + await expect(page.getByLabel(/^adjustment/)).toHaveValue('')
127 + await expect(page.getByTestId('pg-run')).toBeEnabled()
80 128 })
81 129
82 130 test('unknown operationId falls back to a generic form built from /openapi.json', async ({ page }) => {
@@ -96,15 +144,18 @@ test('presets: CL chain and ES continuous', async ({ page }) => {
96 144 await expect(page.getByTestId('pg-url')).toContainText('/v1/futures/ES/continuous?roll=volume&adjust=back_adjusted&depth=1&interval=1d&from=2015-01-01&to=2025-12-31')
97 145 })
98 146
99 −test('429 shows a Retry-After countdown; missing route explains "coming soon"', async ({ page }) => {
147 +test('429 shows a countdown + free-account CTA; missing route explains "coming soon"', async ({ page }) => {
100 148 await mockAnon(page, url => {
101 149 if (url.pathname === '/v1/bars/stock/AAPL') return json({ error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests', docs: 'https://www.hfmarketdata.io/docs/errors#RATE_LIMIT_EXCEEDED' }, detail: 'Too many requests' }, 429, { 'Retry-After': '90', 'X-RateLimit-Remaining-Requests': '0' })
102 150 return null
103 151 })
104 − await page.goto('/playground?ep=stock-bars')
152 + await page.goto('/playground')
105 153 await page.getByTestId('pg-run').click()
106 154 await expect(page.getByTestId('pg-status')).toContainText('429')
107 − await expect(page.getByText('RATE_LIMIT_EXCEEDED · HTTP 429')).toBeVisible()
155 + const err = page.getByTestId('pg-error')
156 + await expect(err).toContainText('Rate limited — not down')
157 + await expect(err.locator('.countdown')).toContainText(/1 min \d+ s/)
158 + await expect(err.getByRole('link', { name: /Create free account/ })).toBeVisible()
108 159 await expect(page.getByRole('region', { name: 'Rate limit' })).toContainText(/Retry in 1 min \d+ s/)
109 160 // v2 endpoint not deployed → coming soon
110 161 await page.getByRole('button', { name: 'NG term structure' }).click()
@@ -113,13 +164,13 @@ test('429 shows a Retry-After countdown; missing route explains "coming soon"',
113 164 await expect(page.getByText(/coming soon/)).toBeVisible()
114 165 })
115 166
116 −test('validation blocks Run; WebSocket tab is disabled without a key', async ({ page }) => {
167 +test('validation blocks Run; WebSocket needs a key', async ({ page }) => {
117 168 await mockAnon(page)
118 169 await page.goto('/playground?ep=options-history')
119 170 await page.getByLabel(/^strike/).fill('')
120 171 await expect(page.getByTestId('pg-run')).toBeDisabled()
121 172 await expect(page.getByText('Required')).toBeVisible()
122 − await page.getByRole('button', { name: /WebSocket stream/ }).click()
173 + await pickEndpoint(page, 'WebSocket stream')
123 174 await expect(page.getByText('WebSocket streaming needs an API key')).toBeVisible()
124 175 await expect(page.getByRole('link', { name: 'Create free account' }).last()).toBeVisible()
125 176 })
@@ -128,10 +179,12 @@ test('CSV format gives a download and a parsed table', async ({ page }) => {
128 179 await mockAnon(page, url => (url.pathname === '/v1/bars/stock/AAPL' && url.searchParams.get('format') === 'csv'
129 180 ? { status: 200, contentType: 'text/csv', headers: { 'X-Row-Count': '2' }, body: 'ticker,datetime,open,high,low,close,volume\nAAPL,2024-06-03,1,2,0.5,1.5,100\nAAPL,2024-06-04,1.5,2.5,1,2,200\n' } : null))
130 181 await page.goto('/playground?ep=stock-bars&format=csv')
131 − await page.getByTestId('pg-run').click()
132 − await expect(page.getByTestId('pg-status')).toContainText('2 rows')
182 + await expect(page.getByTestId('pg-status')).toContainText('2 rows') // auto-run on a valid deep link
133 183 await expect(page.getByRole('link', { name: 'Download CSV' })).toBeVisible()
134 − await expect(page.locator('.pg-codex')).toContainText('-o data.csv')
135 − await page.getByRole('tab', { name: /Python/ }).click()
136 − await expect(page.locator('.pg-codex')).toContainText('pd.read_csv')
184 + await page.getByRole('tab', { name: 'Code' }).click()
185 + const code = page.getByTestId('pg-code')
186 + await code.getByRole('radio', { name: 'curl' }).click()
187 + await expect(code).toContainText('-o data.csv')
188 + await code.getByRole('radio', { name: 'Python' }).click()
189 + await expect(code).toContainText('pd.read_csv')
137 190 })
modified hfmarketdata/web/e2e/site.spec.js +107 −17
@@ -1,25 +1,49 @@
1 −// Site shell: homepage, limits page content (free platform), theme toggle persistence, status page, prerendered shells.
1 +// Site shell: homepage (no API calls except /v1/status), limits, theme, status (429 ≠ down), 404, legal, prerender, a11y.
2 2 import { expect, test } from '@playwright/test'
3 +import { json, mockAnon } from './mocks.js'
4 +
5 +const noOverflow = page => page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
3 6
4 7 test.describe('site', () => {
5 − test('homepage hero, nav and key sections', async ({ page }) => {
8 + test('homepage: hero with captured response, nav, sections, no horizontal overflow', async ({ page }) => {
9 + const calls = []
10 + await mockAnon(page, url => { calls.push(url.pathname); return null })
6 11 await page.goto('/')
7 12 await expect(page.getByRole('heading', { level: 1 })).toContainText('Open high-frequency market data')
8 13 const nav = page.getByRole('navigation', { name: 'Main' })
9 14 for (const label of ['Docs', 'Playground', 'Integrations', 'Limits', 'Status']) await expect(nav.getByRole('link', { name: label })).toBeVisible()
10 15 await expect(nav.getByRole('link', { name: /Pricing/ })).toHaveCount(0)
11 − await expect(page.getByRole('heading', { name: /Get an API key/ })).toBeVisible()
16 + // hero shows the captured AAPL response and a request in four languages
17 + await expect(page.getByTestId('hero-response')).toContainText('"ticker": "AAPL"')
18 + await expect(page.locator('.hero-code').getByRole('radio', { name: 'R', exact: true })).toBeVisible()
19 + await expect(page.getByRole('heading', { name: /Start with a URL/ })).toBeVisible()
12 20 await expect(page.getByRole('heading', { name: /A real request, a real response/ })).toBeVisible()
13 21 await expect(page.getByRole('heading', { name: /SEC EDGAR, point-in-time/ })).toBeVisible()
14 − await expect(page.getByTestId('pe-chart')).toBeVisible()
15 − // preview falls back to the captured ES response when the API is not reachable
16 − await expect(page.locator('.preview .code').nth(1)).toContainText('"ticker": "ES"')
17 22 await expect(page.getByRole('link', { name: 'Request higher limits' })).toHaveAttribute('href', /^mailto:contact@spboucher\.ai/)
23 + // counters fall back to the captured /v1/status silently (no apology text)
24 + await expect(page.getByTestId('counters')).toContainText('7,665')
25 + await expect(page.locator('main')).not.toContainText(/not reachable|did not complete/)
26 + // footer: legal + changelog + author
27 + const footer = page.getByRole('contentinfo')
28 + for (const l of ['Terms', 'Privacy', 'Data license & attribution', 'Changelog', 'Status', 'Playground']) await expect(footer.getByRole('link', { name: l })).toBeVisible()
29 + await expect(footer).toContainText('Built by Simon-Pierre Boucher')
30 + expect(await noOverflow(page)).toBe(true)
31 + // the home makes at most one API call on load: /v1/status (quota-exempt)
32 + await page.waitForTimeout(500)
33 + expect(calls.filter(p => p !== '/v1/me' && p !== '/v1/status')).toEqual([])
34 + })
35 +
36 + test('homepage charts render from bundled data when scrolled into view', async ({ page }) => {
37 + await mockAnon(page)
38 + await page.goto('/')
39 + await page.getByTestId('preview-chart').scrollIntoViewIfNeeded()
40 + await expect(page.getByTestId('preview-chart').locator('canvas').first()).toBeVisible({ timeout: 15_000 })
41 + await page.getByTestId('pe-chart').scrollIntoViewIfNeeded()
42 + await expect(page.getByTestId('pe-chart').locator('canvas').first()).toBeVisible({ timeout: 15_000 })
43 + await expect(page.locator('main')).not.toContainText(/not reachable/)
18 44 })
19 45
20 46 test('limits page: three levels with all numbers, CTAs, no paid wording', async ({ page }) => {
21 − // /pricing is the route App.jsx currently serves; it becomes a redirect to /limits (web-app coordinator), so
22 − // navigating here works before and after that change. Switch to '/limits' once the redirect is in.
23 47 await page.goto('/limits')
24 48 await expect(page.getByTestId('limits-page')).toBeVisible()
25 49 await expect(page.getByRole('heading', { level: 1 })).toHaveText('Free for everyone.')
@@ -32,16 +56,16 @@ test.describe('site', () => {
32 56 await expect(page.getByTestId('cta-signup')).toHaveAttribute('href', '/signup')
33 57 const mail = await page.getByTestId('cta-high-usage').getAttribute('href')
34 58 expect(mail).toMatch(/^mailto:contact@spboucher\.ai\?subject=/)
35 − // incentives + FAQ
36 59 await expect(page.getByText('Parquet responses count half', { exact: true })).toBeVisible()
37 60 await page.getByText('What happens on 429?').click()
38 61 await expect(page.getByText(/RATE_LIMIT_EXCEEDED/).first()).toBeVisible()
39 62 const text = (await page.locator('main').innerText()).toLowerCase()
40 63 for (const banned of ['pricing', 'paid', 'per month', '$/']) expect(text).not.toContain(banned)
41 64 await expect(page).toHaveTitle(/Access & limits — free for everyone/)
65 + expect(await noOverflow(page)).toBe(true)
42 66 })
43 67
44 − test('theme toggle switches and persists across reloads', async ({ page }) => {
68 + test('theme toggle switches and persists across reloads; code blocks stay readable in light mode', async ({ page }) => {
45 69 await page.goto('/')
46 70 const html = page.locator('html')
47 71 await expect(html).toHaveAttribute('data-theme', 'dark')
@@ -50,42 +74,108 @@ test.describe('site', () => {
50 74 expect(await page.evaluate(() => localStorage.getItem('hfmd.theme'))).toBe('light')
51 75 await page.reload()
52 76 await expect(html).toHaveAttribute('data-theme', 'light')
53 − // applied before React mounts (inline script in index.html)
54 77 const early = await page.evaluate(() => document.documentElement.getAttribute('data-theme'))
55 78 expect(early).toBe('light')
79 + // code block background follows the theme (no hard-coded dark palette)
80 + const bg = await page.getByTestId('hero-response').evaluate(el => getComputedStyle(el).backgroundColor)
81 + expect(bg).not.toBe('rgb(1, 22, 39)')
82 + const [r, g, b] = bg.match(/\d+/g).map(Number)
83 + expect(r + g + b).toBeGreaterThan(600)
56 84 await page.getByTestId('theme-toggle').click()
57 85 await expect(html).toHaveAttribute('data-theme', 'dark')
58 86 })
59 87
60 − test('status page shows health cards and handles an unreachable API', async ({ page }) => {
88 + test('status page: health cards, unreachable API', async ({ page }) => {
61 89 await page.goto('/status')
62 90 await expect(page.getByTestId('status-page')).toBeVisible()
63 91 await expect(page.getByRole('heading', { level: 1 })).toContainText('API health')
64 92 await expect(page.locator('.health-card')).toHaveCount(4)
65 − // vite preview has no API → cards settle to Down, warning callout shown, page still usable
93 + // vite preview has no API → cards settle to Down, alert shown, page still usable
66 94 await expect(page.locator('.health-card[data-state="down"]').first()).toBeVisible({ timeout: 15_000 })
67 95 await expect(page.getByRole('alert')).toContainText('did not answer')
68 96 })
69 97
70 − test('prerendered shells carry route meta and preload the route chunk', async ({ request }) => {
98 + test('status page: a 429 is reported as rate limited, not down; inventory renders from /v1/status', async ({ page }) => {
99 + await page.route('**/health', r => r.fulfill(json({ status: 'ok', service: 'hfmarketdata-api', version: '2.0.0', data_root_present: true })))
100 + await page.route('**/openapi.json', r => r.fulfill({ status: 429, contentType: 'application/json', body: '{}' }))
101 + await mockAnon(page, url => {
102 + if (url.pathname === '/v1/status') return json({ error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests' }, detail: 'Too many requests' }, 429, { 'Retry-After': '60' })
103 + return null
104 + })
105 + await page.goto('/status')
106 + await expect(page.getByTestId('status-limited')).toBeVisible()
107 + await expect(page.getByTestId('status-overall')).toContainText('rate limited')
108 + await expect(page.locator('.health-card[data-state="limited"]')).toHaveCount(2)
109 + await expect(page.locator('.health-card[data-state="down"]')).toHaveCount(0)
110 + // now a healthy inventory with a last-date field the API may add
111 + await page.unroute('**/v1/**')
112 + await mockAnon(page, url => (url.pathname === '/v1/status'
113 + ? json({ datasets: { stock: { '1day': { adj_split: 7664, UNADJUSTED: 7665, last_date: '2026-09-05' }, '1min': { adj_split: 7670 } }, options: { quarters: ['2010_q1', '2026_q3_partial'], files_latest_quarter: 6056 } } })
114 + : null))
115 + await page.getByRole('button', { name: 'Re-check' }).click()
116 + await expect(page.getByTestId('inv-stock')).toContainText('7,664')
117 + await expect(page.getByTestId('last-dates')).toContainText('2026-09-05')
118 + await expect(page.getByTestId('inv-options')).toContainText('6,056')
119 + })
120 +
121 + test('404 page has a title, search and links', async ({ page }) => {
122 + await page.goto('/this/does/not/exist')
123 + await expect(page.getByTestId('not-found')).toBeVisible()
124 + await expect(page).toHaveTitle(/Page not found/)
125 + await expect(page.getByRole('heading', { level: 1 })).toContainText('/this/does/not/exist')
126 + for (const l of ['Documentation', 'Playground', 'Status']) await expect(page.getByTestId('not-found').getByRole('link', { name: new RegExp(l) })).toBeVisible()
127 + await page.getByLabel('Search the documentation').fill('rate limits')
128 + await page.getByTestId('not-found').getByRole('button', { name: 'Search' }).click()
129 + const dialog = page.getByTestId('search-dialog')
130 + await expect(dialog).toBeVisible()
131 + await expect(dialog.getByRole('combobox')).toHaveValue('rate limits')
132 + await expect(dialog.getByRole('option').first()).toContainText(/Rate limits/i)
133 + })
134 +
135 + test('legal pages render and cross-link', async ({ page }) => {
136 + await page.goto('/terms')
137 + await expect(page.getByTestId('legal-terms')).toBeVisible()
138 + await expect(page.getByRole('heading', { level: 1 })).toHaveText('Terms of use')
139 + await expect(page.getByRole('heading', { name: /Reasonable use/ })).toBeVisible()
140 + await page.getByRole('navigation', { name: 'Legal pages' }).getByRole('link', { name: 'Privacy' }).click()
141 + await expect(page).toHaveURL(/\/privacy$/)
142 + await expect(page.locator('main')).toContainText('Resend')
143 + await expect(page.locator('main')).toContainText(/no third-party/i)
144 + await page.goto('/data-license')
145 + await expect(page.locator('main')).toContainText('FirstRate Data')
146 + await expect(page.locator('main')).toContainText('public domain')
147 + await expect(page).toHaveTitle(/Data license/)
148 + })
149 +
150 + test('prerendered shells carry route meta and preload the route chunk; every guide is prerendered', async ({ request }) => {
71 151 const limits = await (await request.get('/limits')).text()
72 152 expect(limits).toContain('<title>Access &amp; limits — free for everyone · HF Market Data</title>')
73 153 expect(limits).toContain('<link rel="canonical" href="https://www.hfmarketdata.io/limits" />')
74 − expect(limits).toMatch(/<link rel="modulepreload" crossorigin href="\/assets\/(Limits|Pricing)-[^"]+\.js">/)
154 + expect(limits).toMatch(/<link rel="modulepreload" crossorigin href="\/assets\/Limits-[^"]+\.js">/)
75 155 expect(limits).toContain('<h1>Free for everyone.</h1>')
76 156 const home = await (await request.get('/')).text()
77 157 expect(home).toMatch(/<link rel="modulepreload" crossorigin href="\/assets\/Home-[^"]+\.js">/)
78 158 expect(home).not.toContain('fonts.googleapis.com')
159 + expect(home).toContain('og.png')
160 + expect(home).toContain('rel="manifest"')
161 + const guide = await (await request.get('/docs/futures-contracts')).text()
162 + expect(guide).toContain('<title>Futures: individual contracts · HF Market Data</title>')
163 + expect(guide).toMatch(/modulepreload[^>]+futures-contracts-[^"]+\.js/)
164 + const ref = await (await request.get('/docs/reference')).text()
165 + expect(ref).toContain('<h1>API reference</h1>')
79 166 const sitemap = await (await request.get('/sitemap.xml')).text()
80 − expect(sitemap).toContain('<loc>https://www.hfmarketdata.io/docs/quickstart</loc>')
167 + for (const u of ['/docs/quickstart', '/docs/recipes/pandas-backtest', '/docs/reference', '/terms', '/privacy', '/data-license', '/playground']) expect(sitemap).toContain(`<loc>https://www.hfmarketdata.io${u}</loc>`)
168 + for (const asset of ['/og.png', '/apple-touch-icon.png', '/icon-192.png', '/manifest.webmanifest', '/fonts/jetbrains-mono-latin.woff2']) expect((await request.get(asset)).status()).toBe(200)
81 169 })
82 170
83 − test('accessibility basics: skip link, landmarks, focus-visible on nav', async ({ page }) => {
171 + test('accessibility basics: skip link, landmarks, tap targets ≥ 36 px on desktop', async ({ page }) => {
84 172 await page.goto('/limits')
85 173 await page.keyboard.press('Tab')
86 174 await expect(page.getByRole('link', { name: 'Skip to content' })).toBeFocused()
87 175 await expect(page.getByRole('banner')).toBeVisible()
88 176 await expect(page.getByRole('contentinfo')).toBeVisible()
89 177 await expect(page.getByRole('main')).toBeVisible()
178 + const small = await page.evaluate(() => [...document.querySelectorAll('header a, header button')].filter(e => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 && r.height < 36 }).length)
179 + expect(small).toBe(0)
90 180 })
91 181 })
modified hfmarketdata/web/index.html +12 −4
@@ -2,7 +2,7 @@
2 2 <html lang="en" data-theme="dark">
3 3 <head>
4 4 <meta charset="UTF-8" />
5 − <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6 6 <meta name="color-scheme" content="dark light" />
7 7 <meta name="theme-color" content="#0a0c10" media="(prefers-color-scheme: dark)" />
8 8 <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
@@ -10,14 +10,22 @@
10 10 <meta property="og:type" content="website" />
11 11 <meta property="og:site_name" content="HF Market Data" />
12 12 <meta property="og:title" content="HF Market Data — Open High-Frequency Market Data API" />
13 − <meta property="og:description" content="1-minute to daily bars, futures contracts, options chains with Greeks and point-in-time fundamentals. No signup needed to start." />
14 − <meta property="og:image" content="https://www.hfmarketdata.io/og.svg" />
13 + <meta property="og:description" content="1-minute to daily bars, futures contracts, options chains with Greeks and point-in-time fundamentals. Free, no signup needed to start." />
14 + <meta property="og:image" content="https://www.hfmarketdata.io/og.png" />
15 + <meta property="og:image:width" content="1200" />
16 + <meta property="og:image:height" content="630" />
17 + <meta property="og:image:alt" content="HF Market Data — open high-frequency market data API" />
15 18 <meta name="twitter:card" content="summary_large_image" />
19 + <meta name="twitter:image" content="https://www.hfmarketdata.io/og.png" />
16 20 <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
21 + <link rel="icon" href="/icon-32.png" type="image/png" sizes="32x32" />
22 + <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
23 + <link rel="manifest" href="/manifest.webmanifest" />
24 + <link rel="preload" href="/fonts/jetbrains-mono-latin.woff2" as="font" type="font/woff2" crossorigin />
17 25 <link rel="canonical" href="https://www.hfmarketdata.io/" />
18 26 <title>HF Market Data — Open High-Frequency Market Data API</title>
19 27 <script>
20 − // Theme is resolved before first paint (no flash): localStorage → system preference → dark.
28 + // Theme is resolved before first paint (no flash): localStorage → dark.
21 29 try {
22 30 var t = localStorage.getItem('hfmd.theme');
23 31 if (t !== 'light' && t !== 'dark') t = 'dark';
added hfmarketdata/web/public/apple-touch-icon.png +0 −0

Binary file not shown.

modified hfmarketdata/web/public/favicon.svg +2 −2
@@ -1,6 +1,6 @@
1 1 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2 − <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5ee7a5"/><stop offset="1" stop-color="#4db8ff"/></linearGradient></defs>
2 + <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#60a5fa"/></linearGradient></defs>
3 3 <rect width="64" height="64" rx="14" fill="#0a0c10"/>
4 4 <path d="M10 44 L20 30 L28 36 L38 20 L46 26 L54 14" fill="none" stroke="url(#g)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
5 − <rect x="10" y="50" width="44" height="4" rx="2" fill="#5ee7a5" opacity=".5"/>
5 + <rect x="10" y="50" width="44" height="4" rx="2" fill="#34d399" opacity=".5"/>
6 6 </svg>
added hfmarketdata/web/public/fonts/jetbrains-mono-latin.woff2 +0 −0

Binary file not shown.

added hfmarketdata/web/public/icon-192.png +0 −0

Binary file not shown.

added hfmarketdata/web/public/icon-32.png +0 −0

Binary file not shown.

added hfmarketdata/web/public/icon-512.png +0 −0

Binary file not shown.

modified hfmarketdata/web/public/logo.svg +1 −1
@@ -1,5 +1,5 @@
1 1 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 64" width="320" height="64" role="img" aria-label="HF Market Data">
2 − <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5ee7a5"/><stop offset="1" stop-color="#4db8ff"/></linearGradient></defs>
2 + <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#60a5fa"/></linearGradient></defs>
3 3 <rect width="64" height="64" rx="14" fill="#0a0c10"/>
4 4 <path d="M10 44 L20 30 L28 36 L38 20 L46 26 L54 14" fill="none" stroke="url(#g)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
5 5 <text x="80" y="41" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="28" font-weight="650" fill="#e8ebf1" letter-spacing="-0.5">HF Market Data</text>
added hfmarketdata/web/public/manifest.webmanifest +14 −0
@@ -0,0 +1,14 @@
1 +{
2 + "name": "HF Market Data",
3 + "short_name": "HF Market Data",
4 + "description": "Open high-frequency market data API — free, 1-minute to daily bars, futures contracts, options chains with Greeks, SEC EDGAR fundamentals.",
5 + "start_url": "/",
6 + "display": "standalone",
7 + "background_color": "#0a0c10",
8 + "theme_color": "#0a0c10",
9 + "icons": [
10 + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
11 + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" },
12 + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
13 + ]
14 +}
added hfmarketdata/web/public/og.png +0 −0

Binary file not shown.

deleted hfmarketdata/web/public/og.svg +0 −16
@@ -1,16 +0,0 @@
1 −<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" width="1200" height="630" role="img" aria-label="HF Market Data — Open High-Frequency Market Data API">
2 − <defs>
3 − <linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#5ee7a5"/><stop offset="1" stop-color="#4db8ff"/></linearGradient>
4 − <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#0a0c10"/><stop offset="1" stop-color="#141a24"/></linearGradient>
5 − </defs>
6 − <rect width="1200" height="630" fill="url(#bg)"/>
7 − <g stroke="#232833" stroke-width="1">
8 − <path d="M0 105H1200M0 210H1200M0 315H1200M0 420H1200M0 525H1200"/>
9 − </g>
10 − <path d="M80 470 L200 380 L300 420 L420 300 L540 340 L660 220 L780 280 L900 160 L1020 200 L1120 110" fill="none" stroke="url(#g)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" opacity=".85"/>
11 − <rect x="80" y="80" width="72" height="72" rx="16" fill="#0a0c10" stroke="#313847"/>
12 − <path d="M92 130 L104 112 L114 120 L126 100 L136 108 L142 92" fill="none" stroke="url(#g)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
13 − <text x="172" y="132" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="44" font-weight="700" fill="#e8ebf1" letter-spacing="-1">HF Market Data</text>
14 − <text x="80" y="560" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" font-size="34" fill="#aeb6c4">Open high-frequency market data API · 1-minute to daily since 2010</text>
15 − <text x="80" y="600" font-family="ui-monospace, Menlo, Consolas, monospace" font-size="24" fill="#5ee7a5">curl https://www.hfmarketdata.io/v1/bars/stock/AAPL?timeframe=1min</text>
16 −</svg>
added hfmarketdata/web/scripts/make-brand-assets.mjs +72 −0
@@ -0,0 +1,72 @@
1 +#!/usr/bin/env node
2 +// Renders public/og.png (1200×630), public/apple-touch-icon.png (180) and public/icon-{32,192,512}.png with Playwright
3 +// from self-contained HTML (design tokens inlined, no network). Run after changing the brand: node scripts/make-brand-assets.mjs
4 +import { mkdirSync, writeFileSync } from 'node:fs'
5 +import { dirname, join, resolve } from 'node:path'
6 +import { fileURLToPath } from 'node:url'
7 +import { chromium } from 'playwright'
8 +
9 +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
10 +const PUB = join(ROOT, 'public')
11 +mkdirSync(PUB, { recursive: true })
12 +
13 +const MARK = (size, radius) => `
14 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="${size}" height="${size}">
15 + <rect width="64" height="64" rx="${radius}" fill="#0a0c10"/>
16 + <path d="M10 44 L20 30 L28 36 L38 20 L46 26 L54 14" fill="none" stroke="#34d399" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
17 + <rect x="10" y="50" width="44" height="4" rx="2" fill="#34d399" opacity=".55"/>
18 + </svg>`
19 +
20 +const SANS = `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`
21 +const MONO = `ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace`
22 +
23 +const OG = `<!doctype html><html><head><meta charset="utf-8"><style>
24 + html, body { margin: 0; width: 1200px; height: 630px; background: #0a0c10; color: #e8ebf1; font-family: ${SANS}; -webkit-font-smoothing: antialiased; }
25 + .wrap { position: relative; width: 1200px; height: 630px; overflow: hidden; background: radial-gradient(60% 70% at 15% 20%, rgba(52,211,153,.16), transparent 70%), radial-gradient(45% 60% at 85% 80%, rgba(96,165,250,.14), transparent 70%), #0a0c10; }
26 + .grid { position: absolute; inset: 0; background-image: linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px); background-size: 60px 60px; }
27 + .brand { position: absolute; left: 72px; top: 64px; display: flex; align-items: center; gap: 16px; font-weight: 650; font-size: 30px; letter-spacing: -.01em; }
28 + .brand svg { border-radius: 14px; }
29 + h1 { position: absolute; left: 72px; top: 170px; margin: 0; font-size: 66px; line-height: 1.08; letter-spacing: -.035em; font-weight: 700; max-width: 680px; }
30 + h1 em { font-style: normal; color: #34d399; }
31 + p { position: absolute; left: 72px; top: 400px; margin: 0; font-size: 26px; line-height: 1.4; color: #b4bcc8; max-width: 640px; }
32 + .tags { position: absolute; left: 72px; bottom: 60px; display: flex; gap: 12px; }
33 + .tag { font-family: ${MONO}; font-size: 20px; color: #e8ebf1; background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.14); padding: 8px 16px; border-radius: 999px; }
34 + .code { position: absolute; right: 64px; top: 120px; width: 400px; background: #0c0f15; border: 1px solid #313847; border-radius: 16px; box-shadow: 0 30px 80px -30px rgba(0,0,0,.8); overflow: hidden; }
35 + .code-head { padding: 12px 16px; border-bottom: 1px solid #232833; background: #10131a; font-family: ${MONO}; font-size: 15px; color: #8f98a8; display: flex; justify-content: space-between; }
36 + .code pre { margin: 0; padding: 14px 18px; font-family: ${MONO}; font-size: 16px; line-height: 1.45; color: #d9dee8; }
37 + .k { color: #7fd1ff; } .s { color: #9fe7c0; } .n { color: #ffcf7a; } .c { color: #7d8797; }
38 + .chart { position: absolute; right: 64px; bottom: 52px; width: 400px; height: 90px; }
39 +</style></head><body><div class="wrap"><div class="grid"></div>
40 + <div class="brand">${MARK(56, 14)}HF Market Data</div>
41 + <h1>Open high-frequency market data. <em>Free.</em></h1>
42 + <p>1-minute to daily bars, futures contracts, options chains with Greeks, SEC fundamentals — one URL, no signup to start.</p>
43 + <div class="tags"><span class="tag">JSON · CSV · Parquet</span><span class="tag">since 2010</span><span class="tag">MCP · Python · R · JS</span></div>
44 + <div class="code"><div class="code-head"><span>GET /v1/bars/stock/AAPL</span><span>200 OK</span></div><pre>{
45 + <span class="k">"count"</span>: <span class="n">5</span>,
46 + <span class="k">"data"</span>: [{
47 + <span class="k">"datetime"</span>: <span class="s">"2024-06-03"</span>,
48 + <span class="k">"open"</span>: <span class="n">191.2434</span>,
49 + <span class="k">"high"</span>: <span class="n">193.3154</span>,
50 + <span class="k">"low"</span>: <span class="n">190.8666</span>,
51 + <span class="k">"close"</span>: <span class="n">192.3637</span>,
52 + <span class="k">"volume"</span>: <span class="n">50080500</span>
53 + }, <span class="c">…</span>]
54 +}</pre></div>
55 + <svg class="chart" viewBox="0 0 400 110" preserveAspectRatio="none" fill="none">
56 + ${[[10,70,60,'#2fbf71'],[45,40,55,'#e5484d'],[80,50,75,'#2fbf71'],[115,30,50,'#e5484d'],[150,45,80,'#2fbf71'],[185,60,90,'#2fbf71'],[220,35,65,'#e5484d'],[255,50,85,'#2fbf71'],[290,65,95,'#2fbf71'],[325,40,70,'#e5484d'],[360,55,100,'#2fbf71']].map(([x,y1,y2,c]) => `<line x1="${x+9}" x2="${x+9}" y1="${110-y2-8}" y2="${110-y1+8}" stroke="${c}" stroke-width="2"/><rect x="${x}" y="${110-y2}" width="18" height="${y2-y1}" rx="2" fill="${c}"/>`).join('')}
57 + </svg>
58 +</div></body></html>`
59 +
60 +const ICON = (size, radius) => `<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;background:transparent}svg{display:block}</style></head><body>${MARK(size, radius)}</body></html>`
61 +
62 +const browser = await chromium.launch()
63 +const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1 })
64 +await page.setContent(OG, { waitUntil: 'load' })
65 +writeFileSync(join(PUB, 'og.png'), await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 1200, height: 630 } }))
66 +for (const [file, size, radius] of [['apple-touch-icon.png', 180, 0], ['icon-32.png', 32, 7], ['icon-192.png', 192, 40], ['icon-512.png', 512, 108]]) {
67 + await page.setViewportSize({ width: size, height: size })
68 + await page.setContent(ICON(size, radius), { waitUntil: 'load' })
69 + writeFileSync(join(PUB, file), await page.screenshot({ type: 'png', omitBackground: radius > 0, clip: { x: 0, y: 0, width: size, height: size } }))
70 +}
71 +await browser.close()
72 +console.log('wrote public/og.png, apple-touch-icon.png, icon-32/192/512.png')
modified hfmarketdata/web/scripts/prerender.mjs +63 −35
@@ -1,16 +1,14 @@
1 1 #!/usr/bin/env node
2 −// Post-build prerender of static HTML shells for the public entry routes.
2 +// Post-build prerender of static HTML shells for every public entry route: home, limits, status, playground,
3 +// integrations, legal pages, docs overview, reference index, errors, and EVERY guide (read from src/docs/guides.js +
4 +// the guide's `meta` export). See README "Prerender" for the trade-off versus full SSR.
3 5 //
4 −// Trade-off (see README "Prerender"): a full SSR of the React tree would need a second SSR build + a Node render step
5 −// for every route (lazy routes, MDX, lightweight-charts, browser-only APIs), for pages that are mostly fetched live.
6 −// Instead we emit, per route, a copy of dist/index.html with: route-specific <title>/<meta>/<link rel=canonical>,
7 −// <link rel=modulepreload> for the route's chunk graph and <link rel=stylesheet> for its CSS (kills the lazy-route
8 −// waterfall), and the above-the-fold content (header + h1 + lead) as static markup inside #root so first paint is
9 −// meaningful before React mounts. React's createRoot() then replaces it with the live tree.
10 −//
11 −// Serving: vite preview / any static host resolves /limits → dist/limits/index.html. The FastAPI SPA fallback currently
12 −// serves index.html for directories; it needs a one-line `candidate / "index.html"` check to pick these up.
13 −import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6 +// Per route we emit a copy of dist/index.html with route-specific <title>/<meta>/<link rel=canonical>,
7 +// <link rel=modulepreload> for the route's chunk graph + <link rel=stylesheet> for its CSS (kills the lazy-route
8 +// waterfall), and the above-the-fold content (header + h1 + lead) inside #root so first paint is meaningful before React
9 +// mounts. Serving: vite preview / static hosts resolve /limits → dist/limits/index.html (and dist/limits.html); the
10 +// FastAPI SPA fallback tries candidate, candidate.html and candidate/index.html.
11 +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
14 12 import { dirname, join, resolve } from 'node:path'
15 13 import { fileURLToPath } from 'node:url'
16 14
@@ -20,29 +18,59 @@ const SITE = 'https://www.hfmarketdata.io'
20 18
21 19 const NAV = [['/docs', 'Docs'], ['/playground', 'Playground'], ['/integrations', 'Integrations'], ['/limits', 'Limits'], ['/status', 'Status']]
22 20
23 −const ROUTES = [
21 +const HOME_LEAD = 'Stocks, ETFs, futures — continuous series and every individual contract — crypto, indices and FX, plus full options chains with Greeks and point-in-time SEC fundamentals. One base URL, JSON / CSV / Parquet, docs generated from the spec, a playground, and limits that only grow.'
22 +
23 +const STATIC_ROUTES = [
24 24 { path: '/', entry: 'src/pages/home/Home.jsx', title: 'HF Market Data — Open High-Frequency Market Data API',
25 25 description: 'Free open API for high-frequency historical market data: stocks, ETFs, futures (continuous and individual contracts), crypto, indices, FX from 1-minute to daily, options chains with Greeks and SEC EDGAR fundamentals since 2010.',
26 − h1: 'Open high-frequency market data. 1-minute to daily, since 2010.',
27 − lead: 'Stocks, ETFs, futures — continuous series and every individual contract — crypto, indices and FX, plus full options chains with Greeks and point-in-time SEC fundamentals. One base URL, JSON / CSV / Parquet, generated docs, a playground, and limits that only grow.', cls: 'hero' },
28 − // entry falls back to the temporary src/pages/pricing shim until App.jsx imports Limits.jsx directly
29 − { path: '/limits', entry: ['src/pages/limits/Limits.jsx', 'src/pages/pricing/Pricing.jsx'], title: 'Access & limits — free for everyone · HF Market Data',
26 + h1: 'Open high-frequency market data. 1-minute to daily, since 2010.', lead: HOME_LEAD, cls: 'hero' },
27 + { path: '/limits', entry: 'src/pages/limits/Limits.jsx', title: 'Access & limits — free for everyone · HF Market Data',
30 28 description: 'HF Market Data is free. Keyless access is very limited, a free account gives much higher limits, and higher limits are granted on request by e-mail.',
31 29 h1: 'Free for everyone.', lead: 'There is nothing to buy. Keyless access is deliberately small so you can try things without signing up; a free account raises the limits a lot; and if you need more, you simply ask.' },
32 − { path: '/docs', entry: 'src/pages/docs/Docs.jsx', title: 'Documentation · HF Market Data',
33 − description: 'HF Market Data documentation: quickstart, authentication, rate limits, futures contracts, options, fundamentals, and the full API reference generated from OpenAPI.',
34 − h1: 'Documentation', lead: 'Historical market data from 1-minute to daily since 2010 — stocks, ETFs, futures, crypto, indices, FX — plus options chains with Greeks and SEC EDGAR fundamentals. One base URL, JSON / CSV / Parquet, no signup to start.' },
35 − { path: '/docs/quickstart', entry: 'src/pages/docs/Docs.jsx', title: 'Quickstart · HF Market Data',
36 − description: 'Your first HF Market Data request in 30 seconds — no signup, no key.', h1: 'Quickstart',
37 − lead: 'You can pull real data right now, with nothing but a URL. No account, no key, no SDK.' },
38 30 { path: '/status', entry: 'src/pages/status/Status.jsx', title: 'Status · HF Market Data',
39 31 description: 'Live health and dataset inventory of the HF Market Data API.', h1: 'API health & dataset inventory',
40 32 lead: 'Everything on this page is fetched live from the API when you open it — nothing is cached or hand-maintained.' },
33 + { path: '/playground', entry: 'src/pages/playground/Playground.jsx', title: 'Playground · HF Market Data',
34 + description: 'Run any HF Market Data request in the browser: pick an endpoint, edit parameters, inspect JSON, table or chart, copy the code.', h1: 'Playground',
35 + lead: 'Pick an endpoint, edit the parameters, run it against the live API and inspect the result as JSON, table or chart — then copy the code.' },
36 + { path: '/integrations', entry: 'src/pages/integrations/Integrations.jsx', title: 'Integrations — MCP server, Claude Code, Cursor, Codex, skills · HF Market Data',
37 + description: 'Use HF Market Data from Claude Code, Cursor, Codex and any MCP client: one MCP server with 14 read-only tools, plus a skills pack with ready-to-run Python.', h1: 'Market data where your agent already works.',
38 + lead: 'One MCP server gives Claude Code, Cursor, Codex and Claude Desktop 14 read-only tools over the whole HF Market Data API. Plus five Claude skills with ready-to-run Python.' },
39 + { path: '/terms', entry: 'src/pages/legal/Legal.jsx', title: 'Terms of use · HF Market Data', description: 'Terms of use of the HF Market Data API and website — a free platform, offered as is, with reasonable-use limits.', h1: 'Terms of use', lead: 'HF Market Data is a free, open API and website for historical market data. Using it means you accept these short terms.' },
40 + { path: '/privacy', entry: 'src/pages/legal/Legal.jsx', title: 'Privacy · HF Market Data', description: 'What HF Market Data stores about you (account e-mail, hashed keys, request counters), what it does not (no third-party tracking), and how to delete it.', h1: 'Privacy', lead: 'An e-mail address if you create an account, hashed API keys, request counters — and nothing for advertising.' },
41 + { path: '/data-license', entry: 'src/pages/legal/Legal.jsx', title: 'Data license & attribution · HF Market Data', description: 'Where the data comes from (FirstRate Data, SEC EDGAR), what you may do with it, and how to attribute it.', h1: 'Data license & attribution', lead: 'Market data from FirstRate Data, fundamentals from SEC EDGAR — what you may do with each, and how to give credit.' },
42 + { path: '/docs', entry: 'src/pages/docs/Docs.jsx', title: 'Documentation · HF Market Data',
43 + description: 'HF Market Data documentation: quickstart, authentication, rate limits, futures contracts, options, fundamentals, and the full API reference generated from OpenAPI.',
44 + h1: 'Documentation', lead: 'Historical market data from 1-minute to daily since 2010 — stocks, ETFs, futures, crypto, indices, FX — plus options chains with Greeks and SEC EDGAR fundamentals. One base URL, JSON / CSV / Parquet, no signup to start.' },
45 + { path: '/docs/reference', entry: 'src/pages/docs/Docs.jsx', title: 'API reference · HF Market Data',
46 + description: 'Every endpoint of the HF Market Data API, generated from the OpenAPI 3.1 specification.', h1: 'API reference',
47 + lead: 'Every endpoint, generated from openapi.json. Each page shows parameters, real examples, error codes and a one-click Try it.' },
48 + { path: '/docs/errors', entry: 'src/pages/docs/Docs.jsx', title: 'Error codes · HF Market Data',
49 + description: 'Every error code returned by the HF Market Data API, its HTTP status, meaning and how to fix it.', h1: 'Errors',
50 + lead: 'Every error response — on every endpoint, v1 and v2 — has the same JSON envelope. The error.docs URL points to the matching anchor on this page.' },
41 51 ]
42 52
43 53 const esc = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;')
44 54
45 −function chunkGraph(manifest, entry) {
55 +/** Guides from src/docs/guides.js (slug/title/file/summary) + the MDX `meta.description` when present. */
56 +function guideRoutes() {
57 + const src = readFileSync(join(ROOT, 'src/docs/guides.js'), 'utf8')
58 + const re = /\{\s*slug:\s*'([^']+)',\s*title:\s*'((?:\\'|[^'])+)',\s*section:\s*'[^']+',\s*file:\s*'([^']+)',\s*summary:\s*'((?:\\'|[^'])+)'/g
59 + const out = []
60 + for (const m of src.matchAll(re)) {
61 + const [, slug, title, file, summary] = m.map(x => (x || '').replace(/\\'/g, "'"))
62 + const mdxPath = file === '__changelog' ? join(ROOT, 'content/changelog.mdx') : join(ROOT, `content/guides/${file}.mdx`)
63 + let description = summary
64 + if (existsSync(mdxPath)) {
65 + const meta = /export const meta = \{[^}]*description:\s*'((?:\\'|[^'])+)'/.exec(readFileSync(mdxPath, 'utf8'))
66 + if (meta) description = meta[1].replace(/\\'/g, "'")
67 + }
68 + out.push({ path: `/docs/${slug}`, entry: 'src/pages/docs/Docs.jsx', title: `${title} · HF Market Data`, description, h1: title, lead: description, guide: file })
69 + }
70 + return out
71 +}
72 +
73 +function chunkGraph(manifest, entry, extra = []) {
46 74 const files = new Set(), css = new Set()
47 75 const walk = key => {
48 76 const m = manifest[key]
@@ -53,13 +81,15 @@ function chunkGraph(manifest, entry) {
53 81 }
54 82 const entries = Array.isArray(entry) ? entry : [entry]
55 83 walk(entries.find(e => manifest[e]) || entries[0])
84 + for (const e of extra) walk(e)
56 85 return { files: [...files], css: [...css] }
57 86 }
58 87
59 88 function shell(route) {
60 89 const nav = NAV.map(([to, label]) => `<a href="${to}"${route.path.startsWith(to) ? ' class="active"' : ''}>${label}</a>`).join('')
90 + const hero = route.cls === 'hero'
61 91 return `<div class="shell"><header class="topbar"><a class="brand" href="/"><span class="brand-mark">HF</span><span class="brand-name">Market Data</span></a><nav class="topnav" aria-label="Main">${nav}</nav></header>` +
62 − `<main class="${route.cls === 'hero' ? 'home' : 'page'}"><section class="${route.cls === 'hero' ? 'hero' : 'limits-head'}" style="padding-top:${route.cls === 'hero' ? '72px' : '0'}"><div class="hero-inner"><h1>${esc(route.h1)}</h1><p class="lead">${esc(route.lead)}</p></div></section></main></div>`
92 + `<main class="${hero ? 'home' : 'page'}"><section class="${hero ? 'hero' : 'prerender-head'}"><div class="hero-inner"><h1>${esc(route.h1)}</h1><p class="lead">${esc(route.lead)}</p></div></section></main></div>`
63 93 }
64 94
65 95 function main() {
@@ -68,13 +98,15 @@ function main() {
68 98 const base = readFileSync(indexPath, 'utf8')
69 99 const manifestPath = join(DIST, '.vite', 'manifest.json')
70 100 const manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, 'utf8')) : {}
101 + const routes = [...STATIC_ROUTES, ...guideRoutes()]
71 102 let count = 0
72 − for (const route of ROUTES) {
73 − const { files, css } = chunkGraph(manifest, route.entry)
103 + for (const route of routes) {
104 + const extra = route.guide ? [route.guide === '__changelog' ? 'content/changelog.mdx' : `content/guides/${route.guide}.mdx`] : []
105 + const { files, css } = chunkGraph(manifest, route.entry, extra)
74 106 const alreadyLinked = f => base.includes(`/${f}`)
75 107 const preload = files.filter(f => !alreadyLinked(f)).map(f => `<link rel="modulepreload" crossorigin href="/${f}">`).join('')
76 108 const styles = css.filter(f => !alreadyLinked(f)).map(f => `<link rel="stylesheet" href="/${f}">`).join('')
77 − let html = base
109 + const html = base
78 110 .replace(/<title>[^<]*<\/title>/, `<title>${esc(route.title)}</title>`)
79 111 .replace(/<meta name="description" content="[^"]*"\s*\/?>/, `<meta name="description" content="${esc(route.description)}" />`)
80 112 .replace(/<meta property="og:title" content="[^"]*"\s*\/?>/, `<meta property="og:title" content="${esc(route.title)}" />`)
@@ -82,18 +114,14 @@ function main() {
82 114 .replace(/<link rel="canonical" href="[^"]*"\s*\/?>/, `<link rel="canonical" href="${SITE}${route.path === '/' ? '/' : route.path}" />`)
83 115 .replace('</head>', `${styles}${preload}<meta property="og:url" content="${SITE}${route.path}" /></head>`)
84 116 .replace('<div id="root"></div>', `<div id="root">${shell(route)}</div>`)
85 − // Two file forms so any static server picks the shell up: <route>/index.html (nginx try_files, Netlify, S3)
86 − // and <route>.html (vite preview's html fallback, GitHub Pages-style hosts).
117 + // Two file forms so any static server picks the shell up: <route>/index.html and <route>.html.
87 118 const outs = route.path === '/' ? [indexPath] : [join(DIST, route.path.replace(/^\//, ''), 'index.html'), join(DIST, `${route.path.replace(/^\//, '')}.html`)]
88 119 for (const out of outs) { mkdirSync(dirname(out), { recursive: true }); writeFileSync(out, html) }
89 120 count++
90 − console.log(`prerendered ${route.path} → ${outs.map(o => o.replace(DIST + '/', '')).join(', ')} (${files.length} chunks preloaded, ${css.length} css)`)
91 121 }
92 − // sitemap for the static routes + guides known at build time
93 − const guides = Object.keys(manifest).filter(k => k.startsWith('content/guides/')).map(k => k.replace('content/guides/', '').replace(/\.mdx$/, ''))
94 − const urls = [...ROUTES.map(r => r.path), '/docs/reference', '/docs/errors', '/docs/changelog', ...guides.map(g => `/docs/${g.replace(/^recipe-/, 'recipes/').replace(/^fundamentals-/, 'fundamentals/')}`)]
95 − writeFileSync(join(DIST, 'sitemap.xml'), `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${[...new Set(urls)].map(u => ` <url><loc>${SITE}${u}</loc></url>`).join('\n')}\n</urlset>\n`)
96 − console.log(`${count} shells + sitemap.xml (${urls.length} urls)`)
122 + const urls = [...new Set(routes.map(r => r.path))]
123 + writeFileSync(join(DIST, 'sitemap.xml'), `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map(u => ` <url><loc>${SITE}${u}</loc></url>`).join('\n')}\n</urlset>\n`)
124 + console.log(`prerendered ${count} shells (${routes.filter(r => r.guide).length} guides) + sitemap.xml (${urls.length} urls)`)
97 125 }
98 126
99 127 main()
modified hfmarketdata/web/src/App.jsx +9 −2
@@ -1,5 +1,7 @@
1 −// Route table — one lazy chunk per area. Owners: web-core (home, limits, docs, status, layout),
1 +// Route table — one lazy chunk per area. Owners: web-core (home, limits, docs, status, legal, layout, 404),
2 2 // web-app (playground, auth, dashboard, admin), mcp-skills (integrations).
3 +// Public routes (mirrored by the server's SPA whitelist): / /docs/* /playground /integrations/* /limits /pricing
4 +// /status /terms /privacy /data-license /changelog (→ /docs/changelog) + auth/dashboard/admin.
3 5 import React, { Suspense, lazy } from 'react'
4 6 import { Navigate, Route, Routes } from 'react-router-dom'
5 7 import Layout from './app/Layout.jsx'
@@ -10,10 +12,11 @@ const Playground = lazy(() => import('./pages/playground/Playground.jsx'))
10 12 const Integrations = lazy(() => import('./pages/integrations/Integrations.jsx'))
11 13 const Limits = lazy(() => import('./pages/limits/Limits.jsx'))
12 14 const Status = lazy(() => import('./pages/status/Status.jsx'))
15 +const Legal = lazy(() => import('./pages/legal/Legal.jsx'))
16 +const NotFound = lazy(() => import('./pages/NotFound.jsx'))
13 17 const Auth = lazy(() => import('./pages/auth/Auth.jsx'))
14 18 const Dashboard = lazy(() => import('./pages/dashboard/Dashboard.jsx'))
15 19 const Admin = lazy(() => import('./pages/admin/Admin.jsx'))
16 −const NotFound = () => <main className="page narrow"><h1>404</h1><p className="muted">This page does not exist.</p></main>
17 20
18 21 export default function App() {
19 22 return (
@@ -27,6 +30,10 @@ export default function App() {
27 30 <Route path="/limits" element={<Limits />} />
28 31 <Route path="/pricing" element={<Navigate to="/limits" replace />} />
29 32 <Route path="/status" element={<Status />} />
33 + <Route path="/changelog" element={<Navigate to="/docs/changelog" replace />} />
34 + <Route path="/terms" element={<Legal page="terms" />} />
35 + <Route path="/privacy" element={<Legal page="privacy" />} />
36 + <Route path="/data-license" element={<Legal page="data-license" />} />
30 37 <Route path="/signin" element={<Auth mode="signin" />} />
31 38 <Route path="/signup" element={<Auth mode="signup" />} />
32 39 <Route path="/verify" element={<Auth mode="verify" />} />
modified hfmarketdata/web/src/app/Layout.jsx +24 −8
@@ -1,4 +1,5 @@
1 1 // App shell: top bar (nav · search · theme · session), mobile drawer, footer. Owner: web-core.
2 +// Header is 56 px; every nav target is ≥ 36 px on desktop and ≥ 44 px on touch viewports (theme.css --tap).
2 3 import React, { Suspense, lazy, useCallback, useEffect, useState } from 'react'
3 4 import { Link, NavLink, useLocation } from 'react-router-dom'
4 5 import { useAuth } from './auth.jsx'
@@ -28,6 +29,7 @@ export const NAV = [
28 29 ]
29 30
30 31 export const REPO_URL = 'https://git.spboucher.ai/hfmarketdata.git'
32 +export const AUTHOR = 'Simon-Pierre Boucher'
31 33
32 34 const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent)
33 35
@@ -36,15 +38,24 @@ export default function Layout({ children }) {
36 38 const { theme, toggle } = useTheme()
37 39 const [drawer, setDrawer] = useState(false)
38 40 const [searchOpen, setSearchOpen] = useState(false)
41 + const [searchSeed, setSearchSeed] = useState('')
39 42 const location = useLocation()
40 − const openSearch = useCallback(() => setSearchOpen(true), [])
43 + const openSearch = useCallback(() => { setSearchSeed(''); setSearchOpen(true) }, [])
41 44 useSearchShortcut(openSearch)
45 + // Other pages (404) can open the search dialog with a query: window.dispatchEvent(new CustomEvent('hfmd:search', { detail }))
46 + useEffect(() => {
47 + const on = e => { setSearchSeed(typeof e.detail === 'string' ? e.detail : ''); setSearchOpen(true) }
48 + window.addEventListener('hfmd:search', on)
49 + return () => window.removeEventListener('hfmd:search', on)
50 + }, [])
42 51 useEffect(() => { setDrawer(false) }, [location.pathname])
43 52 useEffect(() => {
44 − if (!drawer) return
53 + if (!drawer) return undefined
45 54 const onKey = e => { if (e.key === 'Escape') setDrawer(false) }
46 55 window.addEventListener('keydown', onKey)
47 − return () => window.removeEventListener('keydown', onKey)
56 + const prev = document.body.style.overflow
57 + document.body.style.overflow = 'hidden'
58 + return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = prev }
48 59 }, [drawer])
49 60 // Scroll to top on route change (hash links handled by the docs pages).
50 61 useEffect(() => { if (!location.hash) window.scrollTo(0, 0) }, [location.pathname, location.hash])
@@ -87,6 +98,7 @@ export default function Layout({ children }) {
87 98 </div>
88 99 <NavLink to="/" end className="navlink">Home</NavLink>
89 100 {NAV.map(([to, label]) => <NavLink key={to} to={to} className="navlink">{label}</NavLink>)}
101 + <NavLink to="/docs/changelog" className="navlink">Changelog</NavLink>
90 102 <div className="drawer-foot">
91 103 {user ? <Link to="/dashboard" className="btn">Dashboard</Link> : <><Link to="/signin" className="btn">Sign in</Link><Link to="/signup" className="btn btn-primary">Create free account</Link></>}
92 104 </div>
@@ -100,16 +112,20 @@ export default function Layout({ children }) {
100 112 <div className="footer-inner">
101 113 <div>
102 114 <div className="brand" style={{ marginBottom: 10 }}><span className="brand-mark" aria-hidden="true">HF</span>Market Data</div>
103 − <p className="muted" style={{ maxWidth: 320 }}>Open high-frequency market data API. 1-minute to daily bars since 2010, futures contracts, options chains with Greeks, SEC EDGAR fundamentals.</p>
115 + <p className="muted" style={{ maxWidth: 320 }}>Open high-frequency market data API. 1-minute to daily bars, futures contracts, options chains with Greeks, SEC EDGAR fundamentals. Free for everyone.</p>
104 116 <p className="muted small">Base URL <code>{PUBLIC_BASE}</code></p>
105 117 </div>
106 118 <div>
107 119 <h4>Product</h4>
108 − <ul><li><Link to="/docs">Documentation</Link></li><li><Link to="/docs/reference">API reference</Link></li><li><Link to="/playground">Playground</Link></li><li><Link to="/integrations">Integrations</Link></li><li><Link to="/limits">Access &amp; limits</Link></li></ul>
120 + <ul><li><Link to="/docs">Docs</Link></li><li><Link to="/docs/reference">API reference</Link></li><li><Link to="/playground">Playground</Link></li><li><Link to="/integrations">Integrations</Link></li><li><Link to="/limits">Limits</Link></li></ul>
109 121 </div>
110 122 <div>
111 123 <h4>Resources</h4>
112 − <ul><li><Link to="/docs/quickstart">Quickstart</Link></li><li><Link to="/docs/changelog">Changelog</Link></li><li><Link to="/docs/errors">Error codes</Link></li><li><Link to="/status">Status</Link></li><li><a href={`${PUBLIC_BASE}/openapi.json`}>openapi.json</a></li></ul>
124 + <ul><li><Link to="/docs/quickstart">Quickstart</Link></li><li><Link to="/status">Status</Link></li><li><Link to="/docs/changelog">Changelog</Link></li><li><Link to="/docs/errors">Error codes</Link></li><li><a href={`${PUBLIC_BASE}/openapi.json`}>openapi.json</a></li></ul>
125 + </div>
126 + <div>
127 + <h4>Legal</h4>
128 + <ul><li><Link to="/terms">Terms</Link></li><li><Link to="/privacy">Privacy</Link></li><li><Link to="/data-license">Data license &amp; attribution</Link></li></ul>
113 129 </div>
114 130 <div>
115 131 <h4>Contact</h4>
@@ -117,14 +133,14 @@ export default function Layout({ children }) {
117 133 </div>
118 134 </div>
119 135 <div className="footer-bottom">
120 − <span>© {new Date().getFullYear()} HF Market Data · Simon-Pierre Boucher</span>
136 + <span>© {new Date().getFullYear()} HF Market Data · Built by {AUTHOR}</span>
121 137 <span>Data: FirstRate Data · SEC EDGAR · UTC / ISO 8601 everywhere</span>
122 138 </div>
123 139 </footer>
124 140
125 141 {searchOpen && (
126 142 <Suspense fallback={null}>
127 − <SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} />
143 + <SearchDialog open={searchOpen} initialQuery={searchSeed} onClose={() => setSearchOpen(false)} />
128 144 </Suspense>
129 145 )}
130 146 </div>
modified hfmarketdata/web/src/app/theme.css +188 −82
Large diff (334 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/Badge.jsx +10 −4
Large diff (24 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/BrandLogos.jsx +27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/Button.jsx +27 −0
Large diff (27 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/Callout.jsx +10 −6
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/Card.jsx +9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
deleted hfmarketdata/web/src/components/Code.jsx +0 −110
Large diff (110 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/CodeBlock.jsx +58 −0
Large diff (58 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/CodeHighlight.jsx +86 −0
Large diff (86 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/CodeTabs.jsx +3 −3
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/CopyButton.jsx +38 −0
Large diff (38 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/Field.jsx +40 −0
Large diff (40 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/Icons.jsx +11 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
deleted hfmarketdata/web/src/components/IntCopyBlock.jsx +0 −66
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/Markdown.jsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/PgCallout.jsx +4 −14
Large diff (18 lines) is not loaded by default to keep the page fast.Load diff
deleted hfmarketdata/web/src/components/PgCodeBlock.jsx +0 −28
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/PgCopyButton.jsx +6 −12
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
deleted hfmarketdata/web/src/components/PgTabs.jsx +0 −35
Large diff (35 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/SearchDialog.jsx +4 −4
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/States.jsx +87 −0
Large diff (87 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/Table.jsx +10 −4
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/Tabs.jsx +23 −18
Large diff (59 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/components.css +88 −36
Large diff (165 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/components/index.js +9 −2
Large diff (19 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/components/useMediaQuery.js +16 −0
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/docs/mdx-components.jsx +2 −2
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
deleted hfmarketdata/web/src/legacy-spec.js +0 −366
Large diff (366 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/pages/NotFound.jsx +49 −0
Large diff (49 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/docs/DocsHome.jsx +1 −1
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/docs/DocsLayout.jsx +28 −9
Large diff (91 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/docs/ErrorsPage.jsx +2 −2
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/docs/GuidePage.jsx +2 −2
Large diff (16 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/docs/OperationPage.jsx +84 −56
Large diff (204 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/docs/docs.css +68 −52
Large diff (205 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/home/Home.jsx +212 −171
Large diff (505 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/home/home.css +56 −44
Large diff (136 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/integrations/Integrations.jsx +19 −9
Large diff (67 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/integrations/integrations.css +50 −52
Large diff (140 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/pages/legal/Legal.jsx +39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/pages/legal/data-license.mdx +44 −0
Large diff (44 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/pages/legal/legal.css +11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/pages/legal/privacy.mdx +28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/pages/legal/terms.mdx +45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/limits/Limits.jsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/limits/limits.css +15 −11
Large diff (51 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/playground/Playground.jsx +1 −1
Large diff (8 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/status/Status.jsx +66 −38
Large diff (173 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/pages/status/status.css +20 −8
Large diff (37 lines) is not loaded by default to keep the page fast.Load diff
added hfmarketdata/web/src/playground/EndpointPicker.jsx +86 −0
Large diff (86 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/Playground.jsx +181 −143
Large diff (454 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/RateLimitPanel.jsx +5 −16
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/RequestForm.jsx +12 −29
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/WebSocketTab.jsx +15 −15
Large diff (69 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/catalog.js +19 −1
Large diff (32 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/playground.css +117 −101
Large diff (264 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/views/ChartView.jsx +9 −7
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
modified hfmarketdata/web/src/playground/views/JsonView.jsx +2 −2
Large diff (15 lines) is not loaded by default to keep the page fast.Load diff
deleted hfmarketdata/web/src/styles.css +0 −581
Large diff (582 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme