Skip to content

feat(market): equity detail page with per-asset-class routing#135

Merged
luokerenx4 merged 5 commits intomasterfrom
dev
Apr 21, 2026
Merged

feat(market): equity detail page with per-asset-class routing#135
luokerenx4 merged 5 commits intomasterfrom
dev

Conversation

@luokerenx4
Copy link
Copy Markdown
Contributor

Summary

Step 2 of the Market workbench. Splits the single-component /market surface into a landing (search only) and /market/:assetClass/:symbol for per-asset-class layouts. Equity is the pilot — everything else falls through to a generic K-line view until each class's data semantics are figured out.

New: Equity detail page

  • Quote header with last / change / open / prev / day range / 52W range / MA50 / MA200 / volume / market cap.
  • K-line (existing component) with interval + range now persisted in the URL (?interval=1h&range=1M) via useSearchParams. SearchBox preserves those across symbol switches so user preferences carry over when jumping between AAPL → MSFT.
  • Profile panel — sector, industry, CEO, employees, HQ, website, company description.
  • Key Metrics panel — P/E, PEG, P/S, P/B, EV/EBITDA, EV/Sales, dividend yield, ROE, ROA, margins, D/E, current ratio, market cap, enterprise value.
  • Financial Statements panel — tabbed Balance / Income / Cash Flow, lazy-loaded per tab and cached per symbol.

Routing

  • /market — landing with search + empty state
  • /market/:assetClass/:symbol — detail dispatcher; equity gets EquityDetail, others get GenericDetail (K-line only)
  • SearchBox uses useNavigate(), preserves existing search params

opentypebb fixes along the way

  • FMP date range filteringgetHistoricalOhlc was passing the canonical start_date / end_date through to FMP which expects from / to; requests were silently returning a fixed 5-year window regardless. Rename keys before serialising.
  • Secret redaction + root cause in amakeRequest — URLs in error messages now redact apikey / api_key / token / access_token / key / secret / password (FMP URL auth was leaking the key into logs), and the caught cause is now included in the thrown message so fetch failures are diagnosable without debugger stepping.
  • FMP equity quote aliasesyearHigh / yearLow / marketCap weren't in the FMP ALIAS_DICT, so the canonical snake_case fields were always null despite data being there in camelCase. Added the three missing entries.

Design decisions worth noting

  • Single Card wrapper as the only reusable primitive — each panel owns its own fetch / loading / error / render. No shared Table or Tabs primitive; the financials table's row=item × col=period shape is too specific to generalize and the tab is just three buttons.
  • Cross-asset-class search ranks globally — the previous order ([equity, crypto, currency, commodity]) buried a commodity gold below 20 equity ticker hits. Now scored by match quality (exact / alias / prefix / word-boundary / substring) so gold commodity wins, xau hits gold via alias, and Goldman Sachs no longer outranks SPDR Gold Trust on gold.
  • Bid/ask dropped from Quote header — real-time L1 data belongs at the execution (UTA) layer, not in an analysis workbench. Keeping them blurred the line between "price to act on" and "price to think about," and most providers don't populate them on their historical quote endpoint anyway.
  • Non-FMP users get empty fundamentals panels, by design — the whole point of forking OpenBB into opentypebb was a standard schema. What each provider can actually fill is the provider's business; Alice doesn't paper over FMP's paywall or yfinance's missing endpoints.

Test plan

  • npx tsc --noEmit clean
  • pnpm test — 1088 tests / 56 files pass
  • Browser: /market/equity/AAPL renders all 5 sections; switching Balance/Income/Cash Flow tabs loads each lazily
  • /market/crypto/BTCUSD renders K-line only (GenericDetail)
  • /market/commodity/gold renders K-line placeholder
  • Search gold → commodity gold at Add Star History chart to README #1; xau → gold via alias; apple → AAPL at Add Star History chart to README #1
  • Interval / range picks persist in URL and survive symbol switch
  • Quote header fields all populate (previously MKT CAP / 52W HIGH / 52W LOW were dashes)
  • FMP date-range-filtered historical returns correct bar count for the requested window
  • amakeRequest errors on bad hosts redact apikey=*** and include the caught cause

🤖 Generated with Claude Code

Ame and others added 5 commits April 21, 2026 11:10
Step 2 of the Market workbench. Splits /market into a landing (search only)
and /market/:assetClass/:symbol for detail views, so the current asset
lives in the URL — shareable, bookmarkable, and back/forward navigable
instead of hidden in component state.

Equity gets a bespoke layout: a quote header (last, change, day range,
52W, MA50/200), the existing K-line, side-by-side profile + key metrics,
and a tabbed financial statements table (Balance / Income / Cash Flow)
that lazy-loads each statement on first view and caches per symbol.
Crypto / Currency / Commodity fall through to a GenericDetail with just
the K-line — their data shapes are different enough that forcing them
into the equity layout would mislead. Each will get its own detail page
when the shape is figured out.

The reusable surface is a single Card wrapper (title + right slot +
content). Every panel owns its own fetch / loading / error / render.
No shared Table / Tabs primitives — financial tables are row=item ×
column=period and don't generalize, so they live inside the financials
panel as a one-off. The SearchBox navigates directly now; its onSelect
prop is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two things users hit after the equity pilot landed.

The financial statements panel was rendering but getting squeezed out of
visible space: EquityDetail's outer flex-col carried min-h-0 (allowing
children to shrink below natural height) while Card's content used
flex-1 min-h-0 for a shape that nobody actually needed. Combined, they
let the browser collapse the bottom panel when content ran long. Dropped
both — panels render at natural height, parent's overflow-y-auto owns
the scroll like it always should have.

K-line interval and range now persist in the URL as ?interval=1h&range=1M
via useSearchParams, with replace so we don't spam browser history.
Default values (1d / 1Y) are elided so shared links stay tidy. The
SearchBox propagates the current search string when switching symbols,
so picking MSFT after configuring AAPL at 1h/1M keeps the view settings
instead of resetting them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Key Metrics and Financial Statements were rendering "—" for a bunch of
rows because the panels were keyed on guesses (priceToEarningsRatioTTM,
operating_income, total_equity, property_plant_equipment_net,
selling_general_and_administrative_expenses, grossProfitMarginTTM, …)
instead of the names the FMP fetcher actually emits after its alias
pass. The fetchers canonicalize to snake_case (price_to_earnings,
return_on_equity, gross_profit_margin, total_operating_income,
selling_general_and_admin_expense, plant_property_equipment_net,
total_common_equity, etc.) and drop the upstream camelCase — so the old
lookups never hit.

This is purely a UI correction — no opentypebb or FMP changes. Verified
against live /fundamental/{metrics,ratios,balance,income,cash}
responses. Also swapped balance's accountsReceivables (upstream raw,
escapes aliasing) for net_receivables (canonical).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FMPEquityQuoteFetcher's ALIAS_DICT had maps for dayHigh/dayLow/price/
priceAvg50/priceAvg200/previousClose/changePercentage/timestamp but not
yearHigh/yearLow/marketCap. The standard EquityQuote schema declares
year_high/year_low/market_cap with default(null), so parsing left those
canonical fields null while the upstream camelCase ones survived via
passthrough — consumers reading the canonical snake_case contract saw
nothing. Add the three missing aliases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bid and ask are real-time L1 quote data whose usefulness lives at the
execution / order-routing layer (UTA), not in an analysis workbench.
Keeping them alongside 52W range and MA200 blurred the line between
\"price to act on\" and \"price to think about\" — and since most providers
don't populate bid/ask on their historical quote endpoints anyway, they
just rendered as dashes.

Drop them from the header. The remaining 10 metrics land evenly on a
5-column grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@luokerenx4 luokerenx4 merged commit 8a47b38 into master Apr 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant