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 + +
{holding.name || holding.ticker}
+ {holding.name && ( +
{holding.ticker}
+ )} + +``` + +To: + +```tsx + + {holding.name || holding.ticker} + +``` + +**Step 3: Update transactions table - show name instead of ticker** + +Change line 457 from: + +```tsx +{tx.ticker} +``` + +To: + +```tsx +{tx.name || tx.ticker} +``` + +**Step 4: Update Target vs Actual section - add title tooltip** + +Change line 522 from: + +```tsx +{holding?.name || target.ticker} +``` + +To: + +```tsx +{holding?.name || target.ticker} +``` + +**Step 5: Commit** + +```bash +git add frontend/src/app/portfolio/[id]/page.tsx +git commit -m "feat: show stock names as primary display in portfolio detail" +``` + +--- + +### Task 9: Update rebalance page - name as primary display + +**Files:** +- Modify: `frontend/src/app/portfolio/[id]/rebalance/page.tsx` + +**Step 1: Fetch stock names for price input labels** + +The rebalance page needs names for the price input section. The targets and holdings don't have names, but the rebalance result does. We need to store a name map. Add state and fetch names when targets/holdings load. + +Add `nameMap` state after existing state declarations (after line 61): + +```typescript +const [nameMap, setNameMap] = useState>({}); +``` + +In the `init` function, after setting targets and holdings, fetch portfolio detail to get names: + +```typescript +// Fetch stock names from portfolio detail +try { + const detail = await api.get<{ holdings: { ticker: string; name: string | null }[] }>(`/api/portfolios/${portfolioId}/detail`); + const names: Record = {}; + for (const h of detail.holdings) { + if (h.name) names[h.ticker] = h.name; + } + setNameMap(names); +} catch { + // Names are optional, continue without +} +``` + +**Step 2: Update price input labels to use name** + +Change line 183 from: + +```tsx +{ticker} {target ? `(목표 ${target.target_ratio}%)` : ''} - 보유 {getHoldingQty(ticker)}주 +``` + +To: + +```tsx +{nameMap[ticker] || ticker} {target ? `(목표 ${target.target_ratio}%)` : ''} - 보유 {getHoldingQty(ticker)}주 +``` + +**Step 3: Update results table - name as primary** + +Change lines 299-304 from: + +```tsx + +
{item.ticker}
+ {item.name && ( +
{item.name}
+ )} + +``` + +To: + +```tsx + + {item.name || item.ticker} + +``` + +**Step 4: Also update nameMap from rebalance results** + +After the calculate function sets the result, update nameMap with any names from the result: + +In the `calculate` function, after `setResult(data)` (line 117), add: + +```typescript +// Update name map from results +const newNames = { ...nameMap }; +for (const item of data.items) { + if (item.name) newNames[item.ticker] = item.name; +} +setNameMap(newNames); +``` + +**Step 5: Commit** + +```bash +git add frontend/src/app/portfolio/[id]/rebalance/page.tsx +git commit -m "feat: show stock names as primary display in rebalance page" +``` + +--- + +### Task 10: Update portfolio history page - show names in snapshot + +**Files:** +- Modify: `frontend/src/app/portfolio/[id]/history/page.tsx` + +**Step 1: Add `name` to SnapshotDetail holdings interface** + +Change the `holdings` type in `SnapshotDetail` interface (lines 23-29): + +```typescript +holdings: { + ticker: string; + name: string | null; + quantity: number; + price: string; + value: string; + current_ratio: string; +}[]; +``` + +**Step 2: Update snapshot detail modal table** + +Change line 428 from: + +```tsx + + {holding.ticker} + +``` + +To: + +```tsx + + {holding.name || holding.ticker} + +``` + +**Step 3: Commit** + +```bash +git add frontend/src/app/portfolio/[id]/history/page.tsx +git commit -m "feat: show stock names in portfolio history snapshots" +``` + +--- + +### Task 11: Update strategy pages - name as primary (3 files) + +**Files:** +- Modify: `frontend/src/app/strategy/multi-factor/page.tsx:209-211` +- Modify: `frontend/src/app/strategy/quality/page.tsx:174-176` +- Modify: `frontend/src/app/strategy/value-momentum/page.tsx:190-192` + +**Step 1: Update multi-factor page** + +Change lines 209-211 from: + +```tsx + +
{stock.ticker}
+
{stock.name}
+ +``` + +To: + +```tsx + + {stock.name || stock.ticker} + +``` + +**Step 2: Update quality page** + +Same change at lines 174-176. + +**Step 3: Update value-momentum page** + +Same change at lines 190-192. + +**Step 4: Commit** + +```bash +git add frontend/src/app/strategy/multi-factor/page.tsx frontend/src/app/strategy/quality/page.tsx frontend/src/app/strategy/value-momentum/page.tsx +git commit -m "feat: show stock names as primary display in strategy pages" +``` + +--- + +### Task 12: Update backtest detail page - name as primary + +**Files:** +- Modify: `frontend/src/app/backtest/[id]/page.tsx` + +**Step 1: Add `name` to TransactionItem interface** + +```typescript +interface TransactionItem { + id: number; + date: string; + ticker: string; + name: string | null; + action: string; + shares: number; + price: number; + commission: number; +} +``` + +**Step 2: Update holdings tab display** + +Change lines 392-394 from: + +```tsx + +
{h.ticker}
+
{h.name}
+ +``` + +To: + +```tsx + + {h.name || h.ticker} + +``` + +**Step 3: Update transactions tab display** + +Change line 425 from: + +```tsx +{t.ticker} +``` + +To: + +```tsx +{t.name || t.ticker} +``` + +**Step 4: Commit** + +```bash +git add frontend/src/app/backtest/[id]/page.tsx +git commit -m "feat: show stock names as primary display in backtest detail" +``` + +--- + +### Task 13: Verify frontend build + +**Step 1: Run frontend build to check for TypeScript errors** + +```bash +cd frontend && npm run build +``` + +Expected: Build succeeds with no type errors. + +**Step 2: If build fails, fix any TypeScript errors and re-run** + +**Step 3: Commit any fixes** + +```bash +git add -A && git commit -m "fix: resolve build errors from stock name display changes" +```