summaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-05-17-financials-tab-design.md
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 14:12:23 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-17 14:12:23 -0700
commit7775463a5fdb6d76dcb178c7ab422e9edf7952ee (patch)
treeeb2d525b77680266c4fcfe828aeb42fd187bfe91 /docs/superpowers/specs/2026-05-17-financials-tab-design.md
parent336ae74d9786d0361e2759bd8897902ab7911d6b (diff)
Add financials tab design spec
Diffstat (limited to 'docs/superpowers/specs/2026-05-17-financials-tab-design.md')
-rw-r--r--docs/superpowers/specs/2026-05-17-financials-tab-design.md177
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