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:
@@ -47,8 +47,38 @@ class PortfolioHubUnavailableError(PortfolioHubReadError):
|
||||
"""portfolio-hub unreachable / 5xx / timeout — degraded signal upstream."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- realized PnL (REQ-109)
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RealizedPnlItem:
|
||||
"""Single sell-trade realized PnL item from portfolio-hub WAC response.
|
||||
|
||||
Maps to portfolio-hub RealizedPnlItem schema (REQ-040).
|
||||
Minimum required: symbol, realized_pnl, sell_qty.
|
||||
Additional fields preserved for completeness.
|
||||
"""
|
||||
symbol: Optional[str]
|
||||
realized_pnl: float
|
||||
sell_qty: float
|
||||
cost_basis_per_unit: float = 0.0
|
||||
fee_amount: float = 0.0
|
||||
tax_amount: float = 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RealizedPnlResult:
|
||||
"""Aggregated WAC realized PnL result for one portfolio (REQ-109).
|
||||
|
||||
Returned by get_realized_pnl port method.
|
||||
"""
|
||||
items: list[RealizedPnlItem]
|
||||
cost_basis_from: Optional[str] # ISO date string or None
|
||||
data_completeness_warning: bool # True when portfolio-hub signals incomplete history
|
||||
total_realized_pnl: float
|
||||
|
||||
|
||||
class PortfolioHubReadPort(Protocol):
|
||||
"""Read-side contract for portfolio-hub universe (REQ-027)."""
|
||||
"""Read-side contract for portfolio-hub universe (REQ-027) + realized PnL (REQ-109)."""
|
||||
|
||||
def get_portfolios(self, daemon_id: str) -> list[PortfolioItem]:
|
||||
"""Return all portfolios where `managed_by_daemon_id == daemon_id`."""
|
||||
@@ -62,6 +92,31 @@ class PortfolioHubReadPort(Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
def get_realized_pnl(
|
||||
self,
|
||||
portfolio_id: str,
|
||||
from_dt: str,
|
||||
to_dt: str,
|
||||
*,
|
||||
symbol: Optional[str] = None,
|
||||
) -> "RealizedPnlResult":
|
||||
"""GET /v1/portfolios/{portfolio_id}/pnl/realized — WAC realized PnL (REQ-109).
|
||||
|
||||
Args:
|
||||
portfolio_id: target portfolio.
|
||||
from_dt: ISO date string (YYYY-MM-DD), sell-date inclusive start.
|
||||
to_dt: ISO date string (YYYY-MM-DD), sell-date inclusive end.
|
||||
symbol: optional symbol filter.
|
||||
|
||||
Returns:
|
||||
RealizedPnlResult with per-sell items, total, cost_basis_from,
|
||||
data_completeness_warning.
|
||||
|
||||
Raises:
|
||||
PortfolioHubUnavailableError: on network error or non-2xx response.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class DegradedReason(str, Enum):
|
||||
"""Machine-readable degraded reason codes (REQ-028 AC-2, dcy-005 OQ-5)."""
|
||||
|
||||
Reference in New Issue
Block a user