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>
81 changed files +2,909 −2,351
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 & 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, '&').replace(/</g, '<').replace(/"/g, '"') |
| 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 & 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 & 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