- GET /v1/portfolios/{id}/trades and GET /v1/portfolios/{id}/pnl/realized endpoints
- Weighted Average Cost (WAC) realized PnL calculation in app/domain/pnl.py
- cost_basis and data_completeness_warning returned in pnl API response
- Test coverage: 50 passed (WAC 20 + trades 12 + pnl 18), full 427 passed regression ✓
- WAC validated against 199,300 unit e2e test cases
- DB migration: none required
- QC: qc-reviewer PASS-WITH-NOTES; team lead confirm-batch-verification → release-candidate
- Remaining non-blocking: symbol filter edge case testing, HTTP_422 deprecation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
4.7 KiB
4.7 KiB
BAT-017 Design
Design Gate 결정
- Design Gate: 통과 (simple, 기존 패턴 재사용, M1 fast-track)
- design.md는 계층 경계와 인터페이스 계약을 명시한다.
계층 경계
API 계층 (app/api/v1_portfolios.py)
└─ require_read 인증 + _get_portfolio_or_404 소유권 확인
└─ 날짜 파라미터 파싱/검증 (Query)
└─ Repository 위임 (list_trades_by_date / get_all_trades_for_pnl)
└─ Domain 위임 (calculate_realized_pnl)
Domain 계층 (app/domain/pnl.py)
└─ calculate_realized_pnl(rows: list[dict]) → WacPnlResult
└─ 순수 Python, psycopg/DB 무의존
└─ WAC 누적 계산 + data_completeness_warning 판단
Repository 계층 (app/repositories/postgres.py)
└─ list_trades_by_date(portfolio_id, date, from_, to_) → list[LedgerEntry]
└─ get_all_trades_for_pnl(portfolio_id, from_, to_, symbol) → list[LedgerEntry]
- from_/to_는 매도 집계 기간
- buy 이력은 집계 기간 이전 전체 조회(WAC 정확성)
Schema 계층 (app/schemas/v1.py)
└─ TradeHistoryItem, TradeHistoryResponse
└─ RealizedPnlItem, RealizedPnlResponse
- data_completeness_warning: str | None
- cost_basis_from: date | None
WAC 계산 함수 인터페이스
# app/domain/pnl.py
@dataclass
class PnlItem:
symbol: str | None
executed_at: datetime
quantity: float
sell_price: float
cost_basis_per_unit: float
fee_amount: float
tax_amount: float
realized_pnl: float
@dataclass
class WacPnlResult:
items: list[PnlItem]
total_realized_pnl: float
cost_basis_from: datetime | None
data_completeness_warning: str | None
def calculate_realized_pnl(rows: list[dict]) -> WacPnlResult:
"""
입력: portfolio_ledger 행 목록 (시간 순 정렬)
각 dict: side, quantity, price, fee_amount, tax_amount, executed_at, order_id, execution_id
출력: WacPnlResult
"""
HTTP API 계약
GET /v1/portfolios/{portfolio_id}/trades
- 인증:
require_read(read_token 이상) - 쿼리 파라미터:
date(str, optional): YYYY-MM-DD — 단일 날짜 (from=to로 처리)from(str, optional): YYYY-MM-DD — 범위 시작 (경계 포함)to(str, optional): YYYY-MM-DD — 범위 종료 (경계 포함)date와from/to중 하나라도 없으면 422
- 응답:
TradeHistoryResponse
GET /v1/portfolios/{portfolio_id}/pnl/realized
- 인증:
require_read(read_token 이상) - 쿼리 파라미터:
from(str, required): YYYY-MM-DDto(str, required): YYYY-MM-DDsymbol(str, optional): 종목 필터
- 응답:
RealizedPnlResponse
Repository 쿼리 설계
list_trades_by_date
SELECT ledger_entry_id, portfolio_id, order_id, execution_id, side,
quantity::float8, price::float8, fee_amount::float8, tax_amount::float8,
executed_at, recorded_at, cash_balance_after::float8, asset_quantity_after::float8,
total_value_after::float8, profit_buffer_after::float8, strategy_set_id, note
FROM portfolio_ledger
WHERE portfolio_id = %s
AND executed_at >= %s -- from_ (date boundary: 00:00:00 UTC)
AND executed_at < %s -- to_ + 1 day (exclusive upper bound)
ORDER BY executed_at ASC, ledger_entry_id ASC
get_all_trades_for_pnl
-- WAC 정확성을 위해 집계 기간 이전 buy 이력도 포함
-- symbol JOIN은 asset 테이블을 통해 수행하지 않고,
-- portfolio_ledger에 symbol 컬럼이 없으므로 portfolio의 symbol을 통해 필터
-- (portfolio 1개 = 1 종목이므로 symbol 파라미터는 portfolio 단위 필터)
SELECT ledger_entry_id, portfolio_id, order_id, execution_id, side,
quantity::float8, price::float8, fee_amount::float8, tax_amount::float8,
executed_at, recorded_at, cash_balance_after::float8, asset_quantity_after::float8,
total_value_after::float8, profit_buffer_after::float8, strategy_set_id, note
FROM portfolio_ledger
WHERE portfolio_id = %s
AND side IN ('buy', 'sell')
ORDER BY executed_at ASC, ledger_entry_id ASC
-- 매도 집계는 Python 계층(WAC 함수)에서 from_/to_ 기간 필터 적용
날짜 파라미터 파싱 규칙
date파라미터가 있으면 from_=date, to_=date (하루 범위)from+to파라미터가 있으면 그대로 사용date도 없고from/to도 없으면 HTTP 422 반환from만 있거나to만 있는 경우: 422 반환- 날짜 형식 파싱 실패 시: HTTP 422 반환 (YYYY-MM-DD 아닌 경우)
data_completeness_warning 트리거
REQ-041 AC-3/AC-4 기준:
- running_qty=0인 상태에서 sell 발생 (buy 이력 없음)
- sell 수량이 running_qty를 초과 (running_qty < sell_qty)
경고 시 계산은 중단하지 않고 최선 추정 계속 (WAC=0 또는 현재 WAC 유지).