diff --git a/docs/plans/2026-02-19-scenario-gap-analysis-plan.md b/docs/plans/2026-02-19-scenario-gap-analysis-plan.md new file mode 100644 index 0000000..86c163b --- /dev/null +++ b/docs/plans/2026-02-19-scenario-gap-analysis-plan.md @@ -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 분석 | 백테스트 기간 분할 검증 | +| 최소 거래 단위 | 리밸런싱 시 최소 거래 금액 고려 | +| 신호 실행 취소 | 실행된 신호 되돌리기 (거래 삭제 포함) | +| 실현/미실현 수익 분리 | 매도 시 실현 수익 계산 및 별도 추적 | +| 포지션 사이징 가이드 | 신호 실행 시 추천 수량/최대 포지션 표시 |