summaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-05-17-financials-tab-design.md
blob: 6aed8aff25edb2b1b5ea75b61acf225f6c1ca37f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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