# 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` 전환.