Skip to content

Commit 7c98af8

Browse files
authored
Guard zero-equity weight target translation (#123)
1 parent f447f8d commit 7c98af8

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

decision_mapper.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,28 @@ def _build_hold_current_value_decision(portfolio_inputs) -> StrategyDecision:
271271
return StrategyDecision(positions=tuple(positions))
272272

273273

274+
def _build_zero_equity_value_decision(decision: StrategyDecision) -> StrategyDecision:
275+
positions: list[PositionTarget] = []
276+
for position in decision.positions:
277+
positions.append(
278+
PositionTarget(
279+
symbol=position.symbol,
280+
target_value=0.0,
281+
role=position.role or _symbol_role(position.symbol),
282+
order_preference=position.order_preference,
283+
)
284+
)
285+
return StrategyDecision(
286+
positions=tuple(positions),
287+
budgets=decision.budgets,
288+
risk_flags=tuple(dict.fromkeys((*decision.risk_flags, "no_execute"))),
289+
diagnostics={
290+
**dict(decision.diagnostics),
291+
"execution_blocked_reason": "non_positive_total_equity",
292+
},
293+
)
294+
295+
274296
def _normalize_to_value_target_decision(
275297
decision: StrategyDecision,
276298
*,
@@ -284,14 +306,24 @@ def _normalize_to_value_target_decision(
284306
return decision, None
285307

286308
if target_mode == "weight" and not no_execute:
309+
total_equity = float(portfolio_inputs.total_equity)
310+
if total_equity <= 0.0:
311+
return _build_zero_equity_value_decision(
312+
decision,
313+
), _build_weight_translation_annotations(
314+
decision,
315+
total_equity=total_equity,
316+
liquid_cash=float(portfolio_inputs.liquid_cash),
317+
runtime_metadata=runtime_metadata,
318+
)
287319
translated = translate_decision_to_target_mode(
288320
decision,
289321
target_mode="value",
290-
total_equity=float(portfolio_inputs.total_equity),
322+
total_equity=total_equity,
291323
)
292324
return translated, _build_weight_translation_annotations(
293325
decision,
294-
total_equity=float(portfolio_inputs.total_equity),
326+
total_equity=total_equity,
295327
liquid_cash=float(portfolio_inputs.liquid_cash),
296328
runtime_metadata=runtime_metadata,
297329
)

tests/test_decision_mapper.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,34 @@ def test_applies_platform_reserved_cash_policy_to_weight_decision(self):
212212
self.assertEqual(plan["execution"]["reserved_cash"], 1500.0)
213213
self.assertEqual(plan["execution"]["investable_cash"], 2500.0)
214214

215+
def test_zero_equity_weight_targets_no_execute_instead_of_translation_error(self):
216+
decision = StrategyDecision(
217+
positions=(
218+
PositionTarget(symbol="AAPL", target_weight=0.5),
219+
PositionTarget(symbol="MSFT", target_weight=0.5),
220+
),
221+
diagnostics={"signal_description": "risk on"},
222+
)
223+
snapshot = PortfolioSnapshot(
224+
as_of=datetime.now(timezone.utc),
225+
total_equity=0.0,
226+
buying_power=0.0,
227+
positions=(),
228+
metadata={"account_hash": "longbridge-zero"},
229+
)
230+
231+
plan = map_strategy_decision_to_plan(
232+
decision,
233+
snapshot=snapshot,
234+
strategy_profile="mega_cap_leader_rotation_top50_balanced",
235+
)
236+
237+
self.assertEqual(plan["allocation"]["target_mode"], "value")
238+
self.assertEqual(plan["allocation"]["targets"], {"AAPL": 0.0, "MSFT": 0.0})
239+
self.assertEqual(plan["portfolio"]["total_equity"], 0.0)
240+
self.assertEqual(plan["execution"]["trade_threshold_value"], 100.0)
241+
self.assertEqual(plan["execution"]["investable_cash"], 0.0)
242+
215243
def test_carries_snapshot_manifest_diagnostics_to_execution(self):
216244
decision = StrategyDecision(
217245
positions=(),

0 commit comments

Comments
 (0)