docs/style(bat-052): QC 재검 비차단 minor 정리

- docs/operations.md: report 섹션을 WAC 실현PnL 정의로 갱신 (portfolio-hub WAC
  기반, 매수 fee cost basis 편입, 멀티데이 정확; 미가용 시 실현섹션 에러·타섹션 정상;
  당일 net 함수는 회귀 비교용 보존 명시)
- docs/runtime-flows.md: 운영 조회 리포트 흐름에 BAT-052 WAC 경로(get_realized_pnl
  N콜) + degrade(폴백 없음) 단계 추가
- docs/batches/bat-052_*/: CHG-20260626-02 기준으로 buy_fee_only_assets·
  buy_fee_by_asset 잔존 참조 제거 및 최종 구현 일치 갱신 (planning/design/
  implementation/verification)
- tests/application/test_operator_report.py:82: 섹션 헤더 주석에서
  buy_fee_by_asset 단어 제거 및 CHG-20260626-02 근거 추가 (주석만)
- trex_core/runtime/cli.py: __import__("datetime").timezone/timedelta inline
  패턴을 정식 from datetime import timezone, timedelta로 교체 (동작 동일)

pytest: 717 passed, 47 skipped (로직 변경 없음, 회귀 0)
This commit is contained in:
2026-06-26 20:35:17 +09:00
parent d3e4af59cf
commit a96fd54799
8 changed files with 31 additions and 26 deletions
@@ -8,12 +8,11 @@
= 오늘 KST date 문자열을 portfolio-hub에 전달한다.
`--from`/`--to` CLI 옵션은 이 batch에서 추가하지 않음(후속).
## OQ-3 — buy_fee_only_assets 수수료 출처
## OQ-3 — 매수 전용 자산 수수료 표시 (CHG-20260626-02로 결정 변경)
결정: **ledger_outbox 당일 매수 거래 fee 합산 (기존 TradeRow 경로 재사용)**.
`_cmd_report`에서 이미 DB에서 ledger_outbox 당일 rows를 읽는다.
buy side rows의 fee를 asset_id별로 합산해 `buy_fee_only_assets`를 채운다.
별도 DB 쿼리 없이 기존 쿼리 결과를 재활용한다.
초안 결정: ledger_outbox 당일 매수 fee 합산 → `buy_fee_only_assets` 표시.
**변경(CHG-20260626-02)**: portfolio-hub WAC는 매수 fee를 cost basis에 편입한다. 따라서 매수 전용일에 별도 수수료 라인을 출력하면 이중계상이 발생한다. `buy_fee_only_assets`·`buy_fee_by_asset` 헬퍼는 최종 구현에서 제거됐다. 매수 전용일에 실현 PnL = 0이고 별도 수수료 라인 없음. 수수료는 매도 시 cost basis를 통해 실현손익에 반영된다.
## OQ-4 — 조회 단위
@@ -59,7 +58,7 @@ class OperatorReport:
# 신규 WAC 필드
realized_pnl_wac: float = 0.0
realized_pnl_wac_by_asset: dict[str, float] = ...
buy_fee_only_assets: dict[str, float] = ...
# buy_fee_only_assets: 제거됨 (CHG-20260626-02 — cost basis 편입, 이중계상 방지)
data_completeness_warning: bool = False
wac_unavailable: bool = False # REQ-111 degrade flag
wac_unavailable_reason: str = ""
@@ -75,7 +74,7 @@ class OperatorReport:
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 합산 (매도 없는 자산)
e. (제거됨 CHG-20260626-02) buy_fee_only_assets — 매수 fee cost basis 편입으로 별도 라인 불필요
f. data_completeness_warning = any(result.data_completeness_warning)
4. portfolio-hub 미설정 or PortfolioHubUnavailableError:
→ rep.wac_unavailable = True, wac_unavailable_reason = ...
@@ -88,10 +87,10 @@ class OperatorReport:
=== WAC 실현 PnL {total:,.0f} === (또는 "portfolio-hub unavailable — realized PnL unavailable")
{symbol}: {pnl:,.0f}
[!] 이력 불완전 경고 (data_completeness_warning=True일 때만)
=== 매수 전용 자산 수수료 ===
{asset}: -{fee:,.0f}
```
> CHG-20260626-02: "매수 전용 자산 수수료" 섹션은 최종 구현에서 제거됐다. 매수 fee는 cost basis에 편입되므로 별도 라인 표시 시 이중계상이 발생한다.
## 계약 매핑 (portfolio-hub 응답 필드명)
| portfolio-hub 필드 | trex-core 필드 |
@@ -5,7 +5,7 @@
| 파일 | 변경 내용 |
| --- | --- |
| `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/application/operator_report.py` | OperatorReport에 WAC 3필드(realized_pnl_wac, realized_pnl_wac_by_asset, data_completeness_warning) + degrade 2필드(wac_unavailable, wac_unavailable_reason) 추가; apply_wac_to_report 헬퍼 추가; render_text/render_json WAC/degrade 섹션 추가. CHG-20260626-02: buy_fee_only_assets·buy_fee_by_asset 제거(cost basis 편입, 이중계상 방지) |
| `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) 통합 |
@@ -19,7 +19,7 @@
## 주요 설계 결정 이행
- 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-3(CHG-20260626-02): buy_fee_only_assets 제거. 매수 fee는 portfolio-hub cost basis에 편입되므로 별도 라인 불필요(이중계상 방지). NFR-2 엄격화: 동일날 매수+매도 시 WAC == 당일 net 성립.
- OQ-4: portfolio_id별 N콜 (holdings+pending union)
- NFR-2: realized_pnl(trades) 함수 및 기존 OperatorReport 필드 유지
@@ -31,7 +31,7 @@ portfolio-hub WAC 실현손익 API(/pnl/realized)를 trex-core 운영 리포트
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 경로 재사용).
3. CHG-20260626-02: 매수 fee가 portfolio-hub cost basis에 편입되므로 매수 전용일에 별도 수수료 라인을 출력하지 않는다. `buy_fee_only_assets`·`buy_fee_by_asset` 헬퍼는 최종 구현에서 제거됐다.
4. 기존 `realized_pnl(trades)` 함수 및 기존 OperatorReport 필드 삭제하지 않는다(NFR-2 회귀 보존).
5. _cmd_report에서 portfolio-hub base_url 미설정 시 REQ-111 degrade 경로로 진입한다.
@@ -45,7 +45,7 @@ python3 -m pytest --tb=short -q
| --- | --- |
| 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-3 (매수만→실현 0, 별도 수수료 라인 없음) | TestApplyWacToReport::test_buy_only_asset_no_fee_line (CHG-20260626-02: buy_fee_only_assets 제거, cost basis 편입) |
| 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 |