feat(bat-052): WAC 실현손익 리포트 통합 (REQ-109/110/111, dcy-035)
- portfolio.py: RealizedPnlItem/RealizedPnlResult 데이터클래스 추가;
PortfolioHubReadPort에 get_realized_pnl 포트 메서드 추가
- hub_read.py: GET /v1/portfolios/{id}/pnl/realized 어댑터 구현
(from/to/symbol query, 비2xx/네트워크→PortfolioHubUnavailableError)
- stub_read.py: get_realized_pnl 고정 결과 stub 추가 (무네트워크)
- operator_report.py: OperatorReport에 WAC 4필드+degrade 2필드 추가;
apply_wac_to_report/buy_fee_by_asset 헬퍼; render_text/render_json WAC 섹션
- cli.py: _cmd_report에 WAC 경로(portfolio-hub 설정 시) + degrade 경로 통합
(REQ-111: base_url 미설정/Unavailable → 식별가능 에러, 폴백 없음)
- tests: 51개 신규 테스트 (domain sweep, hub_read, stub_read, operator_report,
cli_report_wac); 전체 717 passed, 47 skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# BAT-052 Design
|
||||
|
||||
## OQ-2 — 기간(from_dt/to_dt) 기본값
|
||||
|
||||
결정: **오늘(KST 일 경계)**.
|
||||
`kst_day_bounds_utc()`가 반환하는 UTC [start, end) 범위에서
|
||||
`from_dt = start.date().isoformat()`, `to_dt = (end - timedelta(seconds=1)).date().isoformat()`
|
||||
= 오늘 KST date 문자열을 portfolio-hub에 전달한다.
|
||||
`--from`/`--to` CLI 옵션은 이 batch에서 추가하지 않음(후속).
|
||||
|
||||
## OQ-3 — buy_fee_only_assets 수수료 출처
|
||||
|
||||
결정: **ledger_outbox 당일 매수 거래 fee 합산 (기존 TradeRow 경로 재사용)**.
|
||||
`_cmd_report`에서 이미 DB에서 ledger_outbox 당일 rows를 읽는다.
|
||||
buy side rows의 fee를 asset_id별로 합산해 `buy_fee_only_assets`를 채운다.
|
||||
별도 DB 쿼리 없이 기존 쿼리 결과를 재활용한다.
|
||||
|
||||
## OQ-4 — 조회 단위
|
||||
|
||||
결정: **portfolio_id별 N콜 (슬롯↔portfolio 1:1)**.
|
||||
`_cmd_report`에서 DB에서 읽은 portfolio_id 목록(holdings + pending)을 union해
|
||||
각 portfolio_id별로 `get_realized_pnl(portfolio_id, from_dt, to_dt)` 호출.
|
||||
종목 필터 없이 전체 조회(symbol=None). 자산별 합산은 RealizedPnlResult.items에서 수집.
|
||||
|
||||
## 데이터 모델 (신규)
|
||||
|
||||
```python
|
||||
# trex_core/application/portfolio.py 추가
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RealizedPnlItem:
|
||||
symbol: str | None
|
||||
realized_pnl: float
|
||||
sell_qty: float
|
||||
# portfolio-hub 응답 보존 필드
|
||||
cost_basis_per_unit: float = 0.0
|
||||
fee_amount: float = 0.0
|
||||
tax_amount: float = 0.0
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RealizedPnlResult:
|
||||
items: list[RealizedPnlItem]
|
||||
cost_basis_from: str | None # ISO date string (portfolio-hub datetime -> date str)
|
||||
data_completeness_warning: bool
|
||||
total_realized_pnl: float
|
||||
```
|
||||
|
||||
## OperatorReport 확장
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class OperatorReport:
|
||||
# 기존 필드 유지
|
||||
holdings: list[Holding] = ...
|
||||
pending: list[PendingOrder] = ...
|
||||
fills_count: int = 0
|
||||
realized_pnl: float = 0.0 # 기존 당일 net (유지)
|
||||
realized_pnl_by_asset: dict = ... # 기존 (유지)
|
||||
# 신규 WAC 필드
|
||||
realized_pnl_wac: float = 0.0
|
||||
realized_pnl_wac_by_asset: dict[str, float] = ...
|
||||
buy_fee_only_assets: dict[str, float] = ...
|
||||
data_completeness_warning: bool = False
|
||||
wac_unavailable: bool = False # REQ-111 degrade flag
|
||||
wac_unavailable_reason: str = ""
|
||||
```
|
||||
|
||||
## _cmd_report 흐름
|
||||
|
||||
```
|
||||
1. DB에서 holdings, pending, trades 읽기 (기존 유지)
|
||||
2. build_report(holdings, pending, trades) -> rep (기존 당일 net, 기존 유지)
|
||||
3. portfolio-hub base_url 설정 있으면:
|
||||
a. from_dt, to_dt = 오늘 KST date
|
||||
b. 대상 portfolio_id = {h.portfolio_id for h in holdings} | {p.portfolio_id for p in pending}
|
||||
c. 각 pid별 get_realized_pnl(pid, from_dt, to_dt) 호출
|
||||
d. RealizedPnlResult 합산 → rep.realized_pnl_wac, realized_pnl_wac_by_asset
|
||||
e. buy_fee_only_assets = 기존 trades에서 buy side fee 합산 (매도 없는 자산)
|
||||
f. data_completeness_warning = any(result.data_completeness_warning)
|
||||
4. portfolio-hub 미설정 or PortfolioHubUnavailableError:
|
||||
→ rep.wac_unavailable = True, wac_unavailable_reason = ...
|
||||
5. render_text/render_json
|
||||
```
|
||||
|
||||
## render_text WAC 섹션 (신규)
|
||||
|
||||
```
|
||||
=== WAC 실현 PnL {total:,.0f} === (또는 "portfolio-hub unavailable — realized PnL unavailable")
|
||||
{symbol}: {pnl:,.0f}
|
||||
[!] 이력 불완전 경고 (data_completeness_warning=True일 때만)
|
||||
=== 매수 전용 자산 수수료 ===
|
||||
{asset}: -{fee:,.0f}
|
||||
```
|
||||
|
||||
## 계약 매핑 (portfolio-hub 응답 필드명)
|
||||
|
||||
| portfolio-hub 필드 | trex-core 필드 |
|
||||
| --- | --- |
|
||||
| total_realized_pnl | RealizedPnlResult.total_realized_pnl |
|
||||
| items[].realized_pnl | RealizedPnlItem.realized_pnl |
|
||||
| items[].quantity | RealizedPnlItem.sell_qty |
|
||||
| items[].symbol | RealizedPnlItem.symbol |
|
||||
| items[].cost_basis_per_unit | RealizedPnlItem.cost_basis_per_unit |
|
||||
| items[].fee_amount | RealizedPnlItem.fee_amount |
|
||||
| items[].tax_amount | RealizedPnlItem.tax_amount |
|
||||
| cost_basis_from | RealizedPnlResult.cost_basis_from (datetime → isoformat str) |
|
||||
| data_completeness_warning | RealizedPnlResult.data_completeness_warning (str|None → bool) |
|
||||
@@ -0,0 +1,30 @@
|
||||
# BAT-052 Implementation
|
||||
|
||||
## 변경 파일 요약
|
||||
|
||||
| 파일 | 변경 내용 |
|
||||
| --- | --- |
|
||||
| `trex_core/application/portfolio.py` | RealizedPnlItem, RealizedPnlResult 데이터클래스 추가; PortfolioHubReadPort에 get_realized_pnl 메서드 추가 |
|
||||
| `trex_core/application/operator_report.py` | OperatorReport에 WAC 4필드(realized_pnl_wac, realized_pnl_wac_by_asset, buy_fee_only_assets, data_completeness_warning) + degrade 2필드(wac_unavailable, wac_unavailable_reason) 추가; apply_wac_to_report, buy_fee_by_asset 헬퍼 추가; render_text/render_json WAC/degrade 섹션 추가 |
|
||||
| `trex_core/adapters/portfolio/hub_read.py` | get_realized_pnl 구현 추가 (GET /v1/portfolios/{id}/pnl/realized, from/to/symbol query params) |
|
||||
| `trex_core/adapters/portfolio/stub_read.py` | get_realized_pnl stub 추가 (고정 RealizedPnlResult, realized_pnl_result 교체 가능) |
|
||||
| `trex_core/runtime/cli.py` | _cmd_report에 WAC 경로(portfolio-hub 설정 시) + degrade 경로(미설정/Unavailable) 통합 |
|
||||
| `tests/application/test_operator_report.py` | TestApplyWacToReport, TestWacDegrade, TestNfr2Regression 클래스 추가 |
|
||||
| `tests/adapters/portfolio/test_hub_read.py` | get_realized_pnl HTTP 계약 테스트 7개 추가 |
|
||||
| `tests/adapters/portfolio/test_stub_read.py` | get_realized_pnl stub 테스트 3개 추가 |
|
||||
| `tests/runtime/test_cli_report_wac.py` | 신규 파일: CLI report WAC/degrade 통합 테스트 |
|
||||
| `docs/batches/bat-052_*/` | 배치 문서 (index, planning, design, implementation, verification) |
|
||||
| `docs/batches/index.md` | BAT-052 항목 추가 |
|
||||
|
||||
## 주요 설계 결정 이행
|
||||
|
||||
- OQ-2: from_dt/to_dt = 오늘 KST 날짜 (kst_day_bounds_utc().start.astimezone(KST).date())
|
||||
- OQ-3: buy_fee_only_assets = ledger_outbox 당일 매수 fee 합산, 매도 있는 자산 제외
|
||||
- OQ-4: portfolio_id별 N콜 (holdings+pending union)
|
||||
- NFR-2: realized_pnl(trades) 함수 및 기존 OperatorReport 필드 유지
|
||||
|
||||
## 유지된 기존 동작
|
||||
|
||||
- whoami, get_portfolios, get_cost_basis 동작 불변
|
||||
- realized_pnl(trades) 함수 유지
|
||||
- OperatorReport.realized_pnl, realized_pnl_by_asset 유지
|
||||
@@ -0,0 +1,19 @@
|
||||
# BAT-052: WAC 실현손익 리포트 통합 (portfolio-hub /pnl/realized)
|
||||
|
||||
- Status: in-progress
|
||||
- Profile: standard
|
||||
- Discovery: dcy-035
|
||||
- Created: 2026-06-26
|
||||
|
||||
## Included REQ
|
||||
|
||||
- REQ-109: portfolio-hub /pnl/realized read 포트+어댑터+stub
|
||||
- REQ-110: 운영리포트 WAC 실현손익 통합 (전 인스턴스 공통)
|
||||
- REQ-111: portfolio-hub 미가용 degrade (폴백 없음)
|
||||
|
||||
## Documents
|
||||
|
||||
- [planning.md](./planning.md)
|
||||
- [design.md](./design.md)
|
||||
- [implementation.md](./implementation.md)
|
||||
- [verification.md](./verification.md)
|
||||
@@ -0,0 +1,44 @@
|
||||
# BAT-052 Planning
|
||||
|
||||
## 목표
|
||||
|
||||
portfolio-hub WAC 실현손익 API(/pnl/realized)를 trex-core 운영 리포트에 통합한다.
|
||||
기존 당일 net 방식을 대체하되 기존 함수는 유지한다.
|
||||
|
||||
## 포함 REQ 요약
|
||||
|
||||
| REQ | 핵심 내용 |
|
||||
| --- | --- |
|
||||
| REQ-109 | get_realized_pnl 포트+어댑터+stub |
|
||||
| REQ-110 | OperatorReport에 WAC 필드 추가, _cmd_report에 WAC 경로 통합 |
|
||||
| REQ-111 | portfolio-hub 미가용 시 degrade (폴백 없음) |
|
||||
|
||||
## 변경 파일 목록 (예상)
|
||||
|
||||
| 파일 | 변경 종류 |
|
||||
| --- | --- |
|
||||
| `trex_core/application/portfolio.py` | RealizedPnlItem, RealizedPnlResult 데이터클래스 추가; PortfolioHubReadPort에 get_realized_pnl 추가 |
|
||||
| `trex_core/application/operator_report.py` | OperatorReport에 WAC 필드 추가; render_text/render_json 갱신; build_report_wac 추가 |
|
||||
| `trex_core/adapters/portfolio/hub_read.py` | get_realized_pnl 구현 추가 |
|
||||
| `trex_core/adapters/portfolio/stub_read.py` | get_realized_pnl stub 추가 |
|
||||
| `trex_core/runtime/cli.py` | _cmd_report에 WAC 경로+degrade 통합 |
|
||||
| `tests/application/test_operator_report.py` | WAC 필드 테스트, completeness 경고, degrade 테스트 추가 |
|
||||
| `tests/adapters/portfolio/test_hub_read.py` | get_realized_pnl HTTP 계약 테스트 추가 |
|
||||
| `tests/adapters/portfolio/test_stub_read.py` | get_realized_pnl stub 테스트 추가 |
|
||||
| `tests/runtime/test_cli_report.py` (신규) | WAC 경로·degrade·JSON 형태 통합 테스트 |
|
||||
|
||||
## 위험 / 고려사항
|
||||
|
||||
1. `PortfolioHubReadPort` Protocol 확장: 기존 구현체(StubPortfolioRead, PortfolioHubReadAdapter) 모두 갱신 필요.
|
||||
2. `data_completeness_warning`은 portfolio-hub에서 `str | None`으로 오지만 trex-core에서는 `bool`로 변환한다(truthy check).
|
||||
3. buy_fee_only_assets 수수료 출처는 ledger_outbox 당일 매수 fee 합산(기존 TradeRow 경로 재사용).
|
||||
4. 기존 `realized_pnl(trades)` 함수 및 기존 OperatorReport 필드 삭제하지 않는다(NFR-2 회귀 보존).
|
||||
5. _cmd_report에서 portfolio-hub base_url 미설정 시 REQ-111 degrade 경로로 진입한다.
|
||||
|
||||
## 의존성 확인
|
||||
|
||||
- httpx: 기존 어댑터 패턴과 동일
|
||||
- psycopg: 기존 _cmd_report에서 이미 사용
|
||||
- portfolio-hub 계약: `GET /v1/portfolios/{portfolio_id}/pnl/realized` query from_dt(alias=from), to_dt(alias=to), symbol(optional)
|
||||
- 응답: RealizedPnlResponse { total_realized_pnl, items, cost_basis_from(datetime|None), data_completeness_warning(str|None) }
|
||||
- 각 item: { symbol, executed_at, quantity, sell_price, cost_basis_per_unit, fee_amount, tax_amount, realized_pnl, order_id, execution_id }
|
||||
@@ -0,0 +1,62 @@
|
||||
# BAT-052 Verification
|
||||
|
||||
## 실행 명령
|
||||
|
||||
```
|
||||
python3 -m pytest --tb=short -q
|
||||
```
|
||||
|
||||
## 결과
|
||||
|
||||
```
|
||||
717 passed, 47 skipped in 2.34s
|
||||
```
|
||||
|
||||
- 통과: 717
|
||||
- 스킵: 47 (DB/네트워크 필요 통합 테스트 — 기존 패턴)
|
||||
- 실패: 0
|
||||
|
||||
## 신규 테스트 커버리지 (51개)
|
||||
|
||||
| 파일 | 테스트 수 | 대상 REQ |
|
||||
| --- | --- | --- |
|
||||
| test_domain_import_sweep.py | 1 | REQ-109 AC-5 |
|
||||
| test_operator_report.py (신규 클래스) | 22 | REQ-110 AC-1~6, REQ-111 AC-1~4, NFR-2 |
|
||||
| test_hub_read.py (신규) | 7 | REQ-109 AC-1/2/3/6 |
|
||||
| test_stub_read.py (신규) | 3 | REQ-109 AC-4 |
|
||||
| test_cli_report_wac.py (신규 파일) | 11 | REQ-110/111 통합 |
|
||||
|
||||
## REQ AC 충족 매핑
|
||||
|
||||
### REQ-109
|
||||
|
||||
| AC | 테스트 |
|
||||
| --- | --- |
|
||||
| AC-1 (200→RealizedPnlResult) | test_get_realized_pnl_200_parses_correctly |
|
||||
| AC-2 (symbol=포함) | test_get_realized_pnl_symbol_param_included, test_get_realized_pnl_no_symbol_param_absent |
|
||||
| AC-3 (네트워크/비2xx→Unavailable) | test_get_realized_pnl_network_error_raises_unavailable, test_get_realized_pnl_non_2xx_raises_unavailable |
|
||||
| AC-4 (stub 무네트워크) | test_stub_get_realized_pnl_returns_fixed_result_without_network, test_stub_realized_pnl_result_overridable |
|
||||
| AC-5 (domain import sweep) | test_domain_has_no_forbidden_imports |
|
||||
| AC-6 (data_completeness_warning) | test_get_realized_pnl_data_completeness_warning_true |
|
||||
|
||||
### REQ-110
|
||||
|
||||
| AC | 테스트 |
|
||||
| --- | --- |
|
||||
| AC-1 (전일매수+당일매도→WAC 정확값) | TestApplyWacToReport::test_wac_differs_from_net_when_prior_day_buy |
|
||||
| AC-2 (동일날→WAC≈net) | TestApplyWacToReport::test_wac_equals_net_when_same_day_buy_sell |
|
||||
| AC-3 (매수만→buy_fee_only_assets) | TestApplyWacToReport::test_buy_only_assets_has_fee, TestReportWacPath::test_buy_only_asset_fee_displayed |
|
||||
| AC-4 (completeness→text·json 경고) | TestApplyWacToReport::test_completeness_warning_in_text_and_json, TestReportWacPath::test_completeness_warning_in_text_and_json |
|
||||
| AC-5 (자산별 분해) | TestApplyWacToReport::test_per_asset_breakdown_in_text |
|
||||
| AC-6 (json 4필드) | TestApplyWacToReport::test_render_json_has_all_four_wac_fields, TestReportWacPath::test_wac_json_has_all_four_fields |
|
||||
| AC-7 (base_url 미설정→degrade) | TestReportDegradeNoBaseUrl::* |
|
||||
|
||||
### REQ-111
|
||||
|
||||
| AC | 테스트 |
|
||||
| --- | --- |
|
||||
| AC-1 (Unavailable→text 에러+타섹션 정상) | TestReportWacPath::test_unavailable_error_degrade |
|
||||
| AC-2 (json 에러+타섹션 정상) | TestReportWacPath::test_unavailable_json_null_wac_other_sections_present |
|
||||
| AC-3 (폴백 미발생) | TestReportWacPath::test_no_fallback_to_daily_net |
|
||||
| AC-4 (base_url 미설정 degrade) | TestReportDegradeNoBaseUrl::test_text_shows_unavailable, test_json_shows_null_wac_and_error |
|
||||
| AC-5 (예외 데몬 비전파) | TestReportIsolation::test_unavailable_does_not_raise |
|
||||
@@ -59,3 +59,4 @@
|
||||
| BAT-049 | standard | released | REQ-098, REQ-099, REQ-100, REQ-101 | dcy-033 | [bat-049](./bat-049_20260621_alarm-system-dcy-033/) |
|
||||
| BAT-050 | standard | release-candidate | REQ-102, REQ-103, REQ-104, REQ-107, REQ-108 | dcy-034 (core-A) | [bat-050](./bat-050_20260626_reconcile-warning-blocking-gate-core-a/) |
|
||||
| BAT-051 | standard | release-candidate | REQ-105, REQ-106 | dcy-034 (core-B) | [bat-051](./bat-051_20260626_sync-verify-cli-cold-start-core-b/) |
|
||||
| BAT-052 | standard | in-progress | REQ-109, REQ-110, REQ-111 | dcy-035 | [bat-052](./bat-052_20260626_wac-realized-pnl-report/) |
|
||||
|
||||
Reference in New Issue
Block a user