summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 13:36:57 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 13:36:57 -0700
commitc3f19f79f66054dc3b3a98999ea38b0f05248e06 (patch)
tree2d3b551838bfc8abeb52be20350b729377bceea5 /frontend
parent6b8e9470d5b40030172b0413f0c5875fcbe65595 (diff)
Refine overview ratios and shell
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/page.tsx48
-rw-r--r--frontend/app/prism-shell.css170
-rw-r--r--frontend/components/prism/ChartCard.tsx7
-rw-r--r--frontend/components/prism/Sidebar.tsx6
-rw-r--r--frontend/components/prism/TickerHeader.tsx14
-rw-r--r--frontend/lib/overview.ts13
-rw-r--r--frontend/types/api.ts16
7 files changed, 197 insertions, 77 deletions
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 02bd706..41408b0 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -377,12 +377,12 @@ function SignalCard({ overview }: { overview: TickerOverview }) {
<h2 className="psm-card-title">Readthrough</h2>
</div>
</div>
- <div className="psm-signal-grid">
+ <div className="psm-signal-list">
{overview.signals.map((signal) => (
- <article key={signal.key} className={`psm-signal ${signalTone(signal.state)}`}>
+ <article key={signal.key} className={`psm-signal-row ${signalTone(signal.state)}`}>
<span className="psm-signal-key">{signal.key}</span>
- <span className="psm-signal-value">{signal.value}</span>
<span className="psm-signal-copy">{signal.description}</span>
+ <span className="psm-signal-value">{signal.value}</span>
</article>
))}
</div>
@@ -392,12 +392,14 @@ function SignalCard({ overview }: { overview: TickerOverview }) {
function DataStatusCard({ overview, missingFields }: { overview: TickerOverview; missingFields: string[] }) {
const entries = Object.entries(overview.meta.sources).slice(0, 6);
+ const visibleMissing = missingFields.slice(0, 4);
+ const hiddenMissingCount = Math.max(0, missingFields.length - visibleMissing.length);
return (
<section className="psm-card">
<div className="psm-card-head">
<div>
- <div className="psm-eyebrow">Data Quality</div>
+ <div className="psm-eyebrow">Coverage</div>
<h2 className="psm-card-title">Coverage</h2>
</div>
<span className={`psm-status-chip${overview.meta.is_partial ? " partial" : ""}`}>{overview.meta.status}</span>
@@ -405,7 +407,8 @@ function DataStatusCard({ overview, missingFields }: { overview: TickerOverview;
<p className="psm-quality-copy">{availableFieldSummary(overview)}</p>
{overview.meta.is_partial ? (
<div className="psm-stack">
- {missingFields.length ? missingFields.slice(0, 8).map((field) => <span key={field} className="psm-field-tag missing">{field}</span>) : null}
+ {missingFields.length ? visibleMissing.map((field) => <span key={field} className="psm-field-tag missing">{field}</span>) : null}
+ {hiddenMissingCount ? <span className="psm-field-tag missing">+{hiddenMissingCount} more</span> : null}
</div>
) : null}
<div className="psm-source-list">
@@ -433,7 +436,7 @@ function ProfileCard({ overview }: { overview: TickerOverview }) {
<div className="psm-card-head">
<div>
<div className="psm-eyebrow">Company Profile</div>
- <h2 className="psm-card-title">Context</h2>
+ <h2 className="psm-card-title">Company Profile</h2>
</div>
</div>
<div className="psm-profile-list">
@@ -474,7 +477,7 @@ function ShortInterestCard({ overview }: { overview: TickerOverview }) {
<div className="psm-card-head">
<div>
<div className="psm-eyebrow">Short Interest</div>
- <h2 className="psm-card-title">Pressure</h2>
+ <h2 className="psm-card-title">Short Interest</h2>
</div>
</div>
<div className="psm-detail-grid">
@@ -488,21 +491,38 @@ function ShortInterestCard({ overview }: { overview: TickerOverview }) {
}
function StatsCard({ overview }: { overview: TickerOverview }) {
+ const referenceRows = [
+ { label: "Market Cap", value: fmtLarge(overview.stats.market_cap), missing: overview.stats.market_cap == null },
+ { label: "P/E TTM", value: overview.stats.trailing_pe == null ? "-" : `${fmtNumber(overview.stats.trailing_pe)}x`, missing: overview.stats.trailing_pe == null },
+ { label: "EPS TTM", value: fmtCurrency(overview.stats.trailing_eps), missing: overview.stats.trailing_eps == null },
+ { label: "P/B", value: overview.ratios.price_to_book == null ? "-" : `${fmtNumber(overview.ratios.price_to_book)}x`, missing: overview.ratios.price_to_book == null },
+ { label: "P/S", value: overview.ratios.price_to_sales == null ? "-" : `${fmtNumber(overview.ratios.price_to_sales)}x`, missing: overview.ratios.price_to_sales == null },
+ { label: "EV/Sales", value: overview.ratios.ev_to_sales == null ? "-" : `${fmtNumber(overview.ratios.ev_to_sales)}x`, missing: overview.ratios.ev_to_sales == null },
+ { label: "EV/EBITDA", value: overview.ratios.ev_to_ebitda == null ? "-" : `${fmtNumber(overview.ratios.ev_to_ebitda)}x`, missing: overview.ratios.ev_to_ebitda == null },
+ { label: "Gross Margin", value: fmtPct(overview.ratios.gross_margin_ttm), missing: overview.ratios.gross_margin_ttm == null },
+ { label: "Op Margin", value: fmtPct(overview.ratios.operating_margin_ttm), missing: overview.ratios.operating_margin_ttm == null },
+ { label: "Net Margin", value: fmtPct(overview.ratios.net_margin_ttm), missing: overview.ratios.net_margin_ttm == null },
+ { label: "ROE", value: fmtPct(overview.ratios.roe_ttm), missing: overview.ratios.roe_ttm == null },
+ { label: "ROA", value: fmtPct(overview.ratios.roa_ttm), missing: overview.ratios.roa_ttm == null },
+ { label: "ROIC", value: fmtPct(overview.ratios.roic_ttm), missing: overview.ratios.roic_ttm == null },
+ { label: "D/E", value: overview.ratios.debt_to_equity == null ? "-" : `${fmtNumber(overview.ratios.debt_to_equity)}x`, missing: overview.ratios.debt_to_equity == null },
+ { label: "Current Ratio", value: overview.ratios.current_ratio == null ? "-" : `${fmtNumber(overview.ratios.current_ratio)}x`, missing: overview.ratios.current_ratio == null },
+ { label: "Dividend Yield", value: fmtPct(overview.ratios.dividend_yield_ttm), missing: overview.ratios.dividend_yield_ttm == null },
+ { label: "Payout Ratio", value: fmtPct(overview.ratios.dividend_payout_ratio_ttm), missing: overview.ratios.dividend_payout_ratio_ttm == null }
+ ];
+
return (
<section className="psm-card">
<div className="psm-card-head">
<div>
- <div className="psm-eyebrow">Overview Stats</div>
+ <div className="psm-eyebrow">Reference</div>
<h2 className="psm-card-title">Reference</h2>
</div>
</div>
<div className="psm-stat-list">
- <StatRow label="Market Cap" value={fmtLarge(overview.stats.market_cap)} missing={overview.stats.market_cap == null} />
- <StatRow label="P/E TTM" value={fmtNumber(overview.stats.trailing_pe)} missing={overview.stats.trailing_pe == null} />
- <StatRow label="EPS TTM" value={fmtCurrency(overview.stats.trailing_eps)} missing={overview.stats.trailing_eps == null} />
- <StatRow label="Volume" value={fmtNumber(overview.stats.volume, 0)} missing={overview.stats.volume == null} />
- <StatRow label="Avg Volume" value={fmtNumber(overview.stats.average_volume, 0)} missing={overview.stats.average_volume == null} />
- <StatRow label="Beta" value={fmtNumber(overview.stats.beta)} missing={overview.stats.beta == null} />
+ {referenceRows.map((row) => (
+ <StatRow key={row.label} label={row.label} value={row.value} missing={row.missing} />
+ ))}
</div>
</section>
);
diff --git a/frontend/app/prism-shell.css b/frontend/app/prism-shell.css
index 9b0e1b5..b6372c2 100644
--- a/frontend/app/prism-shell.css
+++ b/frontend/app/prism-shell.css
@@ -1,6 +1,6 @@
.prism-app {
display: grid;
- grid-template-columns: 256px minmax(0, 1fr);
+ grid-template-columns: 284px minmax(0, 1fr);
min-height: 100vh;
}
@@ -192,22 +192,22 @@
.psm-watch-row {
display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: var(--sp-2);
+ grid-template-columns: minmax(0, 1fr) 34px;
+ gap: var(--sp-3);
align-items: center;
border-bottom: 1px solid var(--line-1);
}
.psm-watch-select {
display: grid;
- grid-template-columns: minmax(0, 1fr) auto auto;
- gap: var(--sp-2);
+ grid-template-columns: minmax(0, 1fr) minmax(82px, auto) minmax(52px, auto);
+ column-gap: var(--sp-5);
align-items: center;
width: 100%;
border: 0;
background: transparent;
color: var(--fg-2);
- padding: 10px 0;
+ padding: 12px 0;
}
.psm-watch-select:hover,
@@ -221,13 +221,22 @@
.psm-watch-main {
min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
text-align: left;
}
+.psm-watch-cell {
+ min-width: 0;
+}
+
.psm-watch-symbol {
color: var(--fg-1);
+ font-family: var(--font-mono);
font-size: var(--fs-14);
font-weight: 500;
+ letter-spacing: 0.04em;
}
.psm-watch-date,
@@ -240,6 +249,11 @@
font-size: var(--fs-13);
}
+.psm-watch-date {
+ line-height: 1.2;
+ white-space: nowrap;
+}
+
.psm-watch-price,
.psm-watch-change,
.psm-quote-line,
@@ -261,13 +275,13 @@
}
.psm-watch-remove {
- width: 26px;
- height: 26px;
+ width: 30px;
+ height: 30px;
border: 1px solid var(--line-2);
- border-radius: var(--r-full);
+ border-radius: var(--r-2);
background: transparent;
color: var(--fg-4);
- margin-right: 2px;
+ justify-self: end;
}
.psm-watch-remove:hover {
@@ -484,9 +498,9 @@
.psm-ticker-head {
display: grid;
- grid-template-columns: minmax(0, 1.25fr) minmax(220px, 0.75fr) auto;
+ grid-template-columns: minmax(0, 1.4fr) minmax(240px, 0.75fr) minmax(220px, auto);
gap: var(--sp-5);
- align-items: end;
+ align-items: start;
padding-bottom: var(--sp-4);
border-bottom: 1px solid var(--line-1);
}
@@ -495,32 +509,32 @@
min-width: 0;
}
+.psm-head-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: var(--sp-3);
+}
+
.psm-identity-line {
color: var(--brass);
- display: block;
- margin-bottom: var(--sp-2);
+ display: inline-flex;
}
.psm-heading-row {
display: flex;
flex-wrap: wrap;
- align-items: baseline;
- gap: var(--sp-4);
-}
-
-.psm-symbol {
- color: var(--fg-1);
- font-family: var(--font-display);
- font-size: clamp(3rem, 6vw, var(--fs-64));
- line-height: 0.95;
- letter-spacing: -0.03em;
+ align-items: flex-start;
+ gap: var(--sp-3);
}
.psm-company-name {
- color: var(--fg-2);
+ color: var(--fg-1);
font-family: var(--font-display);
- font-size: var(--fs-24);
+ font-size: clamp(2.8rem, 5vw, var(--fs-56));
font-style: italic;
+ line-height: 0.95;
}
.psm-partial-chip,
@@ -559,22 +573,30 @@
color: var(--negative);
}
+.psm-tag {
+ border: 1px solid var(--line-2);
+ background: var(--ink-2);
+ color: var(--fg-2);
+}
+
.psm-subline {
margin-top: var(--sp-2);
color: var(--fg-3);
font-size: var(--fs-14);
+ max-width: 52ch;
}
.psm-price-stack {
display: flex;
flex-direction: column;
align-items: flex-end;
- gap: 4px;
+ gap: 5px;
+ padding-top: 2px;
}
.psm-price {
color: var(--fg-1);
- font-size: clamp(2.4rem, 4vw, var(--fs-48));
+ font-size: clamp(2.6rem, 4vw, var(--fs-48));
line-height: 1;
}
@@ -585,6 +607,8 @@
.psm-quote-line {
color: var(--fg-3);
font-size: var(--fs-12);
+ text-transform: uppercase;
+ letter-spacing: var(--tr-wider);
}
.psm-primary-action,
@@ -601,6 +625,7 @@
border: 1px solid var(--brass);
background: var(--brass);
color: var(--brass-ink);
+ margin-top: var(--sp-2);
}
.psm-primary-action.subtle {
@@ -618,6 +643,7 @@
display: flex;
flex-direction: column;
gap: var(--sp-2);
+ padding-top: 8px;
}
.psm-range-values {
@@ -629,6 +655,10 @@
font-size: var(--fs-12);
}
+.psm-range-spot {
+ color: var(--fg-1);
+}
+
.psm-range-rail {
position: relative;
height: 4px;
@@ -644,6 +674,11 @@
background: var(--brass);
}
+.psm-range-caption {
+ color: var(--fg-4);
+ font-size: var(--fs-12);
+}
+
.psm-kpis {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
@@ -769,14 +804,12 @@
line-height: 1.5;
}
-.psm-signal-grid,
.psm-detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--sp-3);
}
-.psm-signal,
.psm-detail-item {
border: 1px solid var(--line-1);
border-radius: var(--r-2);
@@ -792,7 +825,6 @@
text-transform: uppercase;
}
-.psm-signal-value,
.psm-detail-value,
.psm-stat-value {
display: block;
@@ -801,7 +833,6 @@
font-size: var(--fs-18);
}
-.psm-signal-copy,
.psm-detail-copy {
display: block;
margin-top: 6px;
@@ -810,20 +841,50 @@
line-height: 1.45;
}
-.psm-signal.pos {
- border-color: rgba(79, 140, 94, 0.35);
+.psm-signal-list {
+ display: flex;
+ flex-direction: column;
}
-.psm-signal.warn {
- border-color: rgba(196, 149, 69, 0.35);
+.psm-signal-row {
+ display: grid;
+ grid-template-columns: 84px minmax(0, 1fr) auto;
+ gap: var(--sp-3);
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--line-1);
+}
+
+.psm-signal-row:last-child {
+ border-bottom: 0;
+}
+
+.psm-signal-row.pos .psm-signal-value {
+ color: var(--positive);
+}
+
+.psm-signal-row.warn .psm-signal-value {
+ color: var(--warning);
+}
+
+.psm-signal-row.neg .psm-signal-value {
+ color: var(--negative);
}
-.psm-signal.neg {
- border-color: rgba(181, 73, 75, 0.35);
+.psm-signal-row.neu .psm-signal-value {
+ color: var(--fg-3);
+}
+
+.psm-signal-value {
+ color: var(--fg-1);
+ font-family: var(--font-mono);
+ font-size: var(--fs-14);
+ font-variant-numeric: tabular-nums;
}
-.psm-signal.neu {
- border-color: var(--line-2);
+.psm-signal-copy {
+ color: var(--fg-2);
+ font-size: var(--fs-14);
}
.psm-profile-list,
@@ -841,7 +902,7 @@
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--sp-3);
align-items: start;
- padding-bottom: var(--sp-3);
+ padding-bottom: var(--sp-2);
border-bottom: 1px solid var(--line-1);
}
@@ -868,6 +929,11 @@
word-break: break-word;
}
+.psm-stat-value {
+ text-align: right;
+ white-space: nowrap;
+}
+
.psm-source-value {
font-family: var(--font-mono);
font-size: var(--fs-12);
@@ -884,6 +950,7 @@
display: flex;
flex-wrap: wrap;
gap: var(--sp-2);
+ margin-bottom: var(--sp-2);
}
.psm-field-tag {
@@ -988,18 +1055,33 @@
.psm-kpis,
.psm-main-grid,
.psm-detail-grid,
- .psm-signal-grid,
.psm-ticker-head {
grid-template-columns: 1fr;
}
.psm-heading-row {
align-items: start;
- flex-direction: column;
- gap: var(--sp-2);
}
.psm-price-stack {
align-items: start;
}
+
+ .psm-signal-row {
+ grid-template-columns: 1fr;
+ gap: 4px;
+ }
+
+ .psm-watch-select {
+ grid-template-columns: minmax(0, 1fr) auto auto;
+ row-gap: 4px;
+ }
+
+ .psm-watch-price {
+ grid-column: 2;
+ }
+
+ .psm-watch-change {
+ grid-column: 3;
+ }
}
diff --git a/frontend/components/prism/ChartCard.tsx b/frontend/components/prism/ChartCard.tsx
index bc650d7..87bb399 100644
--- a/frontend/components/prism/ChartCard.tsx
+++ b/frontend/components/prism/ChartCard.tsx
@@ -23,8 +23,8 @@ export function ChartCard({ symbol, period, points, chartState, chartError, onCh
<section className="psm-card">
<div className="psm-card-head">
<div>
- <div className="psm-eyebrow">Price History</div>
- <h2 className="psm-card-title">{symbol}</h2>
+ <div className="psm-eyebrow">Price Action</div>
+ <h2 className="psm-card-title">{symbol} Price History</h2>
</div>
<div className="psm-tabs" role="tablist" aria-label="Chart range">
{PERIODS.map((option) => (
@@ -42,7 +42,7 @@ export function ChartCard({ symbol, period, points, chartState, chartError, onCh
</div>
</div>
- <p className="psm-chart-meta">Chart loading is isolated from the rest of Overview. A history miss only affects this card.</p>
+ <p className="psm-chart-meta">Interactive history for the selected window. If history fails, the rest of Overview stays intact.</p>
<div className="psm-chart-frame">
{chartState === "loading" ? <div className="psm-card-empty">Loading {period.toUpperCase()} history…</div> : null}
@@ -52,4 +52,3 @@ export function ChartCard({ symbol, period, points, chartState, chartError, onCh
</section>
);
}
-
diff --git a/frontend/components/prism/Sidebar.tsx b/frontend/components/prism/Sidebar.tsx
index 80e13f3..7f106d8 100644
--- a/frontend/components/prism/Sidebar.tsx
+++ b/frontend/components/prism/Sidebar.tsx
@@ -75,12 +75,12 @@ export function Sidebar({
return (
<div key={item.symbol} className={`psm-watch-row${active ? " active" : ""}`}>
<button type="button" className="psm-watch-select" onClick={() => onSelectTicker(item.symbol)}>
- <span className="psm-watch-main">
+ <span className="psm-watch-cell psm-watch-main">
<span className="psm-watch-symbol">{item.symbol}</span>
<span className="psm-watch-date">{watchlistSubtitle(item)}</span>
</span>
- <span className="psm-watch-price">{fmtCurrency(item.quote?.price)}</span>
- <span className={`psm-watch-change ${deltaClass(item.quote?.change_pct)}`}>{fmtPct(item.quote?.change_pct, 2, true)}</span>
+ <span className="psm-watch-cell psm-watch-price">{fmtCurrency(item.quote?.price)}</span>
+ <span className={`psm-watch-cell psm-watch-change ${deltaClass(item.quote?.change_pct)}`}>{fmtPct(item.quote?.change_pct, 2, true)}</span>
</button>
<button
type="button"
diff --git a/frontend/components/prism/TickerHeader.tsx b/frontend/components/prism/TickerHeader.tsx
index 23254f8..369d06c 100644
--- a/frontend/components/prism/TickerHeader.tsx
+++ b/frontend/components/prism/TickerHeader.tsx
@@ -10,15 +10,18 @@ type Props = {
export function TickerHeader({ overview, onToggleWatchlist, isSaved }: Props) {
const pct = rangePercent(overview);
+ const lastSource = overview.meta.sources["quote.price"];
return (
<header className="psm-ticker-head">
<div className="psm-header-left">
- <span className="psm-identity-line psm-eyebrow">{overview.profile.symbol}</span>
+ <div className="psm-head-meta">
+ <span className="psm-identity-line psm-eyebrow">{overview.profile.symbol}</span>
+ {overview.profile.exchange ? <span className="psm-tag">{overview.profile.exchange}</span> : null}
+ {overview.meta.is_partial ? <span className="psm-partial-chip">Partial Data</span> : null}
+ </div>
<div className="psm-heading-row">
- <span className="psm-symbol">{overview.profile.symbol}</span>
<span className="psm-company-name">{overview.profile.name || "Name unavailable"}</span>
- {overview.meta.is_partial ? <span className="psm-partial-chip">Partial Data</span> : null}
</div>
<p className="psm-subline">{buildIdentityLine(overview)}</p>
</div>
@@ -27,12 +30,13 @@ export function TickerHeader({ overview, onToggleWatchlist, isSaved }: Props) {
<div className="psm-eyebrow">52 Week Range</div>
<div className="psm-range-values">
<span>{fmtCurrency(overview.range_52w.low)}</span>
- <span>{fmtCurrency(overview.range_52w.price ?? overview.quote.price)}</span>
+ <span className="psm-range-spot">{fmtCurrency(overview.range_52w.price ?? overview.quote.price)}</span>
<span>{fmtCurrency(overview.range_52w.high)}</span>
</div>
<div className="psm-range-rail" aria-hidden>
{pct != null ? <span className="psm-range-indicator" style={{ left: `${pct}%` }} /> : null}
</div>
+ <div className="psm-range-caption">{pct == null ? "Range unavailable" : `${pct.toFixed(0)}% through the annual range`}</div>
</div>
<div className="psm-price-stack">
@@ -41,6 +45,7 @@ export function TickerHeader({ overview, onToggleWatchlist, isSaved }: Props) {
{fmtCurrency(overview.quote.change)} · {fmtPct(overview.quote.change_pct, 2, true)}
</span>
<span className="psm-quote-line">Prev close {fmtCurrency(overview.quote.prev_close)}</span>
+ {lastSource ? <span className="psm-quote-line">Source {lastSource.replaceAll("_", " ")}</span> : null}
<button type="button" className={`psm-primary-action${isSaved ? " subtle" : ""}`} onClick={onToggleWatchlist}>
{isSaved ? "Remove From Watchlist" : "Save To Watchlist"}
</button>
@@ -48,4 +53,3 @@ export function TickerHeader({ overview, onToggleWatchlist, isSaved }: Props) {
</header>
);
}
-
diff --git a/frontend/lib/overview.ts b/frontend/lib/overview.ts
index efbd9f9..d02c5d3 100644
--- a/frontend/lib/overview.ts
+++ b/frontend/lib/overview.ts
@@ -41,14 +41,14 @@ export function buildKpis(overview: TickerOverview): KpiItem[] {
{
key: "P / E",
value: overview.stats.trailing_pe == null ? "Unavailable" : `${fmtNumber(overview.stats.trailing_pe)}x`,
- sublabel: `EPS ${fmtCurrency(overview.stats.trailing_eps)}`,
+ sublabel: `P/B ${overview.ratios.price_to_book == null ? "-" : `${fmtNumber(overview.ratios.price_to_book)}x`}`,
missing: overview.stats.trailing_pe == null
},
{
- key: "Prev Close",
- value: fmtCurrency(overview.quote.prev_close),
- sublabel: `Day chg ${fmtCurrency(overview.quote.change)}`,
- missing: overview.quote.prev_close == null
+ key: "EPS · TTM",
+ value: fmtCurrency(overview.stats.trailing_eps),
+ sublabel: `Net margin ${fmtPct(overview.ratios.net_margin_ttm)}`,
+ missing: overview.stats.trailing_eps == null
},
{
key: "52W Position",
@@ -95,7 +95,7 @@ export function availableFieldSummary(overview: TickerOverview): string {
const fields = Object.values(overview.meta.field_availability);
if (!fields.length) return "Availability metadata unavailable";
const available = fields.filter(Boolean).length;
- return `${available}/${fields.length} tracked fields available`;
+ return `${available} of ${fields.length} tracked fields filled`;
}
export function watchlistSubtitle(item: WatchlistItem): string {
@@ -131,4 +131,3 @@ export function marketClock(now = new Date()) {
}).format(now)
};
}
-
diff --git a/frontend/types/api.ts b/frontend/types/api.ts
index 679ada9..84dfd19 100644
--- a/frontend/types/api.ts
+++ b/frontend/types/api.ts
@@ -44,6 +44,22 @@ export type TickerOverview = {
average_volume?: number | null;
beta?: number | null;
};
+ ratios: {
+ price_to_book?: number | null;
+ price_to_sales?: number | null;
+ ev_to_sales?: number | null;
+ ev_to_ebitda?: number | null;
+ gross_margin_ttm?: number | null;
+ operating_margin_ttm?: number | null;
+ net_margin_ttm?: number | null;
+ roe_ttm?: number | null;
+ roa_ttm?: number | null;
+ roic_ttm?: number | null;
+ debt_to_equity?: number | null;
+ current_ratio?: number | null;
+ dividend_yield_ttm?: number | null;
+ dividend_payout_ratio_ttm?: number | null;
+ };
range_52w: {
low?: number | null;
high?: number | null;