Compare commits
6 Commits
98d8c1115e
...
48417a03f5
| Author | SHA1 | Date | |
|---|---|---|---|
| 48417a03f5 | |||
| 87dff8bfa7 | |||
| 628b431171 | |||
| b6c22f70ae | |||
| b92f8f298b | |||
| c836c133dd |
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -121,6 +121,7 @@ class TransactionItem(BaseModel):
|
||||
id: int
|
||||
date: date
|
||||
ticker: str
|
||||
name: str | None = None
|
||||
action: str
|
||||
shares: int
|
||||
price: FloatDecimal
|
||||
|
||||
@ -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
|
||||
|
||||
52
docs/plans/2026-02-16-stock-name-display-design.md
Normal file
52
docs/plans/2026-02-16-stock-name-display-design.md
Normal 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 |
|
||||
722
docs/plans/2026-02-16-stock-name-display-plan.md
Normal file
722
docs/plans/2026-02-16-stock-name-display-plan.md
Normal 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"
|
||||
```
|
||||
@ -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'
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()}
|
||||
|
||||
@ -11,6 +11,7 @@ import { api } from '@/lib/api';
|
||||
|
||||
interface HoldingWithValue {
|
||||
ticker: string;
|
||||
name: string | null;
|
||||
current_ratio: number | null;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user