diff --git a/docs/batches/bat-052_20260626_wac-realized-pnl-report/design.md b/docs/batches/bat-052_20260626_wac-realized-pnl-report/design.md index 5d086e9..725a282 100644 --- a/docs/batches/bat-052_20260626_wac-realized-pnl-report/design.md +++ b/docs/batches/bat-052_20260626_wac-realized-pnl-report/design.md @@ -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 필드 | diff --git a/docs/batches/bat-052_20260626_wac-realized-pnl-report/implementation.md b/docs/batches/bat-052_20260626_wac-realized-pnl-report/implementation.md index 139e41b..867d179 100644 --- a/docs/batches/bat-052_20260626_wac-realized-pnl-report/implementation.md +++ b/docs/batches/bat-052_20260626_wac-realized-pnl-report/implementation.md @@ -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 필드 유지 diff --git a/docs/batches/bat-052_20260626_wac-realized-pnl-report/planning.md b/docs/batches/bat-052_20260626_wac-realized-pnl-report/planning.md index fba5697..c0c8263 100644 --- a/docs/batches/bat-052_20260626_wac-realized-pnl-report/planning.md +++ b/docs/batches/bat-052_20260626_wac-realized-pnl-report/planning.md @@ -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 경로로 진입한다. diff --git a/docs/batches/bat-052_20260626_wac-realized-pnl-report/verification.md b/docs/batches/bat-052_20260626_wac-realized-pnl-report/verification.md index 2b4235a..5310202 100644 --- a/docs/batches/bat-052_20260626_wac-realized-pnl-report/verification.md +++ b/docs/batches/bat-052_20260626_wac-realized-pnl-report/verification.md @@ -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 | diff --git a/docs/operations.md b/docs/operations.md index f8fbb5b..6497ec4 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -61,14 +61,19 @@ pkill -TERM -f trex-core-daemon - enter/exit 안전장치: 확인 프롬프트 기본 ON, `--yes`(paper 생략), live(`broker=kis`)는 `--yes --live` 이중 ack, `--dry-run` 미리보기. `--json`은 비대화형이라 enter/exit에 `--yes` 필수. - 인스턴스 라우팅: `--config ` 또는 `--instance `, 미지정 시 `default`. -### `report` — 일일 운영 리포트 (REQ-088 / dcy-029) +### `report` — 일일 운영 리포트 (REQ-088 / dcy-029, REQ-110/111 / dcy-035) ```bash .venv/bin/trex-core report # 사람용 표 .venv/bin/trex-core report --json # 자동화용 JSON ``` -출력: **현재 보유**(symbol/qty/avg_price), **오늘(KST) 체결 건수**, **오늘 실현 PnL**(= `ledger_outbox` net 매매 현금흐름 = Σ매도(대금−수수료−세금) − Σ매수(대금+수수료), 수수료·세금 반영 BAT-038 정책), **대기 주문**. -- "오늘" 경계는 trading_date(KST 00:00~익일 00:00). 전일 체결은 제외. -- 일중 진입·청산 전략에서는 net ≈ 실현손익. 미청산 보유의 평가손익은 포함하지 않는다(실현분만). +출력: **현재 보유**(symbol/qty/avg_price), **오늘(KST) 체결 건수**, **WAC 실현 PnL**, **대기 주문**. + +**WAC 실현 PnL (REQ-110/111, BAT-052)**: portfolio-hub WAC 기반 실현손익(`/pnl/realized`). 매수 fee가 cost basis에 편입돼 멀티데이에도 정확한 취득원가 기준 손익을 반영한다. 매수 전용일에 별도 fee 라인은 없다(매수 fee는 cost basis 편입 → 매도 시 실현). +- **portfolio-hub 가용 시**: WAC 실현 PnL 섹션에 자산별 분해 + 이력 불완전 경고(data_completeness_warning) 출력. +- **portfolio-hub 미가용 시**: WAC 실현 PnL 섹션에 에러 메시지 출력. 보유/대기/체결 건수 등 나머지 섹션은 정상 출력. 폴백 없음(당일 net을 WAC 위치에 대체 출력하지 않는다). +- "오늘" 경계는 trading_date(KST 00:00~익일 00:00). 전일 체결은 제외. 미청산 보유의 평가손익은 포함하지 않는다(실현분만). + +> 참고: 당일 net 현금흐름(`realized_pnl(trades)` 함수)은 회귀 비교용으로 코드에 보존돼 있으나 report 출력의 기본 경로가 아니다. WAC 경로는 `TREX_PORTFOLIO_HUB_BASE_URL` 설정 시 자동 활성화된다. --- diff --git a/docs/runtime-flows.md b/docs/runtime-flows.md index f5122b9..271716f 100644 --- a/docs/runtime-flows.md +++ b/docs/runtime-flows.md @@ -304,13 +304,15 @@ timeout 미지정·취소 실패로 잔여 고착된 pending 슬롯을 처분: - **거부 ERROR 가시성(REQ-087)**: dispatch/poll 거부 롤백 시 `event=order_reject`를 **ERROR 레벨**로 — 조용한 롤백 제거(issue #28 이 수 시간 묻혔던 원인). 정상 체결은 INFO. 알림 전송 채널은 Deferred(OQ-3). - **청산 reason+actor(REQ-086)**: `safety-sell fired: ... reason= actor=trex-safety-sell` 로 조건축(reason)과 주체축(actor)을 함께 노출. `stop_loss`/`take_profit`/`forced_close_at` 는 watchdog expired 에서만 발화하므로 actor 동반으로 "안전망 자율청산 vs 전략 손절"을 구분(신규 reason 값·필드 없음). `slot_transition_log` 는 이미 actor 기록. -## Flow: `운영 조회 리포트 (trex-core report)` (BAT-046 / dcy-029, REQ-088) +## Flow: `운영 조회 리포트 (trex-core report)` (BAT-046 / dcy-029, REQ-088; BAT-052 / dcy-035, REQ-110/111) -운영자 `trex-core report [--json]` — **direct-DB 시점 집계**(신규 RPC 없음, OQ-1): +운영자 `trex-core report [--json]` — **direct-DB + portfolio-hub WAC 경로**(RPC 없음): 1. `daily_slot` 에서 보유(status=holding: symbol/qty/avg_price) + 대기(pending_*) 조회. 2. `ledger_outbox` 에서 오늘(trading_date KST 경계 → UTC 범위) 체결 조회. -3. `application/operator_report.py` 순수 집계: 실현 PnL = Σ매도(qty·price − fee − tax) − Σ매수(qty·price + fee) (net 매매 현금흐름, 수수료·세금 반영 BAT-038 정책, 자산별 분해). 일중 진입·청산 전략에서 net ≈ 실현손익(미청산 평가손익 제외). -4. text 표 또는 `--json` 출력. 일일 리포트 자동화 기반. +3. `application/operator_report.py` 순수 집계(`build_report`): 기존 당일 net(Σ매도−Σ매수) 계산 (회귀 보존, NFR-2). +4. **WAC 경로(REQ-110)**: `TREX_PORTFOLIO_HUB_BASE_URL` 설정 시 portfolio-hub `GET /v1/portfolios/{id}/pnl/realized` 를 portfolio_id 별 N회 호출(`get_realized_pnl(pid, from_dt, to_dt)`, from_dt/to_dt = 오늘 KST date). 응답의 `total_realized_pnl` / 자산별 `realized_pnl` 을 합산해 `apply_wac_to_report` 로 report 에 반영. 매수 fee 가 cost basis 에 편입된 WAC 기준이므로 멀티데이 정확. +5. **degrade(REQ-111)**: portfolio-hub 미설정 또는 `PortfolioHubUnavailableError` → `wac_unavailable=True` + `wac_unavailable_reason` 기록. 폴백 없음(당일 net 을 WAC 위치에 대체하지 않음). 보유/대기/체결 건수 섹션은 정상 출력. +6. text 표 또는 `--json` 출력. WAC 섹션 미가용 시 에러 메시지만 표시, 나머지 섹션 정상. ## Shared Components diff --git a/tests/application/test_operator_report.py b/tests/application/test_operator_report.py index 7cddc4c..ece37fe 100644 --- a/tests/application/test_operator_report.py +++ b/tests/application/test_operator_report.py @@ -79,7 +79,8 @@ class TestKstBounds: # --------------------------------------------------------------------------- -# REQ-110: WAC 실현손익 통합 (apply_wac_to_report, buy_fee_by_asset) +# REQ-110: WAC 실현손익 통합 (apply_wac_to_report) +# CHG-20260626-02: buy_fee_only_assets·buy_fee_by_asset 제거 (cost basis 편입) # --------------------------------------------------------------------------- class TestApplyWacToReport: diff --git a/trex_core/runtime/cli.py b/trex_core/runtime/cli.py index 003e480..b37e319 100644 --- a/trex_core/runtime/cli.py +++ b/trex_core/runtime/cli.py @@ -33,7 +33,7 @@ import argparse import json as _json import sys import uuid -from datetime import datetime +from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo from trex_core.adapters.messaging import topology @@ -722,9 +722,7 @@ def _cmd_report(args, cfg) -> int: cfg.portfolio_hub.base_url, token=cfg.portfolio_hub.token ) # OQ-2: 오늘(KST 일 경계) 기본값 - _KST = __import__("datetime").timezone( - __import__("datetime").timedelta(hours=9) - ) + _KST = timezone(timedelta(hours=9)) from_date = start_utc.astimezone(_KST).date() from_dt_str = from_date.isoformat() to_dt_str = from_date.isoformat() # 당일