feat(pnl): trade history + realized PnL API (WAC) — BAT-017, REQ-039~042 Implemented
- 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
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# 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 계산 함수 인터페이스
|
||||
|
||||
```python
|
||||
# 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-DD
|
||||
- `to` (str, required): YYYY-MM-DD
|
||||
- `symbol` (str, optional): 종목 필터
|
||||
- 응답: `RealizedPnlResponse`
|
||||
|
||||
## Repository 쿼리 설계
|
||||
|
||||
### list_trades_by_date
|
||||
```sql
|
||||
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
|
||||
```sql
|
||||
-- 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 기준:
|
||||
1. running_qty=0인 상태에서 sell 발생 (buy 이력 없음)
|
||||
2. sell 수량이 running_qty를 초과 (running_qty < sell_qty)
|
||||
|
||||
경고 시 계산은 중단하지 않고 최선 추정 계속 (WAC=0 또는 현재 WAC 유지).
|
||||
@@ -0,0 +1,60 @@
|
||||
# BAT-017 Implementation
|
||||
|
||||
## Status: Complete
|
||||
|
||||
## Changed Files
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `app/domain/pnl.py` | WAC realized PnL calculation (REQ-041) — pure Python, no DB dependency |
|
||||
| `tests/test_wac_calculation.py` | WAC unit tests — 20 tests, DB-free (REQ-042) |
|
||||
| `tests/test_v1_portfolios_trades.py` | GET /trades integration tests — 12 tests (REQ-042) |
|
||||
| `tests/test_v1_portfolios_pnl.py` | GET /pnl/realized integration tests — 18 tests (REQ-042) |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Changes |
|
||||
| --- | --- |
|
||||
| `app/schemas/v1.py` | Added `date` import; added `TradeHistoryItem`, `TradeHistoryResponse`, `RealizedPnlItem`, `RealizedPnlResponse` |
|
||||
| `app/api/v1_portfolios.py` | Added `date`, `datetime`, `timedelta`, `timezone` imports; added `calculate_realized_pnl` import; added schema imports; added `_parse_date_param()` helper; added `list_portfolio_trades()` endpoint (REQ-039); added `get_realized_pnl()` endpoint (REQ-040) |
|
||||
| `app/repositories/postgres.py` | Added `list_trades_by_date()` method (REQ-039); added `get_all_trades_for_pnl()` method (REQ-040/041) |
|
||||
| `app/repositories/portfolio_repository.py` | Added `list_trades_by_date()` and `get_all_trades_for_pnl()` abstract method signatures |
|
||||
| `docs/batches/index.md` | Added BAT-017 register row |
|
||||
|
||||
### Batch Documents
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `docs/batches/bat-017_.../index.md` | Batch index |
|
||||
| `docs/batches/bat-017_.../planning.md` | Planning |
|
||||
| `docs/batches/bat-017_.../design.md` | Design |
|
||||
| `docs/batches/bat-017_.../implementation.md` | This file |
|
||||
| `docs/batches/bat-017_.../verification.md` | Verification |
|
||||
|
||||
## Key Implementation Decisions
|
||||
|
||||
### WAC 계산 함수 위치
|
||||
- `app/domain/pnl.py` 신규 — domain 계층 (REQ-041 Notes: domain 계층 권장)
|
||||
- `app/services/portfolio_service.py` 수정 없음 — WAC 함수는 domain에 독립 배치
|
||||
|
||||
### `_parse_date_param` 날짜 형식 강제
|
||||
- Python 3.11+ `date.fromisoformat()`은 `YYYYMMDD`(대시 없음)도 수용함
|
||||
- REQ-039 AC-4 준수를 위해 `re.fullmatch(r"\d{4}-\d{2}-\d{2}", value)` 사전 검증 추가
|
||||
|
||||
### `get_all_trades_for_pnl` 쿼리 설계
|
||||
- WAC 정확성 확보를 위해 집계 기간(`from`/`to`) 제약 없이 portfolio의 모든 buy/sell 이력 조회
|
||||
- Python domain 함수에서 `from_dt`/`to_dt` 파라미터로 sell 집계 필터 적용
|
||||
|
||||
### `list_trades_by_date` 날짜 경계
|
||||
- `date` 단일: `[date 00:00 UTC, date+1day 00:00 UTC)` 반개구간
|
||||
- `to_` 포함 경계: `to_ + timedelta(days=1)` exclusive upper bound 적용
|
||||
|
||||
### data_completeness_warning 경고
|
||||
- running_qty=0에서 sell 발생 또는 sell qty > running_qty 시 경고 필드 설정
|
||||
- 계산은 중단하지 않고 최선 추정 계속 (REQ-041 AC-3/AC-4)
|
||||
|
||||
### 서비스/클라이언트 토큰 접근 제한
|
||||
- 두 엔드포인트 모두 `token_type in ("service", "client")` → 403
|
||||
- DB 토큰 또는 env 토큰(read 이상)만 접근 가능
|
||||
@@ -0,0 +1,42 @@
|
||||
# BAT-017: 일일 거래내역 + 실현 PnL 조회 API (dcy-007 M1 fast-track)
|
||||
|
||||
- Status: in-delivery
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-007
|
||||
- Included REQ: REQ-039, REQ-040, REQ-041, REQ-042
|
||||
- Excluded REQ: 없음
|
||||
|
||||
## Summary
|
||||
- dcy-007(일일 거래내역 + 실현 PnL 조회 API) 산출물 4건을 단일 batch로 묶는다.
|
||||
- GET /v1/portfolios/{id}/trades (날짜/범위 필터) 엔드포인트를 신설하고(REQ-039),
|
||||
GET /v1/portfolios/{id}/pnl/realized (WAC 기반 실현 PnL 집계) 엔드포인트를 신설한다(REQ-040).
|
||||
- WAC(weighted average cost) cost_basis 산출 로직을 service/domain 계층에 순수 Python으로 격리 구현해 단위 테스트가 DB 없이 가능하도록 한다(REQ-041).
|
||||
- AC 기댓값 수기 대조·날짜 필터·422·R-1 경고 필드 테스트 커버리지를 확보한다(REQ-042).
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- REQ-039 (Interface): GET /v1/portfolios/{id}/trades (date 단일/from+to 범위 필터, read_token 이상 인증, 소유권 403, 404, 날짜누락 422)
|
||||
- REQ-040 (Interface): GET /v1/portfolios/{id}/pnl/realized (from+to 필수, symbol 선택, WAC 기반 실현 PnL, data_completeness_warning, cost_basis_from)
|
||||
- REQ-041 (Integration): WAC cost_basis 산출 로직 — app/domain/pnl.py 신규(순수 Python, DB 무의존, R-1 완화 경고 로직 포함)
|
||||
- REQ-042 (Testing): tests/test_wac_calculation.py, tests/test_v1_portfolios_trades.py, tests/test_v1_portfolios_pnl.py 신규
|
||||
- app/repositories/postgres.py: list_trades_by_date, get_all_trades_for_pnl 메서드 추가
|
||||
- app/repositories/portfolio_repository.py: abstract method 추가
|
||||
- app/schemas/v1.py: TradeHistoryItem, TradeHistoryResponse, RealizedPnlItem, RealizedPnlResponse 스키마 추가
|
||||
- Out of Scope:
|
||||
- 미실현 PnL(현재가 연동) — dcy-007 OQ-2 Out of Scope
|
||||
- 전체 portfolio 집계 PnL 뷰 — dcy-007 OQ-2 Out of Scope
|
||||
- DB 마이그레이션 — 기존 portfolio_ledger 스키마로 조회 가능
|
||||
- 페이지네이션 — 이번 범위 외 이연
|
||||
- 성능 테스트(p95 500ms) — 통합/성능 테스트 환경에서 별도 검증
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- 6/22 적용 M1 fast-track — 단일 batch, 단일 구현 주기.
|
||||
- DB 마이그레이션 불필요(기존 portfolio_ledger 스키마).
|
||||
- 구현 순서: REQ-041(WAC 도메인 로직) → REQ-039/040(API 계층) → REQ-042(테스트).
|
||||
- WAC 기댓값 199,300 검증: buy 100주@10,000 + buy 100주@12,000 → sell 50주@15,000 fee=500 tax=200.
|
||||
@@ -0,0 +1,64 @@
|
||||
# BAT-017 Planning
|
||||
|
||||
## Included REQ
|
||||
|
||||
| REQ | Type | Summary |
|
||||
| --- | --- | --- |
|
||||
| REQ-039 | Interface | GET /v1/portfolios/{id}/trades — 날짜/범위 필터 체결 내역 |
|
||||
| REQ-040 | Interface | GET /v1/portfolios/{id}/pnl/realized — WAC 실현 PnL 집계 |
|
||||
| REQ-041 | Integration | WAC cost_basis 산출 로직 (service/domain 격리) |
|
||||
| REQ-042 | Testing | 테스트 커버리지 (WAC 기댓값 대조, 날짜 필터, 422, R-1 경고) |
|
||||
|
||||
## Delivery Plan
|
||||
|
||||
### 구현 순서
|
||||
1. **REQ-041 먼저** — WAC 도메인 로직(`app/domain/pnl.py`) 신규. 순수 Python, DB 무의존. REQ-040 API가 이에 의존하므로 선행 필수.
|
||||
2. **REQ-039** — GET /trades 엔드포인트 + repository 메서드(`list_trades_by_date`) + 스키마.
|
||||
3. **REQ-040** — GET /pnl/realized 엔드포인트 + repository 메서드(`get_all_trades_for_pnl`) + 스키마.
|
||||
4. **REQ-042** — 테스트 파일 3종 신규 작성.
|
||||
|
||||
### 파일 변경 목록
|
||||
- **신규 생성**:
|
||||
- `app/domain/pnl.py` — WAC 계산 함수 (REQ-041)
|
||||
- `tests/test_wac_calculation.py` — WAC 순수 단위 테스트 (REQ-042)
|
||||
- `tests/test_v1_portfolios_trades.py` — GET /trades 통합 테스트 (REQ-042)
|
||||
- `tests/test_v1_portfolios_pnl.py` — GET /pnl/realized 통합 테스트 (REQ-042)
|
||||
- **수정**:
|
||||
- `app/schemas/v1.py` — TradeHistoryItem, TradeHistoryResponse, RealizedPnlItem, RealizedPnlResponse 추가
|
||||
- `app/api/v1_portfolios.py` — GET /trades, GET /pnl/realized 엔드포인트 추가
|
||||
- `app/repositories/postgres.py` — list_trades_by_date, get_all_trades_for_pnl 메서드 추가
|
||||
- `app/repositories/portfolio_repository.py` — abstract method 추가
|
||||
- `docs/batches/index.md` — BAT-017 register 행 추가
|
||||
|
||||
## Dependencies
|
||||
|
||||
- 기존 portfolio_ledger 테이블(TimescaleDB hypertable) — 스키마 변경 없음
|
||||
- idx_portfolio_ledger_portfolio_executed 인덱스 — 날짜 범위 쿼리 활용
|
||||
- 기존 require_read 인증 의존성 — 변경 없음
|
||||
- REQ-041 WAC 로직은 REQ-040 엔드포인트의 service 계층 호출 대상
|
||||
|
||||
## Design Gate
|
||||
|
||||
**Design 필요: YES**
|
||||
|
||||
근거: 신규 엔드포인트 2개(HTTP 계약 확장), 신규 스키마 4종, 신규 domain 계층 모듈. 인터페이스 계약 및 계층 경계를 design.md에 명시해야 한다.
|
||||
|
||||
그러나 M1 fast-track 지시로 planning → design → implementation을 연속 수행한다. 설계는 simple하고 기존 코드 패턴을 그대로 따른다:
|
||||
- WAC 함수: 순수 Python 함수, 입/출력 타입 명확, DB 무의존
|
||||
- 두 엔드포인트: 기존 `_get_portfolio_or_404` + `require_read` 패턴 재사용
|
||||
- Repository: 기존 `list_recent_trades` 패턴 확장
|
||||
|
||||
## Milestones
|
||||
|
||||
1. app/domain/pnl.py + test_wac_calculation.py 구현 및 단위 테스트 통과
|
||||
2. GET /trades 엔드포인트 + 통합 테스트 구현
|
||||
3. GET /pnl/realized 엔드포인트 + 통합 테스트 구현
|
||||
4. 전체 pytest 실행 — 신규 3개 파일 + 기존 회귀 확인
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | 완화 |
|
||||
| --- | --- |
|
||||
| repository 메서드가 WAC 산출용 집계기간 이전 buy도 조회해야 함 | get_all_trades_for_pnl은 집계 기간과 무관하게 해당 portfolio+symbol의 전체 이력을 조회해 WAC 정확성 확보 |
|
||||
| TestClient 통합 테스트가 DB 의존 | MagicMock 기반 repo 주입 패턴(기존 test_v1_portfolios_ledger.py 참조)으로 DB 없이 실행 |
|
||||
| R-1 이력 불완전 경고 조건 | running_qty < sell_qty 또는 running_qty=0에서 sell 발생 시 data_completeness_warning 반환 (REQ-041 AC-3/AC-4) |
|
||||
@@ -0,0 +1,166 @@
|
||||
# BAT-017 Verification
|
||||
|
||||
## Status: approved (confirm-batch-verification)
|
||||
|
||||
## 실행 명령
|
||||
|
||||
```bash
|
||||
.venv/bin/python3 -m pytest tests/test_wac_calculation.py tests/test_v1_portfolios_trades.py tests/test_v1_portfolios_pnl.py -v
|
||||
```
|
||||
|
||||
## 실제 pytest 결과 (2026-06-18)
|
||||
|
||||
```
|
||||
================================================================================== test session starts ==================================================================================
|
||||
platform linux -- Python 3.12.3, pytest-8.4.2, pluggy-1.6.0
|
||||
rootdir: /home/genichin/workspace/trex-lead/repos/portfolio-hub
|
||||
collected 50 items
|
||||
|
||||
tests/test_wac_calculation.py::TestWac199300::test_total_realized_pnl PASSED
|
||||
tests/test_wac_calculation.py::TestWac199300::test_cost_basis_per_unit PASSED
|
||||
tests/test_wac_calculation.py::TestWac199300::test_no_warning_when_history_complete PASSED
|
||||
tests/test_wac_calculation.py::TestWac199300::test_cost_basis_from_is_first_buy PASSED
|
||||
tests/test_wac_calculation.py::TestWac199300::test_sell_item_fields PASSED
|
||||
tests/test_wac_calculation.py::TestBuyOnlyNoSell::test_empty_items PASSED
|
||||
tests/test_wac_calculation.py::TestBuyOnlyNoSell::test_cost_basis_from_set PASSED
|
||||
tests/test_wac_calculation.py::TestBuyOnlyNoSell::test_no_warning PASSED
|
||||
tests/test_wac_calculation.py::TestEmptyInput::test_empty_rows PASSED
|
||||
tests/test_wac_calculation.py::TestSellWithNoBuyHistory::test_warning_is_set PASSED
|
||||
tests/test_wac_calculation.py::TestSellWithNoBuyHistory::test_calculation_continues PASSED
|
||||
tests/test_wac_calculation.py::TestSellExceedsRunningQty::test_warning_when_oversell PASSED
|
||||
tests/test_wac_calculation.py::TestSellExceedsRunningQty::test_calculation_not_aborted PASSED
|
||||
tests/test_wac_calculation.py::TestMultipleBuySellCycles::test_wac_is_cumulative PASSED
|
||||
tests/test_wac_calculation.py::TestDateFilter::test_filter_includes_only_sells_in_range PASSED
|
||||
tests/test_wac_calculation.py::TestDateFilter::test_filter_both_sells_in_range PASSED
|
||||
tests/test_wac_calculation.py::TestDateFilter::test_wac_uses_all_buys_regardless_of_filter PASSED
|
||||
tests/test_wac_calculation.py::TestSymbolFilter::test_symbol_filter_excludes_other_symbols PASSED
|
||||
tests/test_wac_calculation.py::TestSymbolFilter::test_no_symbol_filter_includes_all PASSED
|
||||
tests/test_wac_calculation.py::TestNoPsycopgInDomainModule::test_pnl_module_has_no_db_import PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_single_date_returns_matching_count PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_single_date_item_fields PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_range_filter_returns_items PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_no_date_params_returns_422 PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_bad_date_format_returns_422 PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_slash_date_format_returns_422 PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_empty_range_returns_200_empty PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_non_owner_returns_403 PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_nonexistent_portfolio_returns_404 PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_endpoint_in_openapi PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_from_only_without_to_returns_422 PASSED
|
||||
tests/test_v1_portfolios_trades.py::test_trades_to_only_without_from_returns_422 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::TestWac199300EndToEnd::test_total_realized_pnl_is_199300 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::TestWac199300EndToEnd::test_cost_basis_per_unit_is_11000 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::TestWac199300EndToEnd::test_items_contain_required_fields PASSED
|
||||
tests/test_v1_portfolios_pnl.py::TestWac199300EndToEnd::test_data_completeness_warning_is_null_when_complete PASSED
|
||||
tests/test_v1_portfolios_pnl.py::TestWac199300EndToEnd::test_cost_basis_from_is_first_buy_date PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_missing_from_returns_422 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_missing_to_returns_422 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_both_missing_returns_422 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_no_sells_returns_200_with_zero PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_empty_ledger_returns_200_with_zero PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_sell_without_buy_triggers_warning PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_complete_history_warning_is_null PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_oversell_triggers_warning PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_symbol_filter_passed_to_repo PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_symbol_filter_excludes_other_symbol_sells PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_non_owner_returns_403 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_nonexistent_portfolio_returns_404 PASSED
|
||||
tests/test_v1_portfolios_pnl.py::test_pnl_endpoint_in_openapi PASSED
|
||||
|
||||
50 passed, 8 warnings in 3.43s
|
||||
```
|
||||
|
||||
**결과: 50 passed, 0 failed**
|
||||
|
||||
## 기존 회귀 테스트 결과
|
||||
|
||||
```bash
|
||||
.venv/bin/python3 -m pytest \
|
||||
--ignore=tests/test_wac_calculation.py \
|
||||
--ignore=tests/test_v1_portfolios_trades.py \
|
||||
--ignore=tests/test_v1_portfolios_pnl.py
|
||||
```
|
||||
|
||||
```
|
||||
377 passed, 3 warnings in 17.25s
|
||||
```
|
||||
|
||||
**결과: 377 passed, 0 failed — 기존 테스트 회귀 없음**
|
||||
|
||||
## 환경 제약 사항
|
||||
|
||||
- DB 통합 테스트 환경(`pfhub_DB_ENABLED=true`, TimescaleDB 컨테이너) 없이 실행.
|
||||
- 통합 테스트(`test_v1_portfolios_trades.py`, `test_v1_portfolios_pnl.py`)는 TestClient + MagicMock repo 주입 방식으로 DB 없이 실행 가능.
|
||||
- WAC 단위 테스트(`test_wac_calculation.py`)는 순수 Python — DB 불필요 확인.
|
||||
- 성능 테스트(AC-9: p95 500ms) 및 DB 인덱스 활용 검증은 통합/성능 테스트 환경에서 별도 수행 필요.
|
||||
|
||||
## REQ별 AC 충족 매핑
|
||||
|
||||
### REQ-039: GET /v1/portfolios/{id}/trades
|
||||
|
||||
| AC | 테스트 | 결과 |
|
||||
| --- | --- | --- |
|
||||
| AC-1: date 필터 + 200 + 항목 필드 확인 | `test_trades_single_date_returns_matching_count`, `test_trades_single_date_item_fields` | PASS |
|
||||
| AC-2: from/to 범위 필터 + 200 + 경계 포함 | `test_trades_range_filter_returns_items` | PASS |
|
||||
| AC-3: 날짜 파라미터 누락 → 422 | `test_trades_no_date_params_returns_422` | PASS |
|
||||
| AC-4: 날짜 형식 오류 → 422 | `test_trades_bad_date_format_returns_422`, `test_trades_slash_date_format_returns_422` | PASS |
|
||||
| AC-5: 0건 → 200 + empty | `test_trades_empty_range_returns_200_empty` | PASS |
|
||||
| AC-6: 비소유자 → 403 | `test_trades_non_owner_returns_403` | PASS |
|
||||
| AC-7: 존재하지 않는 portfolio → 404 | `test_trades_nonexistent_portfolio_returns_404` | PASS |
|
||||
| AC-8: OpenAPI 문서화 | `test_trades_endpoint_in_openapi` | PASS |
|
||||
| AC-9: p95 500ms | 성능 환경 별도 (환경 제약) | 환경 제약 |
|
||||
|
||||
### REQ-040: GET /v1/portfolios/{id}/pnl/realized
|
||||
|
||||
| AC | 테스트 | 결과 |
|
||||
| --- | --- | --- |
|
||||
| AC-1: 199,300 WAC + cost_basis_per_unit=11,000 | `TestWac199300EndToEnd::test_total_realized_pnl_is_199300`, `test_cost_basis_per_unit_is_11000` | PASS |
|
||||
| AC-2: symbol 필터 | `test_pnl_symbol_filter_passed_to_repo`, `test_pnl_symbol_filter_excludes_other_symbol_sells` | PASS |
|
||||
| AC-3: from/to 누락 → 422 | `test_pnl_missing_from_returns_422`, `test_pnl_missing_to_returns_422`, `test_pnl_both_missing_returns_422` | PASS |
|
||||
| AC-4: 매도 0건 → 200 + total=0 + items=[] | `test_pnl_no_sells_returns_200_with_zero`, `test_pnl_empty_ledger_returns_200_with_zero` | PASS |
|
||||
| AC-5: 이력 불완전 → data_completeness_warning 비어 있지 않음 | `test_pnl_sell_without_buy_triggers_warning`, `test_pnl_oversell_triggers_warning` | PASS |
|
||||
| AC-6: 이력 완전 → data_completeness_warning null | `test_pnl_complete_history_warning_is_null`, `TestWac199300EndToEnd::test_data_completeness_warning_is_null_when_complete` | PASS |
|
||||
| AC-7: 비소유자 → 403 | `test_pnl_non_owner_returns_403` | PASS |
|
||||
| AC-8: 존재하지 않는 portfolio → 404 | `test_pnl_nonexistent_portfolio_returns_404` | PASS |
|
||||
| AC-9: OpenAPI 문서화 (data_completeness_warning 포함) | `test_pnl_endpoint_in_openapi` | PASS |
|
||||
| AC-10: p95 500ms | 성능 환경 별도 (환경 제약) | 환경 제약 |
|
||||
|
||||
### REQ-041: WAC cost_basis 산출 로직
|
||||
|
||||
| AC | 테스트 | 결과 |
|
||||
| --- | --- | --- |
|
||||
| AC-1: WAC=11,000 + realized_pnl=199,300 | `TestWac199300::test_total_realized_pnl`, `test_cost_basis_per_unit` | PASS |
|
||||
| AC-2: buy만 있고 sell 없음 → items=[] | `TestBuyOnlyNoSell::test_empty_items` | PASS |
|
||||
| AC-3: sell 시작(buy 없음) → warning 비어 있지 않음 | `TestSellWithNoBuyHistory::test_warning_is_set`, `test_calculation_continues` | PASS |
|
||||
| AC-4: sell qty > running_qty → warning | `TestSellExceedsRunningQty::test_warning_when_oversell` | PASS |
|
||||
| AC-5: 반복 buy/sell → WAC 누적 정확성 | `TestMultipleBuySellCycles::test_wac_is_cumulative` | PASS |
|
||||
| AC-6: 빈 입력 → items=[], total=0, cost_basis_from=None | `TestEmptyInput::test_empty_rows` | PASS |
|
||||
| AC-7: DB import 없음 (grep) | `TestNoPsycopgInDomainModule::test_pnl_module_has_no_db_import` | PASS |
|
||||
| AC-8: DB 없이 실행 | 전체 WAC 단위 테스트 DB 미사용 — PASS | PASS |
|
||||
|
||||
### REQ-042: 테스트 커버리지
|
||||
|
||||
| AC | 결과 |
|
||||
| --- | --- |
|
||||
| AC-1: test_wac_calculation.py — WAC 시나리오 포함 199,300 | 20 tests PASS |
|
||||
| AC-2: test_v1_portfolios_trades.py — date/from-to/422/403/404 | 12 tests PASS |
|
||||
| AC-3: test_v1_portfolios_pnl.py — 199,300/422/0건/warning | 18 tests PASS |
|
||||
| AC-4: 전체 50 tests PASSED | PASS |
|
||||
| AC-5: WAC 단위 테스트 DB 없이 통과 | PASS |
|
||||
| AC-6: 기존 377 tests 회귀 없음 | PASS |
|
||||
|
||||
## 남은 한계 (환경 제약 및 OoS)
|
||||
|
||||
1. **성능 테스트(AC-9/AC-10)**: p95 500ms — TimescaleDB 인덱스 환경에서 별도 실행 필요. 이번 batch 검증 범위 외.
|
||||
2. **DB 직접 통합 테스트**: `pfhub_DB_ENABLED=true` + Docker 인프라 없이 실행. MagicMock 기반 단위/통합 테스트로 행동 검증 완료.
|
||||
3. **symbol 컬럼 부재**: `portfolio_ledger`에 symbol 컬럼 없음. 현재 portfolio 단위(1 portfolio = 1 symbol) 가정. 멀티 symbol portfolio 지원 시 별도 설계 필요.
|
||||
4. **DeprecationWarning**: `HTTP_422_UNPROCESSABLE_ENTITY` → `HTTP_422_UNPROCESSABLE_CONTENT` (FastAPI 최신 버전 경고). 기존 코드 패턴 유지(breaking change 없음). 별도 cleanup batch에서 처리 권장.
|
||||
|
||||
## Approval
|
||||
|
||||
- Status: **approved** (confirm-batch-verification) → batch `release-candidate`
|
||||
- Approver: 팀장 (Claude), 2026-06-18
|
||||
- Independent QC: `qc-reviewer` verdict **PASS-WITH-NOTES**. 50 passed 독립 재현 + 전체 427 passed 회귀 없음, WAC=11,000/realized=199,300 단위+e2e 진짜 검증, R-1 경고필드(buy없음·oversell·완전이력 null) 정상, NFR-3 격리(domain DB import 0) 확인, 스코프·DB 마이그레이션 무, 위생 이상 없음.
|
||||
- 수용한 잔여(비차단 follow-up): (1) symbol 필터 `symbol_param != portfolio_symbol` edge 테스트 공백 — AC-2는 충족이나 완전 커버 아님. (2) `HTTP_422_UNPROCESSABLE_ENTITY` deprecated(기존 패턴). 둘 다 별도 cleanup batch 또는 후속 처리.
|
||||
- 성능 AC-9/AC-10(p95 500ms)은 TimescaleDB 환경 필요 — 환경 제약 미검증(코드결함 아님).
|
||||
- REQ-039~042: confirm-req-implemented로 `Implemented` 전환.
|
||||
@@ -24,3 +24,4 @@
|
||||
| BAT-014 | batch-lite | release-candidate | REQ-025, REQ-026, REQ-027, REQ-028, REQ-032 | dcy-003 | [bat-014](./bat-014_20260524_portfolioitem-symbol-response-contract/) |
|
||||
| BAT-015 | standard | release-candidate | REQ-033, REQ-034, REQ-035 | dcy-005 | [bat-015](./bat-015_20260601_rel-001-feedback-cleanup/) |
|
||||
| BAT-016 | standard | release-candidate | REQ-036, REQ-037, REQ-038 | dcy-006 | [bat-016](./bat-016_20260605_ledger-accounting-consolidation/) |
|
||||
| BAT-017 | standard | release-candidate | REQ-039, REQ-040, REQ-041, REQ-042 | dcy-007 | [bat-017](./bat-017_20260618_trade-history-and-pnl-api/) |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# REQ-041: WAC 기반 cost_basis 산출 및 실현 PnL 계산 로직 (service/domain 계층 격리)
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Integration
|
||||
- Priority: High
|
||||
- Owner: Justin Ko
|
||||
@@ -87,3 +87,10 @@
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req: `Proposed` → `Approved`. WAC 공식 확정(FIFO 제외), NFR-3 격리 AC-7/AC-8·R-1 완화 AC-3/AC-4 확인, blocker 없음. 구현 위치(domain vs service)는 구현 단계 결정. M1 fast-track.
|
||||
|
||||
### CHG-20260618-03
|
||||
- Date: 2026-06-18
|
||||
- Author: 팀장 (Claude)
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req-implemented: `Approved` → `Implemented`. Evidence: app/domain/pnl.py 구현, WAC 누적 정확성+199,300 검증, NFR-3 격리(psycopg import 0) QC 확인, R-1 경고 동작, test_wac_calculation.py 20 passed.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# REQ-039: GET /v1/portfolios/{id}/trades 체결 내역 조회 엔드포인트 신설
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Interface
|
||||
- Priority: High
|
||||
- Owner: Justin Ko
|
||||
@@ -87,3 +87,10 @@
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req: `Proposed` → `Approved`. dcy-007 FR-1/FR-4 정합, AC 구체적·test 가능, blocker 없음. `date` vs `from`/`to` 상호작용은 구현 단계 결정(AC-3 422 확정). M1 fast-track — REQ-040~042와 동일 batch.
|
||||
|
||||
### CHG-20260618-03
|
||||
- Date: 2026-06-18
|
||||
- Author: 팀장 (Claude)
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req-implemented: `Approved` → `Implemented`. Evidence: BAT-017 release-candidate, 독립 QC PASS-WITH-NOTES, test_v1_portfolios_trades.py 12 passed(AC-1~8), 성능 AC-9는 환경제약 미검증. metrics/DB 마이그레이션 무.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# REQ-040: GET /v1/portfolios/{id}/pnl/realized 실현 PnL 집계 엔드포인트 신설
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Interface
|
||||
- Priority: High
|
||||
- Owner: Justin Ko
|
||||
@@ -92,3 +92,10 @@
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req: `Proposed` → `Approved`. WAC 수치 AC-1(199,300)·R-1 경고 필드 AC-5/AC-6 확인, OQ-2 OoS 반영, blocker 없음. REQ-041 의존(동일 batch). M1 fast-track.
|
||||
|
||||
### CHG-20260618-03
|
||||
- Date: 2026-06-18
|
||||
- Author: 팀장 (Claude)
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req-implemented: `Approved` → `Implemented`. Evidence: BAT-017 release-candidate, 독립 QC PASS-WITH-NOTES, WAC 199,300 단위+e2e 검증, R-1 경고필드 정상, AC-1~9 PASS(AC-10 환경제약). 잔여(비차단): symbol 필터 edge 테스트 공백.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# REQ-042: 체결 내역/실현 PnL API 테스트 커버리지 (WAC 시나리오 기댓값 대조, 날짜 필터, 422)
|
||||
|
||||
- Status: Approved
|
||||
- Status: Implemented
|
||||
- Type: Testing
|
||||
- Priority: High
|
||||
- Owner: Justin Ko
|
||||
@@ -98,3 +98,10 @@
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req: `Proposed` → `Approved`. WAC 시나리오 기댓값·날짜필터·422·R-1 경고 검증 AC가 REQ-039~041과 정합, blocker 없음. 구현 REQ와 동일 batch. M1 fast-track.
|
||||
|
||||
### CHG-20260618-03
|
||||
- Date: 2026-06-18
|
||||
- Author: 팀장 (Claude)
|
||||
|
||||
#### Change Summary
|
||||
- confirm-req-implemented: `Approved` → `Implemented`. Evidence: 3개 테스트 파일 50 passed(20+12+18) 독립 QC 재현, 전체 427 passed 회귀 없음. AC-1~6 충족.
|
||||
|
||||
+5
-4
@@ -119,13 +119,14 @@
|
||||
| REQ-036 | portfolio_ledger 회계 원장 통합 마이그레이션 (ledger_trades 흡수/DROP) | Migration | Implemented | High | Justin Ko | [req-036](./Migration/req-036_portfolio-ledger-consolidation-migration.md) |
|
||||
| REQ-037 | 신 path 회계 책임 단일화 + 응답에 portfolio 상태 포함 | Interface | Implemented | High | Justin Ko | [req-037](./Interface/req-037_ledger-trade-write-accounting-and-state-response.md) |
|
||||
| REQ-038 | 레거시 buy/sell endpoint deprecated 표기 | Interface | Implemented | Low | Justin Ko | [req-038](./Interface/req-038_legacy-buy-sell-endpoint-deprecation.md) |
|
||||
| REQ-039 | GET /v1/portfolios/{id}/trades 체결 내역 조회 엔드포인트 신설 | Interface | Approved | High | Justin Ko | [req-039](./Interface/req-039_trade-history-query-endpoint.md) |
|
||||
| REQ-040 | GET /v1/portfolios/{id}/pnl/realized 실현 PnL 집계 엔드포인트 신설 | Interface | Approved | High | Justin Ko | [req-040](./Interface/req-040_realized-pnl-query-endpoint.md) |
|
||||
| REQ-041 | WAC 기반 cost_basis 산출 및 실현 PnL 계산 로직 (service/domain 계층 격리) | Integration | Approved | High | Justin Ko | [req-041](./Integration/req-041_wac-cost-basis-pnl-calculation.md) |
|
||||
| REQ-042 | 체결 내역/실현 PnL API 테스트 커버리지 (WAC 시나리오 기댓값 대조, 날짜 필터, 422) | Testing | Approved | High | Justin Ko | [req-042](./Testing/req-042_trade-pnl-api-test-coverage.md) |
|
||||
| REQ-039 | GET /v1/portfolios/{id}/trades 체결 내역 조회 엔드포인트 신설 | Interface | Implemented | High | Justin Ko | [req-039](./Interface/req-039_trade-history-query-endpoint.md) |
|
||||
| REQ-040 | GET /v1/portfolios/{id}/pnl/realized 실현 PnL 집계 엔드포인트 신설 | Interface | Implemented | High | Justin Ko | [req-040](./Interface/req-040_realized-pnl-query-endpoint.md) |
|
||||
| REQ-041 | WAC 기반 cost_basis 산출 및 실현 PnL 계산 로직 (service/domain 계층 격리) | Integration | Implemented | High | Justin Ko | [req-041](./Integration/req-041_wac-cost-basis-pnl-calculation.md) |
|
||||
| REQ-042 | 체결 내역/실현 PnL API 테스트 커버리지 (WAC 시나리오 기댓값 대조, 날짜 필터, 422) | Testing | Implemented | High | Justin Ko | [req-042](./Testing/req-042_trade-pnl-api-test-coverage.md) |
|
||||
|
||||
## Recent Change Log Summary
|
||||
|
||||
- 2026-06-18: REQ-039/040/041/042 `Approved` → `Implemented` (BAT-017 release-candidate, 독립 QC PASS-WITH-NOTES, 50 passed/전체 427 회귀없음, WAC 199,300 검증). 잔여 비차단: symbol 필터 edge 테스트·HTTP_422 deprecated.
|
||||
- 2026-06-18: REQ-039/040/041/042 `Proposed` → `Approved` (confirm-req 일괄 승인, dcy-007 결정 정합·R-1 완화 반영·blocker 없음, M1 fast-track 동일 batch).
|
||||
- 2026-06-18: REQ-039/040/041/042 신규 (Proposed) — dcy-007(일일 거래내역 + 실현 PnL 조회 API) draft-req 산출물. REQ-039 체결 내역 조회(Interface), REQ-040 실현 PnL 집계(Interface), REQ-041 WAC cost_basis 산출 로직(Integration), REQ-042 테스트 커버리지(Testing). OQ-1=WAC(사장님 결정), OQ-2=Out of Scope. R-1 경고 필드(data_completeness_warning + cost_basis_from)가 REQ-040 AC-5/AC-6 및 REQ-042 AC-1/AC-3에 반영.
|
||||
- 2026-06-05: REQ-036/037/038 Approved → Implemented (CHG-20260605-04/04/03) — BAT-016 confirm-batch-verification 완료. 전체 376 passed(실 DB 통합 3건 포함). verify 단계에서 hypertable ON CONFLICT 불가 + 공유 connection close 결함 2건 발견·수정, REQ-036/037 CHG-20260605-03으로 idempotency를 DB partial unique → application-level(SELECT-before-insert under FOR UPDATE)로 재정의. BAT-016 release-candidate. REL-003 staging 이연: alembic fresh-DB 적용 + 정밀도/action 분포.
|
||||
|
||||
Reference in New Issue
Block a user