Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions application/runtime_broker_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,23 @@ def _resolve_total_equity(
cash_balance: float | None,
buying_power: float | None,
position_market_value: float,
prefer_cash_plus_positions: bool = False,
) -> tuple[float, str]:
resolved_cash = _positive_or_none(cash_balance)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle zero managed cash before using account totals

When a strategy-managed account is fully invested (cash_balance is reported as $0.00) and also has unmanaged holdings, _positive_or_none(cash_balance) makes resolved_cash None, so the new managed override is skipped and the later balance_total branch still returns the full account value. That reintroduces the exact sizing bug this change is trying to avoid for a common fully-invested portfolio: managed snapshots can include unmanaged holdings in total_equity instead of using the filtered position value (plus zero cash).

Useful? React with 👍 / 👎.

if resolved_cash is not None:
combined_value = resolved_cash + max(0.0, float(position_market_value))
if prefer_cash_plus_positions and combined_value > 0.0:
return combined_value, "cash_plus_positions"
else:
combined_value = None

balance_total = _positive_or_none(
_first_numeric_by_keyword_groups(balances, _TOTAL_EQUITY_KEYWORD_GROUPS)
)
if balance_total is not None:
return balance_total, "balance_total"

resolved_cash = _positive_or_none(cash_balance)
if resolved_cash is not None:
combined_value = resolved_cash + max(0.0, float(position_market_value))
if combined_value is not None:
if combined_value > 0.0:
return combined_value, "cash_plus_positions"

Expand Down Expand Up @@ -257,6 +264,7 @@ def build_portfolio_snapshot(self) -> PortfolioSnapshot:
cash_balance=cash_balance,
buying_power=buying_power,
position_market_value=position_market_value,
prefer_cash_plus_positions=bool(managed),
)
return PortfolioSnapshot(
as_of=self.clock(),
Expand Down
30 changes: 28 additions & 2 deletions tests/test_runtime_broker_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ def test_runtime_adapters_build_quote_and_portfolio_ports():
portfolio = adapters.build_portfolio_port().get_portfolio_snapshot()

assert quote.last_price == 10.5
assert portfolio.total_equity == 120.0
assert portfolio.total_equity == 41.0
assert portfolio.cash_balance == 20.0
assert portfolio.positions[0].symbol == "SPY"
assert portfolio.metadata["total_equity_source"] == "cash_plus_positions"


def test_portfolio_snapshot_uses_account_value_balance_key():
Expand All @@ -56,7 +57,6 @@ def get_balances(self, _account):
adapters = build_runtime_broker_adapters(
client=AccountValueClient(),
account="12345678",
strategy_symbols=("SPY",),
)

portfolio = adapters.build_portfolio_port().get_portfolio_snapshot()
Expand All @@ -66,6 +66,32 @@ def get_balances(self, _account):
assert portfolio.metadata["total_equity_source"] == "balance_total"


def test_managed_portfolio_snapshot_ignores_full_account_value_balance_key():
class AccountValueClient(FakeClient):
def get_balances(self, _account):
return {"account_value": "$1,234.56", "cash_balance": "$200.00"}

def get_positions(self, _account):
return {
"items": [
{"symbol": "SPY", "quantity": "2", "market_value": "21.00"},
{"symbol": "AAPL", "quantity": "3", "market_value": "300.00"},
]
}

adapters = build_runtime_broker_adapters(
client=AccountValueClient(),
account="12345678",
strategy_symbols=("SPY",),
)

portfolio = adapters.build_portfolio_port().get_portfolio_snapshot()

assert portfolio.total_equity == 221.0
assert [position.symbol for position in portfolio.positions] == ["SPY"]
assert portfolio.metadata["total_equity_source"] == "cash_plus_positions"


def test_portfolio_snapshot_falls_back_to_cash_when_total_value_missing():
class CashOnlyClient(FakeClient):
def get_balances(self, _account):
Expand Down