diff --git a/broker_sync/cli.py b/broker_sync/cli.py
index 7f855f5..64057f7 100644
--- a/broker_sync/cli.py
+++ b/broker_sync/cli.py
@@ -310,6 +310,16 @@ def ibkr(
float(drift),
)
)
+ # Cash balances (one row per currency from CashReport, plus a
+ # BASE_SUMMARY row consolidated in account base currency).
+ for currency, ending_cash in provider.cash_balances():
+ drift_metrics.append(
+ (
+ "ibkr_cash_balance",
+ {"currency": currency, "account": "ibkr-uk"},
+ float(ending_cash),
+ )
+ )
drift_metrics.append(
("ibkr_sync_last_success_timestamp_seconds", {}, float(time.time()))
)
diff --git a/broker_sync/providers/ibkr.py b/broker_sync/providers/ibkr.py
index fcff89f..e180bcb 100644
--- a/broker_sync/providers/ibkr.py
+++ b/broker_sync/providers/ibkr.py
@@ -257,3 +257,20 @@ class IBKRProvider:
)
out.append((symbol, Decimal(str(pos.position))))
return out
+
+ def cash_balances(self) -> list[tuple[str, Decimal]]:
+ """Return ``[(currency, ending_cash), ...]`` from the CashReport.
+
+ Includes the ``BASE_SUMMARY`` aggregate row (account base currency
+ consolidated) plus any per-currency rows. Empty list if no
+ CashReport section in the Flex query or before first ``fetch()``.
+ """
+ if self._last_response is None:
+ return []
+ stmt = self._last_response.FlexStatements[0]
+ out: list[tuple[str, Decimal]] = []
+ for row in stmt.CashReport or []:
+ if row.endingCash is None or row.currency is None:
+ continue
+ out.append((str(row.currency), Decimal(str(row.endingCash))))
+ return out
diff --git a/tests/fixtures/ibkr/sample_flex.xml b/tests/fixtures/ibkr/sample_flex.xml
index 0d82fcf..d3130a3 100644
--- a/tests/fixtures/ibkr/sample_flex.xml
+++ b/tests/fixtures/ibkr/sample_flex.xml
@@ -16,6 +16,10 @@
+
+
+
+
diff --git a/tests/providers/test_ibkr.py b/tests/providers/test_ibkr.py
index 8dfba07..edbc51d 100644
--- a/tests/providers/test_ibkr.py
+++ b/tests/providers/test_ibkr.py
@@ -194,3 +194,31 @@ async def test_ibkr_provider_open_positions_after_fetch(
positions = provider.open_positions()
# VUAG → VUAG.L (LSE inferred from GBP); AAPL unchanged (USD)
assert dict(positions) == {"VUAG.L": Decimal("8"), "AAPL": Decimal("5")}
+
+
+async def test_ibkr_provider_cash_balances_after_fetch(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ """cash_balances() returns (currency, ending_cash) tuples from CashReport."""
+ from ibflex import client as ib_client
+
+ with open("tests/fixtures/ibkr/sample_flex.xml", "rb") as f:
+ xml_bytes = f.read()
+ monkeypatch.setattr(ib_client, "download", lambda *a, **kw: xml_bytes)
+
+ provider = IBKRProvider(
+ token="t",
+ query_id="q",
+ upstream_account_id="U12345678",
+ )
+ [a async for a in provider.fetch()]
+
+ balances = provider.cash_balances()
+ # Fixture has BASE_SUMMARY + USD rows, both 1.23
+ assert dict(balances) == {"BASE_SUMMARY": Decimal("1.23"), "USD": Decimal("1.23")}
+
+
+def test_ibkr_provider_cash_balances_before_fetch_returns_empty() -> None:
+ """No CashReport data before fetch()."""
+ provider = IBKRProvider(token="t", query_id="q", upstream_account_id="U12345678")
+ assert provider.cash_balances() == []