feat: auto-reconcile stale holding mirrors

Closes #94
This commit is contained in:
Hermes Agent
2026-09-14 12:55:04 +09:00
parent adf131f19c
commit c70ac0fb24
19 changed files with 1181 additions and 16 deletions
+52
View File
@@ -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,