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:
@@ -666,18 +666,22 @@ def _cmd_verify(args, cfg) -> int:
|
||||
|
||||
|
||||
def _cmd_report(args, cfg) -> int:
|
||||
"""REQ-088: holdings / today's fills / realized PnL / pending — direct-DB."""
|
||||
"""REQ-088/110/111: holdings / today's fills / WAC realized PnL / pending — direct-DB."""
|
||||
import psycopg
|
||||
from datetime import date, timedelta
|
||||
|
||||
from trex_core.application.operator_report import (
|
||||
Holding,
|
||||
PendingOrder,
|
||||
TradeRow,
|
||||
apply_wac_to_report,
|
||||
build_report,
|
||||
buy_fee_by_asset,
|
||||
kst_day_bounds_utc,
|
||||
render_json,
|
||||
render_text,
|
||||
)
|
||||
from trex_core.application.portfolio import PortfolioHubUnavailableError
|
||||
|
||||
start_utc, end_utc = kst_day_bounds_utc()
|
||||
holdings: list[Holding] = []
|
||||
@@ -709,6 +713,66 @@ def _cmd_report(args, cfg) -> int:
|
||||
float(fee or 0), float(tax or 0)))
|
||||
|
||||
rep = build_report(holdings, pending, trades)
|
||||
|
||||
# REQ-110: WAC 실현손익 통합 (portfolio-hub 설정 있으면 WAC 경로)
|
||||
# REQ-111: 미가용 시 degrade (폴백 없음)
|
||||
if cfg.portfolio_hub and cfg.portfolio_hub.base_url:
|
||||
try:
|
||||
from trex_core.adapters.portfolio.hub_read import PortfolioHubReadAdapter
|
||||
|
||||
hub = PortfolioHubReadAdapter(
|
||||
cfg.portfolio_hub.base_url, token=cfg.portfolio_hub.token
|
||||
)
|
||||
# OQ-2: 오늘(KST 일 경계) 기본값
|
||||
_KST = __import__("datetime").timezone(
|
||||
__import__("datetime").timedelta(hours=9)
|
||||
)
|
||||
from_date = start_utc.astimezone(_KST).date()
|
||||
from_dt_str = from_date.isoformat()
|
||||
to_dt_str = from_date.isoformat() # 당일
|
||||
|
||||
# OQ-4: portfolio_id별 N콜
|
||||
target_pids = (
|
||||
{h.portfolio_id for h in holdings}
|
||||
| {p.portfolio_id for p in pending}
|
||||
)
|
||||
|
||||
wac_total = 0.0
|
||||
wac_by_asset_accum: dict[str, float] = {}
|
||||
any_warning = False
|
||||
for pid in sorted(target_pids):
|
||||
result = hub.get_realized_pnl(pid, from_dt_str, to_dt_str)
|
||||
wac_total += result.total_realized_pnl
|
||||
if result.data_completeness_warning:
|
||||
any_warning = True
|
||||
for item in result.items:
|
||||
key = item.symbol or pid
|
||||
wac_by_asset_accum[key] = wac_by_asset_accum.get(key, 0.0) + item.realized_pnl
|
||||
|
||||
# OQ-3: buy_fee_only_assets = 당일 매수 거래 fee 합산 (매도 없는 자산 대상)
|
||||
sell_assets = {t.asset_id for t in trades if t.side == "sell"}
|
||||
buy_fee_only = buy_fee_by_asset(trades, exclude_symbols=sell_assets)
|
||||
|
||||
apply_wac_to_report(
|
||||
rep,
|
||||
wac_total=wac_total,
|
||||
wac_by_asset=wac_by_asset_accum,
|
||||
buy_fee_only_assets=buy_fee_only,
|
||||
data_completeness_warning=any_warning,
|
||||
)
|
||||
|
||||
except PortfolioHubUnavailableError as exc:
|
||||
rep.wac_unavailable = True
|
||||
rep.wac_unavailable_reason = (
|
||||
f"portfolio-hub unavailable — realized PnL unavailable ({exc})"
|
||||
)
|
||||
else:
|
||||
# REQ-111 AC-4: base_url 미설정도 동일 degrade
|
||||
rep.wac_unavailable = True
|
||||
rep.wac_unavailable_reason = (
|
||||
"portfolio-hub unavailable — realized PnL unavailable (base_url not configured)"
|
||||
)
|
||||
|
||||
if getattr(args, "json", False):
|
||||
print(_json.dumps(render_json(rep), ensure_ascii=False))
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user