@@ -16,10 +16,12 @@ signal (REQ-028 AC-2) without callers seeing raw httpx exceptions.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from trex_core.application.auto_reconcile import HoldingsCostBasisEvidence
|
||||
from trex_core.application.portfolio import (
|
||||
BrokerCredentialPayload,
|
||||
PortfolioHubUnavailableError,
|
||||
@@ -268,6 +270,56 @@ class PortfolioHubReadAdapter:
|
||||
return None, "invalid"
|
||||
return value, "ok"
|
||||
|
||||
def get_cost_basis_evidence(self, portfolio_id: str) -> HoldingsCostBasisEvidence:
|
||||
"""Return only canonical, complete evidence used by auto-reconcile."""
|
||||
url = f"{self._base_url}/v1/portfolios/{portfolio_id}/holdings/cost-basis"
|
||||
try:
|
||||
resp = self._get(url)
|
||||
except httpx.HTTPError as exc:
|
||||
raise PortfolioHubUnavailableError(
|
||||
f"cost_basis network error: {exc.__class__.__name__}"
|
||||
) from exc
|
||||
if not (200 <= resp.status_code < 300):
|
||||
raise PortfolioHubUnavailableError(
|
||||
f"cost_basis http_status_{resp.status_code}"
|
||||
)
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError as exc:
|
||||
raise PortfolioHubUnavailableError("cost_basis invalid_json") from exc
|
||||
if not isinstance(body, dict):
|
||||
raise PortfolioHubUnavailableError("cost_basis invalid_payload")
|
||||
|
||||
response_pid = body.get("portfolio_id")
|
||||
symbol = body.get("symbol")
|
||||
if str(response_pid or "") != portfolio_id or not isinstance(symbol, str) or not symbol:
|
||||
raise PortfolioHubUnavailableError("cost_basis identity_mismatch")
|
||||
|
||||
def _finite_number(name: str, *, positive: bool) -> float:
|
||||
raw = body.get(name)
|
||||
if isinstance(raw, bool):
|
||||
raise PortfolioHubUnavailableError(f"cost_basis invalid_{name}")
|
||||
try:
|
||||
value = float(str(raw))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PortfolioHubUnavailableError(f"cost_basis invalid_{name}") from exc
|
||||
if not math.isfinite(value) or (positive and value <= 0) or (not positive and value < 0):
|
||||
raise PortfolioHubUnavailableError(f"cost_basis invalid_{name}")
|
||||
return value
|
||||
|
||||
warning = body.get("data_completeness_warning")
|
||||
if warning is not None and not isinstance(warning, str):
|
||||
raise PortfolioHubUnavailableError("cost_basis invalid_completeness_warning")
|
||||
return HoldingsCostBasisEvidence(
|
||||
portfolio_id=portfolio_id,
|
||||
symbol=symbol,
|
||||
current_quantity=_finite_number("current_quantity", positive=True),
|
||||
cost_basis_per_unit=_finite_number("cost_basis_per_unit", positive=True),
|
||||
total_cost_basis=_finite_number("total_cost_basis", positive=True),
|
||||
observed_at=datetime.now(tz=UTC),
|
||||
data_completeness_warning=warning or None,
|
||||
)
|
||||
|
||||
def get_cost_basis_with_diagnostics(
|
||||
self,
|
||||
portfolio_id: str,
|
||||
|
||||
Reference in New Issue
Block a user