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 |
+9 -4
View File
@@ -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 <toml>` 또는 `--instance <key>`, 미지정 시 `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` 설정 시 자동 활성화된다.
---
+6 -4
View File
@@ -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=<r> 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
+2 -1
View File
@@ -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:
+2 -4
View File
@@ -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() # 당일