summaryrefslogtreecommitdiff
path: root/docs/superpowers
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/specs/2026-05-18-valuation-tab-design.md164
1 files changed, 164 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-05-18-valuation-tab-design.md b/docs/superpowers/specs/2026-05-18-valuation-tab-design.md
new file mode 100644
index 0000000..3400029
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-18-valuation-tab-design.md
@@ -0,0 +1,164 @@
+# Valuation Tab — Design Spec
+
+**Date:** 2026-05-18
+**Status:** Approved
+**Scope:** v2 read-only valuation models (DCF + multiples). No sliders, no subtabs.
+
+---
+
+## Context and Decisions
+
+### Scope: Models only (no subtabs)
+
+v1 had subtabs under Valuation: Key Ratios, Historical Ratios, Models, Comps, Forward Estimates, Analyst Targets, Earnings History. In v2 these are decomposed into separate sidebar tabs (future work). This implementation covers **Models only** — the core DCF + multiples valuation content.
+
+v1 had WACC / terminal growth / FCF growth / horizon sliders. These are deferred — the backend schema is designed so they can be added as query params later without breaking changes.
+
+### Layout: Summary strip + DCF detail (Option C)
+
+4-model summary chips at the top for instant scanning, DCF detail panel below, multiples as compact rows. Chose over stacked-vertical (matches Financials but buries headline numbers) and 2-col + chart (more complex, less scannable).
+
+### Multiples target: trailing market multiple
+
+Since v2 has no FMP integration, multiples use the company's own trailing multiple from yfinance `.info` (`ev_to_ebitda`, `ev_to_sales`, `price_to_book`) as the target. This produces "market-implied price at current multiple" — a legitimate cross-check against DCF. UI labels make this explicit.
+
+---
+
+## Backend
+
+### New cache
+
+```python
+VALUATION_CACHE = TTLCache(maxsize=128, ttl=3600)
+```
+
+Added to `backend/app/services/data_service.py` alongside the existing statement caches. Never share with other cached functions that have the same argument signature.
+
+### Ported valuation logic
+
+Port `run_dcf`, `run_ev_ebitda`, `run_ev_revenue`, `run_price_to_book`, `compute_historical_growth_rate` from `../prism/services/valuation_service.py` directly into `data_service.py`. No new service file — keeps v2 flat.
+
+DCF defaults: `wacc=0.10`, `terminal_growth=0.03`, `projection_years=5`. Growth rate derived from historical FCF median (capped ±50%).
+
+### `get_valuation(symbol)` data assembly
+
+1. Annual cash flow → FCF series: `Operating Cash Flow + Capital Expenditure` (CapEx is negative in yfinance — addition, not subtraction)
+2. Quarterly income → EBITDA TTM via `_statement_ttm(frame_q, "EBITDA", "Normalized EBITDA")`
+3. Quarterly income → Revenue TTM via `_statement_ttm(frame_q, "Total Revenue")`
+4. Quarterly balance → MRQ values via `_balance_value`:
+ - `Total Debt`
+ - `Cash And Cash Equivalents` or `Cash Cash Equivalents And Short Term Investments`
+ - `Preferred Stock`
+ - `Minority Interest`
+ - `Stockholders Equity` (for P/B: book value per share = equity / shares)
+5. Shares: `get_shares_outstanding(sym)`
+6. Current price: `get_company_info(sym).get("currentPrice")`
+7. Multiples targets: `info.get("enterpriseToEbitda")`, `info.get("enterpriseToRevenue")`, `info.get("priceToBook")`
+
+### Route
+
+```python
+@app.get("/api/tickers/{symbol}/valuation", response_model=ValuationResponse)
+def ticker_valuation(symbol: str) -> dict:
+ return data_service.get_valuation(symbol)
+```
+
+No query params in v1 scope. WACC/growth overrides can be added as optional `Query` params later.
+
+### Schema (`backend/app/schemas.py`)
+
+```python
+class DcfResult(BaseModel):
+ available: bool = True
+ error: str | None = None
+ intrinsic_value_per_share: float | None = None
+ enterprise_value: float | None = None
+ equity_value: float | None = None
+ net_debt: float | None = None
+ cash_and_equivalents: float | None = None
+ total_debt: float | None = None
+ terminal_value_pv: float | None = None
+ fcf_pv_sum: float | None = None
+ growth_rate_used: float | None = None
+ base_fcf: float | None = None
+ wacc: float = 0.10
+ terminal_growth: float = 0.03
+
+class MultipleResult(BaseModel):
+ available: bool = True
+ implied_price_per_share: float | None = None
+ implied_ev: float | None = None
+ equity_value: float | None = None
+ net_debt: float | None = None
+ multiple_used: float | None = None
+
+class ValuationResponse(BaseModel):
+ symbol: str
+ current_price: float | None = None
+ shares_outstanding: float | None = None
+ dcf: DcfResult
+ ev_ebitda: MultipleResult
+ ev_revenue: MultipleResult
+ price_to_book: MultipleResult
+```
+
+---
+
+## Frontend
+
+### New files
+
+**`frontend/components/prism/ValuationPage.tsx`**
+Mirrors `FinancialsPage.tsx` exactly: same props (`ticker`, `overview`, `isSaved`, `onToggleWatchlist`), same `useEffect` + cancellation flag pattern, same loading/error/ready states. Renders `TickerHeader` + `KPIStrip` + `ValuationCard`.
+
+**`frontend/components/prism/ValuationCard.tsx`**
+Three sections:
+
+1. **Summary strip** — 5 chips in a row (current price + 4 model outputs). Each chip shows label, implied price (mono font), and ±% vs current (green/red). DCF error message shown inline if `dcf.error` is set. Unavailable chips show "—".
+
+2. **DCF detail panel** — two-column layout:
+ - Left: assumption metadata — growth rate used, WACC, terminal growth, base FCF (TTM), EV bridge table (EV → −net debt → −other claims → = equity value)
+ - Right: per-share summary — intrinsic value, market price, gap
+
+3. **Multiples rows** — compact table: EV/EBITDA, EV/Revenue, P/Book. Each row: multiple used, implied price, ±% vs current. Rows with `available: false` are hidden.
+
+CSS prefix: `.psm-val-*` in `frontend/app/prism-shell.css`.
+
+### Wiring edits (existing files)
+
+| File | Change |
+|---|---|
+| `frontend/lib/overview.ts` | Remove `disabled: true` from valuation nav item |
+| `frontend/lib/api.ts` | Add `valuation(symbol)` → `request<ValuationResponse>(...)` |
+| `frontend/types/api.ts` | Add `DcfResult`, `MultipleResult`, `ValuationResponse` types |
+| `frontend/app/page.tsx` | Add `tab === "valuation"` branch rendering `<ValuationPage>` |
+
+---
+
+## Error Handling
+
+| Condition | Backend | Frontend |
+|---|---|---|
+| DCF math error (negative FCF, WACC ≤ 0) | `dcf.error = "<message>"`, `available: true` | Summary chip shows "—", DCF panel shows error message inline |
+| DCF insufficient data (<2 FCF points) | `dcf = DcfResult(available=False)` | Summary chip shows "—", DCF panel shows "Insufficient free cash flow history" |
+| Multiple unavailable (zero EBITDA, missing data) | `MultipleResult(available=False)` | Chip shows "—", multiples row hidden |
+| Full endpoint failure | 500 / exception propagates as 500 | `ValuationPage` error state: "Valuation data unavailable for {ticker}" |
+
+---
+
+## Testing
+
+**`backend/tests/test_api.py`**
+- One test: GET `/api/tickers/AAPL/valuation` with monkeypatched `get_valuation` returning a valid dict. Assert 200 + response shape.
+
+**`backend/tests/test_data_service.py`**
+- Happy path: valid FCF series + balance data → intrinsic value populated
+- Negative FCF: `dcf.error` is set, `available` is True
+- Insufficient history (<2 points): `dcf.available` is False
+- Zero shares: `dcf.available` is False
+- Missing multiples data: `MultipleResult.available` is False
+- All monkeypatched — no live yfinance calls
+
+**Cache cleanup:** `clear_service_caches()` updated to call `VALUATION_CACHE.clear()`.
+
+**Frontend:** `npm run build` + `npm run lint` (no frontend test runner per project convention).