# Financials Tab — Design Spec **Date:** 2026-05-17 **Status:** Approved --- ## Overview Add a Financials tab to Prism v2 that surfaces Income Statement, Balance Sheet, and Cash Flow for the selected ticker. Annual (4 years + TTM) and Quarterly (last 8 quarters) periods are available via a toggle. The tab reuses the existing AppShell and follows the Prism design system exactly. --- ## Routing The app uses `?ticker=SYMBOL` today. Tab state is added as a second query param: `?ticker=AAPL&tab=financials`. - Clicking "Financials" in the Sidebar pushes `?ticker=AAPL&tab=financials` - `page.tsx` reads both `ticker` and `tab` from `useSearchParams` - When `tab === "financials"` and a ticker is loaded, render `` in place of the Overview columns - When `tab` is absent or `"overview"`, existing behavior is unchanged - `OVERVIEW_NAV_ITEMS` in `lib/overview.ts` gains a `financials` entry with the `ledger` icon This approach keeps `page.tsx` as a thin shell. The financials content lives in its own component file. When more tabs are built out, each gets the same treatment — a natural migration to proper Next.js routes can happen once 4+ tabs exist. --- ## Backend ### New endpoint ``` GET /api/tickers/{symbol}/financials?period=annual|quarterly ``` - `period` defaults to `annual` - TTL cache: 1 hour (matching v1 financials TTL) - Returns `FinancialsResponse` ### Schemas (`backend/app/schemas.py`) ```python class FinancialRow(BaseModel): label: str indent: int = 0 # 0 = top-level/total, 1 = sub-item is_total: bool = False # bold, fg-0 color is_section: bool = False # small muted eyebrow label (no values) is_margin: bool = False # small italic % row values: list[float | None] class FinancialStatement(BaseModel): columns: list[str] # e.g. ["FY 2021", "FY 2022", ..., "TTM"] or ["Q1 2024", ...] rows: list[FinancialRow] class FinancialsResponse(BaseModel): period: Literal["annual", "quarterly"] income: FinancialStatement balance: FinancialStatement cash_flow: FinancialStatement ``` ### Data service (`backend/app/services/data_service.py`) New function `get_financials(ticker, period)` using yfinance: - **Annual:** `t.income_stmt`, `t.balance_sheet`, `t.cashflow` (4 fiscal years). TTM column computed as sum of last 4 quarters from quarterly statements. Balance sheet last column is MRQ (most recent quarter), not TTM. - **Quarterly:** `t.quarterly_income_stmt`, `t.quarterly_balance_sheet`, `t.quarterly_cashflow` (last 8 quarters). No TTM column. - Follows v1 `get_income_statement` / `get_balance_sheet` / `get_cash_flow` pattern with empty-DataFrame fallback on error. - Margin rows (gross margin, net margin, FCF margin) are computed server-side and included as `is_margin=True` rows. --- ## Row Definitions ### Income Statement | Label | indent | is_total | is_margin | yfinance key | |-------|--------|----------|-----------|--------------| | Total Revenue | 0 | true | | `Total Revenue` | | Cost of Revenue | 1 | | | `Cost Of Revenue` | | Gross Profit | 0 | true | | `Gross Profit` | | gross margin | 1 | | true | derived | | Operating Expenses | 1 | | | `Operating Expense` | | Operating Income | 0 | true | | `Operating Income` | | EBITDA | 1 | | | `EBITDA` or `Normalized EBITDA` | | Interest Expense | 1 | | | `Interest Expense` | | Pretax Income | 0 | | | `Pretax Income` | | Tax Provision | 1 | | | `Tax Provision` | | Net Income | 0 | true | | `Net Income` | | net margin | 1 | | true | derived | | EPS Basic | 1 | | | `Basic EPS` | ### Balance Sheet Section eyebrows: ASSETS, LIABILITIES, EQUITY (is_section=True, no values) | Label | indent | is_total | Section | |-------|--------|----------|---------| | Current Assets | 0 | true | ASSETS | | Cash & Equivalents | 1 | | | | Short Term Investments | 1 | | | | Receivables | 1 | | | | Inventory | 1 | | | | Total Assets | 0 | true | | | Current Liabilities | 0 | true | LIABILITIES | | Accounts Payable | 1 | | | | Short Term Debt | 1 | | | | Long Term Debt | 1 | | | | Total Liabilities | 0 | true | | | Stockholders Equity | 0 | true | EQUITY | Balance sheet columns: 4 fiscal years + MRQ (most recent quarter). MRQ is always from `quarterly_balance_sheet.iloc[:, 0]`. ### Cash Flow Section eyebrows: OPERATING, INVESTING, FINANCING (is_section=True) | Label | indent | is_total | is_margin | Section | |-------|--------|----------|-----------|---------| | Net Income | 1 | | | OPERATING | | D&A | 1 | | | | | Changes in Working Capital | 1 | | | | | Operating Cash Flow | 0 | true | | | | CapEx | 1 | | | INVESTING | | Free Cash Flow | 0 | true | | | | FCF margin | 1 | | true | | | Investing Cash Flow | 0 | true | | | | Dividends Paid | 1 | | | FINANCING | | Buybacks | 1 | | | | | Financing Cash Flow | 0 | true | | | | Net Change in Cash | 0 | true | | | --- ## Frontend ### New files - `frontend/components/prism/FinancialsCard.tsx` — the card component with tabbed statements and period toggle - `frontend/components/prism/FinancialsPage.tsx` — thin wrapper rendered by `page.tsx` when `tab === "financials"`, handles data fetching state ### Modified files - `frontend/types/api.ts` — add `FinancialRow`, `FinancialStatement`, `FinancialsResponse` - `frontend/lib/api.ts` — add `api.financials(symbol, period)` - `frontend/lib/overview.ts` — add `financials` to `OVERVIEW_NAV_ITEMS` - `frontend/app/page.tsx` — read `tab` from `useSearchParams`, render `` when appropriate ### FinancialsCard Props: `{ data: FinancialsResponse, statement: StatementKey, period: PeriodKey, onChangeStatement, onChangePeriod }` - Card header: statement tabs (INCOME / BALANCE / CASH FLOW) left-aligned, underline active indicator. Period toggle (ANNUAL / QUARTERLY) right-aligned. Same row, `border-bottom: 1px solid var(--hairline)`. - Table: `` with sticky first column (label). Columns right-aligned, IBM Plex Mono. - Row rendering by type: - `is_section`: full-width muted eyebrow cell, `colspan` across all columns, no value cells - `is_total`: `color: var(--fg-0)`, `font-weight: 500` - `is_margin`: smaller font, italic, muted color (`var(--fg-3)`) - `indent=1`: left padding `26px` vs `14px` - Negative values: `color: var(--loss)` - TTM / MRQ column header: `color: var(--brass)`; value cells: `color: var(--brass)` - Loading state: skeleton rows (3 pulse placeholders) - Error state: inline muted copy "Statement data unavailable" - Missing values (`null`): render as `—` ### FinancialsPage Handles three states: `loading`, `ready`, `error`. Fetches on ticker + period change. Renders ``, `` (reused from Overview), then ``. --- ## What Is Not In Scope - Charts / sparklines on financial rows (future) - Peer comparison columns (future) - Export to CSV (future) - Key Ratios section — already covered on the Overview tab's Reference card