diff --git a/docs/plans/2026-02-16-stock-name-display-plan.md b/docs/plans/2026-02-16-stock-name-display-plan.md new file mode 100644 index 0000000..2501489 --- /dev/null +++ b/docs/plans/2026-02-16-stock-name-display-plan.md @@ -0,0 +1,722 @@ +# Stock Name Display Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace stock code displays with stock name as primary text across all portfolio-related views, with stock codes available via tooltip. + +**Architecture:** Backend schemas and API endpoints that currently lack stock names (`SnapshotHoldingResponse`, `TransactionResponse`, backtest `TransactionItem`) get `name` fields added, with name resolution via the existing `RebalanceService.get_stock_names()`. Frontend swaps display pattern from `ticker` (primary) + `name` (subtitle) to `name` (primary) with `title={ticker}` tooltip. + +**Tech Stack:** FastAPI + SQLAlchemy (backend), Next.js 15 + React 19 + TypeScript (frontend) + +--- + +### Task 1: Add `name` field to `SnapshotHoldingResponse` schema + +**Files:** +- Modify: `backend/app/schemas/portfolio.py:114-122` + +**Step 1: Add name field to schema** + +In `backend/app/schemas/portfolio.py`, add `name` field to `SnapshotHoldingResponse`: + +```python +class SnapshotHoldingResponse(BaseModel): + ticker: str + name: str | None = None + quantity: int + price: FloatDecimal + value: FloatDecimal + current_ratio: FloatDecimal + + class Config: + from_attributes = True +``` + +**Step 2: Commit** + +```bash +git add backend/app/schemas/portfolio.py +git commit -m "feat: add name field to SnapshotHoldingResponse schema" +``` + +--- + +### Task 2: Add `name` field to `TransactionResponse` schema + +**Files:** +- Modify: `backend/app/schemas/portfolio.py:72-76` + +**Step 1: Add name field to schema** + +In `backend/app/schemas/portfolio.py`, add `name` field to `TransactionResponse`: + +```python +class TransactionResponse(TransactionBase): + id: int + name: str | None = None + + class Config: + from_attributes = True +``` + +**Step 2: Commit** + +```bash +git add backend/app/schemas/portfolio.py +git commit -m "feat: add name field to TransactionResponse schema" +``` + +--- + +### Task 3: Add `name` field to backtest `TransactionItem` schema + +**Files:** +- Modify: `backend/app/schemas/backtest.py:119-131` + +**Step 1: Add name field to schema** + +In `backend/app/schemas/backtest.py`, add `name` field to `TransactionItem`: + +```python +class TransactionItem(BaseModel): + """Single transaction.""" + id: int + date: date + ticker: str + name: str | None = None + action: str + shares: int + price: FloatDecimal + commission: FloatDecimal + + class Config: + from_attributes = True +``` + +**Step 2: Commit** + +```bash +git add backend/app/schemas/backtest.py +git commit -m "feat: add name field to backtest TransactionItem schema" +``` + +--- + +### Task 4: Populate snapshot holdings with stock names in snapshot API + +**Files:** +- Modify: `backend/app/api/snapshot.py:53-128` (create_snapshot endpoint) +- Modify: `backend/app/api/snapshot.py:131-168` (get_snapshot endpoint) + +**Step 1: Update imports** + +Add `RebalanceService` import at top of `backend/app/api/snapshot.py`: + +```python +from app.services.rebalance import RebalanceService +``` + +**Step 2: Update `create_snapshot` endpoint to include names** + +In the `create_snapshot` function, after getting prices and before creating snapshot, resolve names and include them in the response. Replace the return statement (lines 113-128): + +```python + # Get stock names + name_service = RebalanceService(db) + names = name_service.get_stock_names(tickers) + + return SnapshotResponse( + id=snapshot.id, + portfolio_id=snapshot.portfolio_id, + total_value=snapshot.total_value, + snapshot_date=snapshot.snapshot_date, + holdings=[ + SnapshotHoldingResponse( + ticker=h.ticker, + name=names.get(h.ticker), + quantity=h.quantity, + price=h.price, + value=h.value, + current_ratio=h.current_ratio, + ) + for h in snapshot.holdings + ], + ) +``` + +**Step 3: Update `get_snapshot` endpoint to include names** + +In the `get_snapshot` function, resolve names before returning. Replace lines 153-168: + +```python + # Get stock names + tickers = [h.ticker for h in snapshot.holdings] + name_service = RebalanceService(db) + names = name_service.get_stock_names(tickers) + + return SnapshotResponse( + id=snapshot.id, + portfolio_id=snapshot.portfolio_id, + total_value=snapshot.total_value, + snapshot_date=snapshot.snapshot_date, + holdings=[ + SnapshotHoldingResponse( + ticker=h.ticker, + name=names.get(h.ticker), + quantity=h.quantity, + price=h.price, + value=h.value, + current_ratio=h.current_ratio, + ) + for h in snapshot.holdings + ], + ) +``` + +**Step 4: Commit** + +```bash +git add backend/app/api/snapshot.py +git commit -m "feat: include stock names in snapshot API responses" +``` + +--- + +### Task 5: Populate transactions with stock names in portfolio API + +**Files:** +- Modify: `backend/app/api/portfolio.py:218-234` + +**Step 1: Update imports** + +At top of `backend/app/api/portfolio.py`, `RebalanceService` is already imported (line 21). No change needed. + +**Step 2: Update `get_transactions` endpoint** + +Replace the `get_transactions` function body to resolve names: + +```python +@router.get("/{portfolio_id}/transactions", response_model=List[TransactionResponse]) +async def get_transactions( + portfolio_id: int, + current_user: CurrentUser, + db: Session = Depends(get_db), + limit: int = 50, +): + """Get transaction history for a portfolio.""" + _get_portfolio(db, portfolio_id, current_user.id) + transactions = ( + db.query(Transaction) + .filter(Transaction.portfolio_id == portfolio_id) + .order_by(Transaction.executed_at.desc()) + .limit(limit) + .all() + ) + + # Resolve stock names + tickers = list({tx.ticker for tx in transactions}) + service = RebalanceService(db) + names = service.get_stock_names(tickers) + + return [ + TransactionResponse( + id=tx.id, + ticker=tx.ticker, + name=names.get(tx.ticker), + tx_type=tx.tx_type.value, + quantity=tx.quantity, + price=tx.price, + executed_at=tx.executed_at, + memo=tx.memo, + ) + for tx in transactions + ] +``` + +**Step 3: Commit** + +```bash +git add backend/app/api/portfolio.py +git commit -m "feat: include stock names in transaction API responses" +``` + +--- + +### Task 6: Populate backtest transactions with stock names + +**Files:** +- Modify: `backend/app/api/backtest.py:209-242` + +**Step 1: Add import** + +Add `RebalanceService` import at top of `backend/app/api/backtest.py`: + +```python +from app.services.rebalance import RebalanceService +``` + +**Step 2: Update `get_transactions` endpoint** + +Replace the return logic in the backtest `get_transactions` endpoint: + +```python + transactions = ( + db.query(BacktestTransaction) + .filter(BacktestTransaction.backtest_id == backtest_id) + .order_by(BacktestTransaction.date, BacktestTransaction.id) + .all() + ) + + # Resolve stock names + tickers = list({t.ticker for t in transactions}) + name_service = RebalanceService(db) + names = name_service.get_stock_names(tickers) + + return [ + TransactionItem( + id=t.id, + date=t.date, + ticker=t.ticker, + name=names.get(t.ticker), + action=t.action, + shares=t.shares, + price=t.price, + commission=t.commission, + ) + for t in transactions + ] +``` + +**Step 3: Commit** + +```bash +git add backend/app/api/backtest.py +git commit -m "feat: include stock names in backtest transaction responses" +``` + +--- + +### Task 7: Update portfolio-card.tsx - show name instead of ticker + +**Files:** +- Modify: `frontend/src/components/portfolio/portfolio-card.tsx` + +**Step 1: Add `name` to Holding interface** + +```typescript +interface Holding { + ticker: string; + name: string | null; + current_ratio: number | null; +} +``` + +**Step 2: Update pieData mapping to use name** + +Change line 64 from `name: h.ticker` to: + +```typescript +name: h.name || h.ticker, +``` + +**Step 3: Add title attribute to holdings preview badges** + +Change the badge span (line 144-149) to include a `title`: + +```tsx + (h.name || h.ticker) === item.name)?.ticker : undefined} +> + {item.name} + +``` + +Actually, simpler approach - since `pieData` is derived from `holdings`, we can track the original ticker. Let's use a simpler approach: map pieData to include the original ticker, then use `title={item.ticker}`: + +Change the pieData mapping (lines 60-67): + +```typescript +const pieData = holdings + .filter((h) => h.current_ratio !== null && h.current_ratio > 0) + .slice(0, 6) + .map((h, index) => ({ + name: h.name || h.ticker, + ticker: h.ticker, + value: h.current_ratio ?? 0, + color: CHART_COLORS[index % CHART_COLORS.length], + })); +``` + +Then update the badge (lines 143-149): + +```tsx +{pieData.slice(0, 4).map((item, index) => ( + + {item.name} + +))} +``` + +**Step 4: Also update HoldingWithValue in portfolio list page** + +In `frontend/src/app/portfolio/page.tsx`, add `name` to the `HoldingWithValue` interface (line 12-15): + +```typescript +interface HoldingWithValue { + ticker: string; + name: string | null; + current_ratio: number | null; +} +``` + +**Step 5: Commit** + +```bash +git add frontend/src/components/portfolio/portfolio-card.tsx frontend/src/app/portfolio/page.tsx +git commit -m "feat: show stock names in portfolio cards" +``` + +--- + +### Task 8: Update portfolio detail page - fix holdings ticker subtitle removal + transactions name + +**Files:** +- Modify: `frontend/src/app/portfolio/[id]/page.tsx` + +**Step 1: Add `name` to Transaction interface** + +```typescript +interface Transaction { + id: number; + ticker: string; + name: string | null; + tx_type: string; + quantity: number; + price: number; + executed_at: string; +} +``` + +**Step 2: Update holdings table - remove ticker subtitle, add title tooltip** + +Change lines 343-348 from: + +```tsx +