Compare commits

..

6 Commits

Author SHA1 Message Date
48417a03f5 feat: show stock names as primary display in history, strategy, and backtest pages
All checks were successful
Deploy to Production / deploy (push) Successful in 1m32s
2026-02-16 12:52:56 +09:00
87dff8bfa7 feat: show stock names as primary display in portfolio card, detail, and rebalance pages 2026-02-16 12:51:56 +09:00
628b431171 feat: include stock names in snapshot, transaction, and backtest transaction API responses 2026-02-16 12:50:21 +09:00
b6c22f70ae feat: add name field to SnapshotHoldingResponse, TransactionResponse, and backtest TransactionItem schemas 2026-02-16 12:49:18 +09:00
b92f8f298b docs: add stock name display implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 12:47:13 +09:00
c836c133dd docs: add stock name display design
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 12:44:54 +09:00
16 changed files with 854 additions and 24 deletions

View File

@ -17,6 +17,7 @@ from app.schemas.backtest import (
EquityCurvePoint, RebalanceHoldings, HoldingItem, TransactionItem,
)
from app.services.backtest import submit_backtest
from app.services.rebalance import RebalanceService
router = APIRouter(prefix="/api/backtest", tags=["backtest"])
@ -228,11 +229,17 @@ async def get_transactions(
.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,

View File

@ -231,7 +231,25 @@ async def get_transactions(
.limit(limit)
.all()
)
return transactions
# 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
]
@router.post("/{portfolio_id}/transactions", response_model=TransactionResponse, status_code=status.HTTP_201_CREATED)

View File

@ -16,6 +16,7 @@ from app.schemas.portfolio import (
ReturnsResponse, ReturnDataPoint,
)
from app.services.price_service import PriceService
from app.services.rebalance import RebalanceService
router = APIRouter(prefix="/api/portfolios", tags=["snapshots"])
@ -110,6 +111,10 @@ async def create_snapshot(
db.commit()
db.refresh(snapshot)
# Get stock names
name_service = RebalanceService(db)
names = name_service.get_stock_names(tickers)
return SnapshotResponse(
id=snapshot.id,
portfolio_id=snapshot.portfolio_id,
@ -118,6 +123,7 @@ async def create_snapshot(
holdings=[
SnapshotHoldingResponse(
ticker=h.ticker,
name=names.get(h.ticker),
quantity=h.quantity,
price=h.price,
value=h.value,
@ -150,6 +156,11 @@ async def get_snapshot(
if not snapshot:
raise HTTPException(status_code=404, detail="Snapshot not found")
# 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,
@ -158,6 +169,7 @@ async def get_snapshot(
holdings=[
SnapshotHoldingResponse(
ticker=h.ticker,
name=names.get(h.ticker),
quantity=h.quantity,
price=h.price,
value=h.value,

View File

@ -121,6 +121,7 @@ class TransactionItem(BaseModel):
id: int
date: date
ticker: str
name: str | None = None
action: str
shares: int
price: FloatDecimal

View File

@ -71,6 +71,7 @@ class TransactionCreate(TransactionBase):
class TransactionResponse(TransactionBase):
id: int
name: str | None = None
class Config:
from_attributes = True
@ -113,6 +114,7 @@ class PortfolioDetail(PortfolioResponse):
# Snapshot schemas
class SnapshotHoldingResponse(BaseModel):
ticker: str
name: str | None = None
quantity: int
price: FloatDecimal
value: FloatDecimal

View File

@ -0,0 +1,52 @@
# Stock Name Display Design
## Problem
Portfolio-related menus display stock codes (e.g., "095570") instead of stock names (e.g., "AJ네트웍스"), making them hard to read. Stock names should be the primary display, with codes available via tooltip on hover.
## Decision
- Display stock name as primary text everywhere
- Show stock code via browser-native tooltip (`title` attribute) on hover
- Fallback to stock code when name is unavailable: `name || ticker`
## Display Pattern
```tsx
<span title={item.ticker}>{item.name || item.ticker}</span>
```
## Backend Changes
### Schemas to Update
| Schema | Change |
|--------|--------|
| `SnapshotHoldingResponse` | Add `name: str \| None = None` |
| `TransactionResponse` | Add `name: str \| None = None` |
### API Endpoints to Update
| Endpoint | Change |
|----------|--------|
| `GET /snapshots/{snapshot_id}` | Call `get_stock_names()`, populate holdings name |
| `GET /transactions` | Call `get_stock_names()`, populate transactions name |
### Already Has Name (No Change)
- `HoldingWithValue`, `RebalanceItem`, `RebalanceCalculateItem`
- Strategy API responses (all 3 strategies)
- Backtest holdings (`BacktestHolding.name` column exists)
## Frontend Changes
| File | Change |
|------|--------|
| `portfolio-card.tsx` | Add `name` to Holding, show name in badges with ticker tooltip |
| `history/page.tsx` | Add `name` to snapshot holdings, show name in table |
| `[id]/page.tsx` | Transactions tab: add name display. Holdings already uses name. |
| `rebalance/page.tsx` | Swap: name as primary in price inputs and results table |
| `strategy/multi-factor/page.tsx` | Swap: name as primary, ticker as tooltip |
| `strategy/quality/page.tsx` | Swap: name as primary, ticker as tooltip |
| `strategy/value-momentum/page.tsx` | Swap: name as primary, ticker as tooltip |
| `backtest/[id]/page.tsx` | Swap: name as primary for holdings, add name for transactions |

View File

@ -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
<span
key={index}
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground"
title={pieData[index]?.name !== holdings[index]?.name ? holdings.find(h => (h.name || h.ticker) === item.name)?.ticker : undefined}
>
{item.name}
</span>
```
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) => (
<span
key={index}
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground"
title={item.ticker}
>
{item.name}
</span>
))}
```
**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
<td className="px-4 py-3">
<div className="font-medium text-sm">{holding.name || holding.ticker}</div>
{holding.name && (
<div className="text-xs text-muted-foreground">{holding.ticker}</div>
)}
</td>
```
To:
```tsx
<td className="px-4 py-3">
<span className="font-medium text-sm" title={holding.ticker}>{holding.name || holding.ticker}</span>
</td>
```
**Step 3: Update transactions table - show name instead of ticker**
Change line 457 from:
```tsx
<td className="px-4 py-3 text-sm font-medium">{tx.ticker}</td>
```
To:
```tsx
<td className="px-4 py-3 text-sm font-medium" title={tx.ticker}>{tx.name || tx.ticker}</td>
```
**Step 4: Update Target vs Actual section - add title tooltip**
Change line 522 from:
```tsx
<span className="font-medium">{holding?.name || target.ticker}</span>
```
To:
```tsx
<span className="font-medium" title={target.ticker}>{holding?.name || target.ticker}</span>
```
**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<Record<string, string>>({});
```
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<string, string> = {};
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
<td className="px-3 py-3">
<div className="font-medium">{item.ticker}</div>
{item.name && (
<div className="text-xs text-muted-foreground">{item.name}</div>
)}
</td>
```
To:
```tsx
<td className="px-3 py-3">
<span className="font-medium" title={item.ticker}>{item.name || item.ticker}</span>
</td>
```
**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
<td className="px-4 py-2 text-sm text-foreground">
{holding.ticker}
</td>
```
To:
```tsx
<td className="px-4 py-2 text-sm text-foreground" title={holding.ticker}>
{holding.name || holding.ticker}
</td>
```
**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
<td className="px-4 py-3">
<div className="font-medium">{stock.ticker}</div>
<div className="text-xs text-muted-foreground">{stock.name}</div>
</td>
```
To:
```tsx
<td className="px-4 py-3">
<span className="font-medium" title={stock.ticker}>{stock.name || stock.ticker}</span>
</td>
```
**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
<td className="px-4 py-3">
<div className="font-medium">{h.ticker}</div>
<div className="text-xs text-muted-foreground">{h.name}</div>
</td>
```
To:
```tsx
<td className="px-4 py-3">
<span className="font-medium" title={h.ticker}>{h.name || h.ticker}</span>
</td>
```
**Step 3: Update transactions tab display**
Change line 425 from:
```tsx
<td className="px-4 py-3 text-sm font-medium">{t.ticker}</td>
```
To:
```tsx
<td className="px-4 py-3 text-sm font-medium" title={t.ticker}>{t.name || t.ticker}</td>
```
**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"
```

View File

@ -60,6 +60,7 @@ interface TransactionItem {
id: number;
date: string;
ticker: string;
name: string | null;
action: string;
shares: number;
price: number;
@ -390,8 +391,7 @@ export default function BacktestDetailPage() {
{selectedHoldings.holdings.map((h) => (
<tr key={h.ticker} className="hover:bg-muted/50">
<td className="px-4 py-3">
<div className="font-medium">{h.ticker}</div>
<div className="text-xs text-muted-foreground">{h.name}</div>
<span className="font-medium" title={h.ticker}>{h.name || h.ticker}</span>
</td>
<td className="px-4 py-3 text-sm text-right">{formatNumber(h.weight)}%</td>
<td className="px-4 py-3 text-sm text-right">{formatCurrency(h.shares)}</td>
@ -422,7 +422,7 @@ export default function BacktestDetailPage() {
{transactions.map((t) => (
<tr key={t.id} className="hover:bg-muted/50">
<td className="px-4 py-3 text-sm">{t.date}</td>
<td className="px-4 py-3 text-sm font-medium">{t.ticker}</td>
<td className="px-4 py-3 text-sm font-medium" title={t.ticker}>{t.name || t.ticker}</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-1 rounded text-xs ${
t.action === 'buy'

View File

@ -22,6 +22,7 @@ interface SnapshotDetail {
snapshot_date: string;
holdings: {
ticker: string;
name: string | null;
quantity: number;
price: string;
value: string;
@ -424,8 +425,8 @@ export default function PortfolioHistoryPage() {
<tbody className="divide-y divide-border">
{selectedSnapshot.holdings.map((holding) => (
<tr key={holding.ticker}>
<td className="px-4 py-2 text-sm text-foreground">
{holding.ticker}
<td className="px-4 py-2 text-sm text-foreground" title={holding.ticker}>
{holding.name || holding.ticker}
</td>
<td className="px-4 py-2 text-sm text-right text-foreground">
{holding.quantity.toLocaleString()}

View File

@ -33,6 +33,7 @@ interface Target {
interface Transaction {
id: number;
ticker: string;
name: string | null;
tx_type: string;
quantity: number;
price: number;
@ -341,10 +342,7 @@ export default function PortfolioDetailPage() {
{portfolio.holdings.map((holding, index) => (
<tr key={holding.ticker}>
<td className="px-4 py-3">
<div className="font-medium text-sm">{holding.name || holding.ticker}</div>
{holding.name && (
<div className="text-xs text-muted-foreground">{holding.ticker}</div>
)}
<span className="font-medium text-sm" title={holding.ticker}>{holding.name || holding.ticker}</span>
</td>
<td className="px-4 py-3 text-sm text-right">
{holding.quantity.toLocaleString()}
@ -454,7 +452,7 @@ export default function PortfolioDetailPage() {
<td className="px-4 py-3 text-sm text-muted-foreground">
{new Date(tx.executed_at).toLocaleString('ko-KR')}
</td>
<td className="px-4 py-3 text-sm font-medium">{tx.ticker}</td>
<td className="px-4 py-3 text-sm font-medium" title={tx.ticker}>{tx.name || tx.ticker}</td>
<td className="px-4 py-3 text-sm text-center">
<span
className={`px-2 py-1 rounded text-xs font-medium ${
@ -519,7 +517,7 @@ export default function PortfolioDetailPage() {
return (
<div key={target.ticker} className="space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">{holding?.name || target.ticker}</span>
<span className="font-medium" title={target.ticker}>{holding?.name || target.ticker}</span>
<span className="text-muted-foreground">
{actualRatio.toFixed(1)}% / {target.target_ratio.toFixed(1)}%
<span

View File

@ -59,6 +59,7 @@ export default function RebalancePage() {
const [result, setResult] = useState<RebalanceResponse | null>(null);
const [calculating, setCalculating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [nameMap, setNameMap] = useState<Record<string, string>>({});
useEffect(() => {
const init = async () => {
@ -81,6 +82,18 @@ export default function RebalancePage() {
initialPrices[ticker] = '';
});
setPrices(initialPrices);
// 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<string, string> = {};
for (const h of detail.holdings) {
if (h.name) names[h.ticker] = h.name;
}
setNameMap(names);
} catch {
// Names are optional, continue without
}
} catch {
router.push('/login');
} finally {
@ -115,6 +128,12 @@ export default function RebalancePage() {
body
);
setResult(data);
// Update name map from results
const newNames = { ...nameMap };
for (const item of data.items) {
if (item.name) newNames[item.ticker] = item.name;
}
setNameMap(newNames);
} catch (err) {
setError(err instanceof Error ? err.message : 'Calculation failed');
} finally {
@ -180,7 +199,7 @@ export default function RebalancePage() {
return (
<div key={ticker}>
<Label htmlFor={`price-${ticker}`}>
{ticker} {target ? `(목표 ${target.target_ratio}%)` : ''} - {getHoldingQty(ticker)}
{nameMap[ticker] || ticker} {target ? `(목표 ${target.target_ratio}%)` : ''} - {getHoldingQty(ticker)}
</Label>
<Input
id={`price-${ticker}`}
@ -297,10 +316,7 @@ export default function RebalancePage() {
{result.items.map((item) => (
<tr key={item.ticker}>
<td className="px-3 py-3">
<div className="font-medium">{item.ticker}</div>
{item.name && (
<div className="text-xs text-muted-foreground">{item.name}</div>
)}
<span className="font-medium" title={item.ticker}>{item.name || item.ticker}</span>
</td>
<td className="px-3 py-3 text-sm text-right">
{item.current_quantity.toLocaleString()}

View File

@ -11,6 +11,7 @@ import { api } from '@/lib/api';
interface HoldingWithValue {
ticker: string;
name: string | null;
current_ratio: number | null;
}

View File

@ -207,8 +207,7 @@ export default function MultiFactorPage() {
<tr key={stock.ticker} className="hover:bg-muted/50">
<td className="px-4 py-3 text-sm font-medium">{stock.rank}</td>
<td className="px-4 py-3">
<div className="font-medium">{stock.ticker}</div>
<div className="text-xs text-muted-foreground">{stock.name}</div>
<span className="font-medium" title={stock.ticker}>{stock.name || stock.ticker}</span>
</td>
<td className="px-4 py-3 text-sm">{stock.sector_name || '-'}</td>
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.market_cap)}</td>

View File

@ -172,8 +172,7 @@ export default function QualityStrategyPage() {
<tr key={stock.ticker} className="hover:bg-muted/50">
<td className="px-4 py-3 text-sm font-medium">{stock.rank}</td>
<td className="px-4 py-3">
<div className="font-medium">{stock.ticker}</div>
<div className="text-xs text-muted-foreground">{stock.name}</div>
<span className="font-medium" title={stock.ticker}>{stock.name || stock.ticker}</span>
</td>
<td className="px-4 py-3 text-sm">{stock.sector_name || '-'}</td>
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.market_cap)}</td>

View File

@ -188,8 +188,7 @@ export default function ValueMomentumPage() {
<tr key={stock.ticker} className="hover:bg-muted/50">
<td className="px-4 py-3 text-sm font-medium">{stock.rank}</td>
<td className="px-4 py-3">
<div className="font-medium">{stock.ticker}</div>
<div className="text-xs text-muted-foreground">{stock.name}</div>
<span className="font-medium" title={stock.ticker}>{stock.name || stock.ticker}</span>
</td>
<td className="px-4 py-3 text-sm">{stock.sector_name || '-'}</td>
<td className="px-4 py-3 text-sm text-right">{formatCurrency(stock.market_cap)}</td>

View File

@ -6,6 +6,7 @@ import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
interface Holding {
ticker: string;
name: string | null;
current_ratio: number | null;
}
@ -61,7 +62,8 @@ export function PortfolioCard({
.filter((h) => h.current_ratio !== null && h.current_ratio > 0)
.slice(0, 6)
.map((h, index) => ({
name: h.ticker,
name: h.name || h.ticker,
ticker: h.ticker,
value: h.current_ratio ?? 0,
color: CHART_COLORS[index % CHART_COLORS.length],
}));
@ -144,6 +146,7 @@ export function PortfolioCard({
<span
key={index}
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground"
title={item.ticker}
>
{item.name}
</span>