galaxis-po/docs/plans/2026-02-19-scenario-gap-analysis-plan.md
zephyrdark c97bb8595e 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>
2026-02-19 16:44:40 +09:00

15 KiB

시나리오 갭 분석 결과 및 구현 계획

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

# 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

# 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

# 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

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: 공통 컴포넌트 생성

// frontend/src/components/strategy/apply-to-portfolio.tsx
// 포트폴리오 선택 + 적용 버튼 + 확인 모달
// 3개 전략 페이지에서 공유

Step 3: 각 전략 페이지에 컴포넌트 배치

Step 4: 수동 테스트

전략 실행 → 결과에서 포트폴리오 선택 → 적용 → 포트폴리오 목표 확인

Step 5: Commit

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 호출하여 해당 종목의 현재 보유 수량을 표시.

// 모달 내 신호 정보 섹션에 추가:
// "현재 보유: {quantity}주 (평균단가: {avg_price}원)" 또는 "미보유"

Step 2: 매도 신호일 때 보유 수량 기반 기본값 설정

매도/부분매도 신호의 경우, 수량 기본값을 현재 보유량(또는 보유량의 50%)으로 설정.

Step 3: Commit

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

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

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 모델에 실행 관련 필드 추가

# 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

git commit -m "feat: track signal execution details for PnL analysis"

Phase 3: Nice-to-have (별도 계획 필요)

아래 항목은 핵심 루프 완성 후 별도 계획으로 진행:

항목 설명
전략 결과 비교 UI 멀티팩터/퀄리티/밸류모멘텀 결과를 나란히 비교
DC 투자가능 종목 필터 ETF-only 필터, DC 적격 종목만 표시
Walk-forward 분석 백테스트 기간 분할 검증
최소 거래 단위 리밸런싱 시 최소 거래 금액 고려
신호 실행 취소 실행된 신호 되돌리기 (거래 삭제 포함)
실현/미실현 수익 분리 매도 시 실현 수익 계산 및 별도 추적
포지션 사이징 가이드 신호 실행 시 추천 수량/최대 포지션 표시