diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/specs/2026-05-17-financials-tab-design.md | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-05-17-financials-tab-design.md b/docs/superpowers/specs/2026-05-17-financials-tab-design.md new file mode 100644 index 0000000..6aed8af --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-financials-tab-design.md @@ -0,0 +1,177 @@ +# 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 `<FinancialsPage>` 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 `<FinancialsPage>` 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: `<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 `<TickerHeader>`, `<KPIStrip>` (reused from Overview), then `<FinancialsCard>`. + +--- + +## 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 |
