From c27da7ed870cd837e96565b297029c98f4cfbdac Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:51:22 +0800 Subject: [PATCH] Guard zero-equity weight target translation --- decision_mapper.py | 36 +++++++++++++++++++++++++++++++++-- tests/test_decision_mapper.py | 28 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/decision_mapper.py b/decision_mapper.py index 9190e2f..d3dfb45 100644 --- a/decision_mapper.py +++ b/decision_mapper.py @@ -271,6 +271,28 @@ def _build_hold_current_value_decision(portfolio_inputs) -> StrategyDecision: return StrategyDecision(positions=tuple(positions)) +def _build_zero_equity_value_decision(decision: StrategyDecision) -> StrategyDecision: + positions: list[PositionTarget] = [] + for position in decision.positions: + positions.append( + PositionTarget( + symbol=position.symbol, + target_value=0.0, + role=position.role or _symbol_role(position.symbol), + order_preference=position.order_preference, + ) + ) + return StrategyDecision( + positions=tuple(positions), + budgets=decision.budgets, + risk_flags=tuple(dict.fromkeys((*decision.risk_flags, "no_execute"))), + diagnostics={ + **dict(decision.diagnostics), + "execution_blocked_reason": "non_positive_total_equity", + }, + ) + + def _normalize_to_value_target_decision( decision: StrategyDecision, *, @@ -284,14 +306,24 @@ def _normalize_to_value_target_decision( return decision, None if target_mode == "weight" and not no_execute: + total_equity = float(portfolio_inputs.total_equity) + if total_equity <= 0.0: + return _build_zero_equity_value_decision( + decision, + ), _build_weight_translation_annotations( + decision, + total_equity=total_equity, + liquid_cash=float(portfolio_inputs.liquid_cash), + runtime_metadata=runtime_metadata, + ) translated = translate_decision_to_target_mode( decision, target_mode="value", - total_equity=float(portfolio_inputs.total_equity), + total_equity=total_equity, ) return translated, _build_weight_translation_annotations( decision, - total_equity=float(portfolio_inputs.total_equity), + total_equity=total_equity, liquid_cash=float(portfolio_inputs.liquid_cash), runtime_metadata=runtime_metadata, ) diff --git a/tests/test_decision_mapper.py b/tests/test_decision_mapper.py index 39ae5fc..617ff82 100644 --- a/tests/test_decision_mapper.py +++ b/tests/test_decision_mapper.py @@ -212,6 +212,34 @@ def test_applies_platform_reserved_cash_policy_to_weight_decision(self): self.assertEqual(plan["execution"]["reserved_cash"], 1500.0) self.assertEqual(plan["execution"]["investable_cash"], 2500.0) + def test_zero_equity_weight_targets_no_execute_instead_of_translation_error(self): + decision = StrategyDecision( + positions=( + PositionTarget(symbol="AAPL", target_weight=0.5), + PositionTarget(symbol="MSFT", target_weight=0.5), + ), + diagnostics={"signal_description": "risk on"}, + ) + snapshot = PortfolioSnapshot( + as_of=datetime.now(timezone.utc), + total_equity=0.0, + buying_power=0.0, + positions=(), + metadata={"account_hash": "longbridge-zero"}, + ) + + plan = map_strategy_decision_to_plan( + decision, + snapshot=snapshot, + strategy_profile="mega_cap_leader_rotation_top50_balanced", + ) + + self.assertEqual(plan["allocation"]["target_mode"], "value") + self.assertEqual(plan["allocation"]["targets"], {"AAPL": 0.0, "MSFT": 0.0}) + self.assertEqual(plan["portfolio"]["total_equity"], 0.0) + self.assertEqual(plan["execution"]["trade_threshold_value"], 100.0) + self.assertEqual(plan["execution"]["investable_cash"], 0.0) + def test_carries_snapshot_manifest_diagnostics_to_execution(self): decision = StrategyDecision( positions=(),