From 62bdd79b3473262dde5fb0a90eab34fe7bf344fd Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Sun, 17 May 2026 13:07:40 -0700 Subject: 'UI Shell and General Architecture' --- frontend/lib/overview.ts | 134 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 frontend/lib/overview.ts (limited to 'frontend/lib/overview.ts') diff --git a/frontend/lib/overview.ts b/frontend/lib/overview.ts new file mode 100644 index 0000000..db30050 --- /dev/null +++ b/frontend/lib/overview.ts @@ -0,0 +1,134 @@ +import { fmtCurrency, fmtLarge, fmtNumber, fmtPct } from "@/lib/format"; +import type { MarketIndex, Signal, TickerOverview, WatchlistItem } from "@/types/api"; + +export type KpiItem = { + key: string; + value: string; + sublabel: string; + missing?: boolean; +}; + +export type NavItem = { + key: string; + label: string; + icon: string; + disabled?: boolean; +}; + +export const OVERVIEW_NAV_ITEMS: NavItem[] = [ + { key: "overview", label: "Overview", icon: "chart" }, + { key: "financials", label: "Financials", icon: "ledger", disabled: true }, + { key: "valuation", label: "Valuation", icon: "dollar", disabled: true }, + { key: "options", label: "Options", icon: "window", disabled: true }, + { key: "insiders", label: "Insiders", icon: "pulse", disabled: true }, + { key: "filings", label: "Filings", icon: "folder", disabled: true }, + { key: "news", label: "News", icon: "terminal", disabled: true } +]; + +export function buildIdentityLine(overview: TickerOverview): string { + const parts = [overview.profile.sector, overview.profile.industry, overview.profile.exchange].filter(Boolean); + return parts.length ? parts.join(" · ") : "Profile details unavailable"; +} + +export function buildKpis(overview: TickerOverview): KpiItem[] { + return [ + { + key: "Market Cap", + value: fmtLarge(overview.stats.market_cap), + sublabel: `${fmtNumber(overview.stats.volume, 0)} volume`, + missing: overview.stats.market_cap == null + }, + { + key: "P / E", + value: overview.stats.trailing_pe == null ? "Unavailable" : `${fmtNumber(overview.stats.trailing_pe)}x`, + sublabel: `EPS ${fmtCurrency(overview.stats.trailing_eps)}`, + missing: overview.stats.trailing_pe == null + }, + { + key: "EPS · TTM", + value: fmtCurrency(overview.stats.trailing_eps), + sublabel: `Prev close ${fmtCurrency(overview.quote.prev_close)}`, + missing: overview.stats.trailing_eps == null + }, + { + key: "52W Position", + value: formatRangePosition(overview), + sublabel: `${fmtCurrency(overview.range_52w.low)} to ${fmtCurrency(overview.range_52w.high)}`, + missing: rangePercent(overview) == null + }, + { + key: "Short Float", + value: fmtPct(overview.short_interest.short_percent_of_float), + sublabel: `${fmtNumber(overview.short_interest.short_ratio)} days cover`, + missing: overview.short_interest.short_percent_of_float == null + }, + { + key: "Beta", + value: fmtNumber(overview.stats.beta), + sublabel: `Avg vol ${fmtNumber(overview.stats.average_volume, 0)}`, + missing: overview.stats.beta == null + } + ]; +} + +export function formatRangePosition(overview: TickerOverview): string { + const pct = rangePercent(overview); + if (pct == null) return "Unavailable"; + return `${pct.toFixed(0)}%`; +} + +export function rangePercent(overview: TickerOverview): number | null { + const low = overview.range_52w.low; + const high = overview.range_52w.high; + const price = overview.range_52w.price ?? overview.quote.price; + if (low == null || high == null || price == null || high <= low) return null; + return Math.max(0, Math.min(100, ((price - low) / (high - low)) * 100)); +} + +export function unavailableFields(overview: TickerOverview): string[] { + return Object.entries(overview.meta.field_availability) + .filter(([, available]) => !available) + .map(([field]) => field); +} + +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`; +} + +export function watchlistSubtitle(item: WatchlistItem): string { + return new Date(item.created_at).toLocaleDateString(undefined, { + month: "short", + day: "numeric" + }); +} + +export function sortIndices(indices: MarketIndex[]): MarketIndex[] { + return [...indices].slice(0, 4); +} + +export function signalTone(signal: Signal["state"]): "pos" | "warn" | "neg" | "neu" { + return signal; +} + +export function marketClock(now = new Date()) { + const eastern = new Date(now.toLocaleString("en-US", { timeZone: "America/New_York" })); + const day = eastern.getDay(); + const minutes = eastern.getHours() * 60 + eastern.getMinutes(); + const isWeekday = day >= 1 && day <= 5; + const open = isWeekday && minutes >= 570 && minutes < 960; + + return { + isOpen: open, + status: open ? "US Market Open" : "US Market Closed", + time: new Intl.DateTimeFormat("en-US", { + hour: "numeric", + minute: "2-digit", + timeZone: "America/New_York", + timeZoneName: "short" + }).format(now) + }; +} + -- cgit v1.3-2-g0d8e