docs: add scenario gap analysis results and implementation plan
Expert team identified 12 gaps across 6 scenarios. Prioritized into 3 phases: Critical (rebalance apply, strategy-to-portfolio link), Important (signal holdings, DC limits, benchmark, signal PnL), and Nice-to-have (comparison UI, walk-forward, undo). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7cff4003d7
commit
c97bb8595e
395
docs/plans/2026-02-19-scenario-gap-analysis-plan.md
Normal file
395
docs/plans/2026-02-19-scenario-gap-analysis-plan.md
Normal file
@ -0,0 +1,395 @@
|
||||
# 시나리오 갭 분석 결과 및 구현 계획
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** DC형 퇴직연금 퀀트 포트폴리오 관리의 6대 시나리오에서 발견된 Gap을 해소하여 end-to-end 사용 흐름을 완성한다.
|
||||
|
||||
**Architecture:** 기존 FastAPI + Next.js 구조를 유지하면서, 누락된 API 엔드포인트와 프론트엔드 UI를 추가한다. 백엔드 변경은 기존 모델/서비스를 확장하고, 프론트엔드는 기존 컴포넌트 패턴을 따른다.
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy, Next.js 15, React 19, TypeScript, Radix UI, Recharts
|
||||
|
||||
---
|
||||
|
||||
## 전문가 팀 갭 분석 결과
|
||||
|
||||
### 종합 매핑표
|
||||
|
||||
| # | 요구사항 | 상태 | 우선순위 |
|
||||
|---|---------|------|----------|
|
||||
| 1.1 | 퇴직연금 포트폴리오 생성 | IMPLEMENTED | - |
|
||||
| 1.2 | DC형 위험자산 70% 제한 경고 | MISSING | Important |
|
||||
| 1.3 | 보유종목 일괄 입력 UI | PARTIAL (API만 존재) | Important |
|
||||
| 1.4 | 실시간 평가금액 표시 | IMPLEMENTED | - |
|
||||
| 2.1 | 전략 결과 → 포트폴리오 목표 연결 | MISSING | Critical |
|
||||
| 2.2 | 팩터 점수/투명성 | IMPLEMENTED | - |
|
||||
| 2.3 | DC 투자가능 종목 필터 | MISSING | Nice-to-have |
|
||||
| 2.4 | 전략 결과 비교 UI | PARTIAL | Nice-to-have |
|
||||
| 3.1 | 전 전략 백테스트 지원 | IMPLEMENTED | - |
|
||||
| 3.2 | Out-of-sample/Walk-forward | MISSING | Nice-to-have |
|
||||
| 3.3 | 백테스트 에러 처리 | PARTIAL | Nice-to-have |
|
||||
| 4.1 | 리밸런싱 결과 → 일괄 거래 생성 | MISSING | Critical |
|
||||
| 4.2 | 최소 거래 금액 고려 | MISSING | Nice-to-have |
|
||||
| 4.3 | 리밸런싱 후 위험자산 비율 체크 | MISSING | Important |
|
||||
| 5.1 | 신호별 PnL 추적 | MISSING | Important |
|
||||
| 5.2 | 실행된 신호 취소/수정 | MISSING | Nice-to-have |
|
||||
| 5.3 | 실행 모달에 현재 보유량 표시 | MISSING | Important |
|
||||
| 5.4 | 포지션 사이징 가이드 | PARTIAL | Nice-to-have |
|
||||
| 6.1 | TWR 수익률 계산 | IMPLEMENTED | - |
|
||||
| 6.2 | 포트폴리오 벤치마크 비교 | PARTIAL (백테스트만) | Important |
|
||||
| 6.3 | 스냅샷 데이터 충분성 | IMPLEMENTED | - |
|
||||
| 6.4 | 실현/미실현 수익 구분 | MISSING | Important |
|
||||
| 6.5 | 대시보드 전체 현황 | IMPLEMENTED | - |
|
||||
|
||||
### 전문가별 핵심 의견
|
||||
|
||||
**Q (퀀트 전략가):**
|
||||
- 전략 실행 → 포트폴리오 반영 연결이 완전히 끊겨 있음. 전략을 실행해도 결과를 수동으로 옮겨야 하므로 시스템의 핵심 가치가 반감됨.
|
||||
- 신호 기반 매매의 성과를 추적할 수 없어 전략 개선이 불가능함.
|
||||
|
||||
**PM (포트폴리오 매니저):**
|
||||
- 리밸런싱 계산 후 "적용" 기능이 없어, 계산 결과를 보고 다시 수동으로 거래를 하나씩 입력해야 함. 핵심 워크플로우의 마지막 단계가 빠져있음.
|
||||
- 실현/미실현 수익 구분이 없으면 세금 신고나 성과 분석이 부정확함.
|
||||
|
||||
**P (DC형 퇴직연금 전문가):**
|
||||
- DC형 퇴직연금의 위험자산 70% 제한은 법적 요건. 경고 없이 운용하면 퇴직연금사업자의 제재 대상이 될 수 있음. 최소한 경고 수준은 필수.
|
||||
- 단, 이 시스템은 개인용이므로 제재라기보다 자기 관리 차원의 알림이면 충분.
|
||||
|
||||
**SE (시스템 엔지니어):**
|
||||
- 데이터 파이프라인과 백테스트 엔진은 안정적으로 구현됨.
|
||||
- 신호 실행 시 동시성 문제 (같은 신호 중복 실행) 검토 필요하나, 단일 사용자 시스템이므로 당장 Critical은 아님.
|
||||
|
||||
**DA (Devil's Advocate):**
|
||||
- "전략 → 포트폴리오 → 리밸런싱 → 성과 추적"의 핵심 루프가 중간중간 끊겨 있음. 각 기능이 개별적으로는 존재하지만 **연결 흐름이 없으면 Excel 스프레드시트보다 나을 게 없다.**
|
||||
- 특히 리밸런싱 결과 적용과 전략→목표 연결은 이 시스템의 존재 이유. 이것 없이는 단순 조회 도구에 불과.
|
||||
- 신호 실행 취소가 없으면 실수 한 번에 데이터 정합성이 깨질 수 있음. 최소한 거래 삭제 기능은 필요.
|
||||
- 벤치마크 비교가 백테스트에만 있고 실제 포트폴리오에 없는 것은 "과거는 분석하면서 현재는 분석 못하는" 아이러니.
|
||||
|
||||
---
|
||||
|
||||
## 구현 우선순위
|
||||
|
||||
DA의 의견을 반영하여, **"핵심 루프 완성"**에 집중합니다.
|
||||
|
||||
### Phase 1: Critical - 핵심 루프 연결 (Task 1-2)
|
||||
### Phase 2: Important - 실용성 강화 (Task 3-6)
|
||||
### Phase 3: Nice-to-have - 고도화 (별도 계획)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 리밸런싱 결과 일괄 적용 API + UI
|
||||
|
||||
리밸런싱 계산 결과에서 "적용" 버튼을 누르면 매수/매도 거래가 일괄 생성되는 기능.
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/portfolio.py` (리밸런싱 적용 엔드포인트 추가)
|
||||
- Modify: `backend/app/schemas/portfolio.py` (적용 요청/응답 스키마)
|
||||
- Modify: `frontend/src/app/portfolio/[id]/rebalance/page.tsx` (적용 버튼 + 확인 모달)
|
||||
- Test: `backend/tests/e2e/test_rebalance_flow.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
# backend/tests/e2e/test_rebalance_flow.py - 기존 파일에 추가
|
||||
def test_apply_rebalance(client, auth_headers, db_session):
|
||||
"""리밸런싱 결과를 적용하면 거래가 일괄 생성된다."""
|
||||
# Setup: portfolio with targets and holdings
|
||||
# ...
|
||||
response = client.post(
|
||||
f"/api/portfolios/{portfolio_id}/rebalance/apply",
|
||||
json={
|
||||
"items": [
|
||||
{"ticker": "005930", "action": "buy", "quantity": 10, "price": 70000},
|
||||
{"ticker": "000660", "action": "sell", "quantity": 5, "price": 150000},
|
||||
]
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert len(data["transactions"]) == 2
|
||||
assert data["transactions"][0]["tx_type"] == "buy"
|
||||
assert data["transactions"][1]["tx_type"] == "sell"
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `backend/.venv/bin/pytest backend/tests/e2e/test_rebalance_flow.py::test_apply_rebalance -v`
|
||||
Expected: FAIL (endpoint does not exist)
|
||||
|
||||
**Step 3: Add backend schema**
|
||||
|
||||
```python
|
||||
# backend/app/schemas/portfolio.py에 추가
|
||||
class RebalanceApplyItem(BaseModel):
|
||||
ticker: str
|
||||
action: str # "buy" or "sell"
|
||||
quantity: int = Field(..., gt=0)
|
||||
price: FloatDecimal = Field(..., gt=0)
|
||||
|
||||
class RebalanceApplyRequest(BaseModel):
|
||||
items: List[RebalanceApplyItem]
|
||||
|
||||
class RebalanceApplyResponse(BaseModel):
|
||||
transactions: List[TransactionResponse]
|
||||
holdings_updated: int
|
||||
```
|
||||
|
||||
**Step 4: Add backend endpoint**
|
||||
|
||||
```python
|
||||
# backend/app/api/portfolio.py에 추가
|
||||
@router.post("/{portfolio_id}/rebalance/apply", response_model=RebalanceApplyResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def apply_rebalance(
|
||||
portfolio_id: int,
|
||||
data: RebalanceApplyRequest,
|
||||
current_user: CurrentUser,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""리밸런싱 결과를 적용하여 거래를 일괄 생성한다."""
|
||||
portfolio = _get_portfolio(db, portfolio_id, current_user.id)
|
||||
transactions = []
|
||||
|
||||
for item in data.items:
|
||||
tx_type = TransactionType(item.action)
|
||||
transaction = Transaction(
|
||||
portfolio_id=portfolio_id,
|
||||
ticker=item.ticker,
|
||||
tx_type=tx_type,
|
||||
quantity=item.quantity,
|
||||
price=item.price,
|
||||
executed_at=datetime.utcnow(),
|
||||
memo="리밸런싱 적용",
|
||||
)
|
||||
db.add(transaction)
|
||||
|
||||
# Update holding (기존 add_transaction 로직 재사용)
|
||||
holding = db.query(Holding).filter(
|
||||
Holding.portfolio_id == portfolio_id,
|
||||
Holding.ticker == item.ticker,
|
||||
).first()
|
||||
|
||||
if tx_type == TransactionType.BUY:
|
||||
if holding:
|
||||
total_value = (holding.quantity * holding.avg_price) + (item.quantity * item.price)
|
||||
new_quantity = holding.quantity + item.quantity
|
||||
holding.quantity = new_quantity
|
||||
holding.avg_price = total_value / new_quantity if new_quantity > 0 else 0
|
||||
else:
|
||||
holding = Holding(
|
||||
portfolio_id=portfolio_id,
|
||||
ticker=item.ticker,
|
||||
quantity=item.quantity,
|
||||
avg_price=item.price,
|
||||
)
|
||||
db.add(holding)
|
||||
elif tx_type == TransactionType.SELL:
|
||||
if not holding or holding.quantity < item.quantity:
|
||||
raise HTTPException(status_code=400, detail=f"Insufficient quantity for {item.ticker}")
|
||||
holding.quantity -= item.quantity
|
||||
if holding.quantity == 0:
|
||||
db.delete(holding)
|
||||
|
||||
transactions.append(transaction)
|
||||
|
||||
db.commit()
|
||||
for tx in transactions:
|
||||
db.refresh(tx)
|
||||
|
||||
return RebalanceApplyResponse(
|
||||
transactions=transactions,
|
||||
holdings_updated=len(transactions),
|
||||
)
|
||||
```
|
||||
|
||||
**Step 5: Run test to verify it passes**
|
||||
|
||||
Run: `backend/.venv/bin/pytest backend/tests/e2e/test_rebalance_flow.py::test_apply_rebalance -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 6: Add frontend "적용" 버튼 + 확인 모달**
|
||||
|
||||
`frontend/src/app/portfolio/[id]/rebalance/page.tsx`에 추가:
|
||||
- 리밸런싱 계산 결과 테이블 아래에 "리밸런싱 적용" 버튼
|
||||
- 클릭 시 확인 모달 표시 (매수/매도 요약)
|
||||
- 사용자가 각 항목의 실제 체결가를 수정 가능
|
||||
- "적용" 시 POST /api/portfolios/{id}/rebalance/apply 호출
|
||||
- 성공 시 포트폴리오 상세 페이지로 이동
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/api/portfolio.py backend/app/schemas/portfolio.py \
|
||||
frontend/src/app/portfolio/\[id\]/rebalance/page.tsx \
|
||||
backend/tests/e2e/test_rebalance_flow.py
|
||||
git commit -m "feat: add bulk rebalance apply endpoint and UI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 전략 결과 → 포트폴리오 목표 연결
|
||||
|
||||
전략 실행 결과에서 "포트폴리오에 적용" 버튼으로 목표 배분을 설정하는 기능.
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/app/strategy/multi-factor/page.tsx` (적용 버튼 추가)
|
||||
- Modify: `frontend/src/app/strategy/quality/page.tsx` (적용 버튼 추가)
|
||||
- Modify: `frontend/src/app/strategy/value-momentum/page.tsx` (적용 버튼 추가)
|
||||
- Test: 프론트엔드 E2E 또는 수동 테스트
|
||||
|
||||
**Step 1: 전략 결과 페이지에 "포트폴리오에 적용" 버튼 추가**
|
||||
|
||||
각 전략 결과 페이지에:
|
||||
- 포트폴리오 선택 드롭다운 (GET /api/portfolios)
|
||||
- "목표 배분으로 적용" 버튼
|
||||
- 클릭 시: 전략 결과의 종목 + 동일 비중을 PUT /api/portfolios/{id}/targets로 전송
|
||||
- 기존 목표를 덮어쓸지 확인 모달
|
||||
|
||||
**Step 2: 공통 컴포넌트 생성**
|
||||
|
||||
```tsx
|
||||
// frontend/src/components/strategy/apply-to-portfolio.tsx
|
||||
// 포트폴리오 선택 + 적용 버튼 + 확인 모달
|
||||
// 3개 전략 페이지에서 공유
|
||||
```
|
||||
|
||||
**Step 3: 각 전략 페이지에 컴포넌트 배치**
|
||||
|
||||
**Step 4: 수동 테스트**
|
||||
|
||||
전략 실행 → 결과에서 포트폴리오 선택 → 적용 → 포트폴리오 목표 확인
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/components/strategy/apply-to-portfolio.tsx \
|
||||
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: add strategy results to portfolio targets flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 신호 실행 모달에 현재 보유량 표시
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/app/signals/page.tsx`
|
||||
|
||||
**Step 1: 실행 모달 열 때 선택된 포트폴리오의 보유 종목 조회**
|
||||
|
||||
포트폴리오 선택 시 GET /api/portfolios/{id}/holdings 호출하여 해당 종목의 현재 보유 수량을 표시.
|
||||
|
||||
```tsx
|
||||
// 모달 내 신호 정보 섹션에 추가:
|
||||
// "현재 보유: {quantity}주 (평균단가: {avg_price}원)" 또는 "미보유"
|
||||
```
|
||||
|
||||
**Step 2: 매도 신호일 때 보유 수량 기반 기본값 설정**
|
||||
|
||||
매도/부분매도 신호의 경우, 수량 기본값을 현재 보유량(또는 보유량의 50%)으로 설정.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add frontend/src/app/signals/page.tsx
|
||||
git commit -m "feat: show current holdings in signal execute modal"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: DC형 위험자산 비율 경고
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/portfolio.py` (비율 계산 유틸)
|
||||
- Modify: `frontend/src/app/portfolio/[id]/page.tsx` (경고 배너)
|
||||
|
||||
**Step 1: 백엔드에 위험자산 비율 계산 추가**
|
||||
|
||||
포트폴리오 상세 응답에 `risk_asset_ratio` 필드 추가. ETF 중 채권형/MMF를 안전자산, 나머지를 위험자산으로 분류. pension 유형일 때만 계산.
|
||||
|
||||
**Step 2: 프론트엔드에 경고 배너 표시**
|
||||
|
||||
pension 유형 포트폴리오에서 위험자산 비율이 70%를 초과하면 상단에 경고 배너 표시.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "feat: add risk asset ratio warning for DC pension portfolios"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 포트폴리오 벤치마크 비교
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/api/snapshot.py` (returns 엔드포인트에 벤치마크 추가)
|
||||
- Modify: `backend/app/services/returns_calculator.py` (벤치마크 데이터 조회)
|
||||
- Modify: `frontend/src/app/portfolio/[id]/history/page.tsx` (벤치마크 차트)
|
||||
|
||||
**Step 1: returns API에 벤치마크 수익률 추가**
|
||||
|
||||
GET /api/portfolios/{id}/returns 응답에 벤치마크(KOSPI) 누적수익률을 함께 반환.
|
||||
|
||||
**Step 2: 프론트엔드 수익률 차트에 벤치마크 라인 추가**
|
||||
|
||||
기존 포트폴리오 수익률 라인 차트에 KOSPI 수익률 라인을 오버레이.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "feat: add benchmark comparison to portfolio returns"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 신호 성과 추적 (Signal PnL)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/models/signal.py` (executed_price, exit_price, pnl 필드 추가)
|
||||
- Create: Alembic migration
|
||||
- Modify: `backend/app/api/signal.py` (신호 성과 조회 엔드포인트)
|
||||
- Modify: `backend/app/schemas/signal.py` (성과 관련 필드)
|
||||
- Modify: `frontend/src/app/signals/page.tsx` (성과 컬럼 추가)
|
||||
|
||||
**Step 1: Signal 모델에 실행 관련 필드 추가**
|
||||
|
||||
```python
|
||||
# backend/app/models/signal.py에 추가
|
||||
executed_price = Column(Numeric(12, 2)) # 실제 체결가
|
||||
executed_quantity = Column(Integer) # 실행 수량
|
||||
executed_at = Column(DateTime) # 실행 시점
|
||||
```
|
||||
|
||||
**Step 2: 신호 실행 시 필드 업데이트**
|
||||
|
||||
POST /api/signal/{id}/execute에서 executed_price, executed_quantity, executed_at 저장.
|
||||
|
||||
**Step 3: 신호 응답에 성과 필드 포함**
|
||||
|
||||
SignalResponse에 executed_price, executed_quantity를 추가하여 프론트엔드에서 표시.
|
||||
|
||||
**Step 4: 프론트엔드 신호 테이블에 체결가/수량 컬럼 추가**
|
||||
|
||||
executed 상태 신호에 체결가, 수량 표시.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "feat: track signal execution details for PnL analysis"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Nice-to-have (별도 계획 필요)
|
||||
|
||||
아래 항목은 핵심 루프 완성 후 별도 계획으로 진행:
|
||||
|
||||
| 항목 | 설명 |
|
||||
|------|------|
|
||||
| 전략 결과 비교 UI | 멀티팩터/퀄리티/밸류모멘텀 결과를 나란히 비교 |
|
||||
| DC 투자가능 종목 필터 | ETF-only 필터, DC 적격 종목만 표시 |
|
||||
| Walk-forward 분석 | 백테스트 기간 분할 검증 |
|
||||
| 최소 거래 단위 | 리밸런싱 시 최소 거래 금액 고려 |
|
||||
| 신호 실행 취소 | 실행된 신호 되돌리기 (거래 삭제 포함) |
|
||||
| 실현/미실현 수익 분리 | 매도 시 실현 수익 계산 및 별도 추적 |
|
||||
| 포지션 사이징 가이드 | 신호 실행 시 추천 수량/최대 포지션 표시 |
|
||||
Loading…
x
Reference in New Issue
Block a user