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
36 changes: 34 additions & 2 deletions decision_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve holdings for blocked zero-equity plans

When a margin or loan-backed account reports total_equity <= 0 while still holding strategy symbols, this branch converts the blocked weight decision into zero-dollar targets. The generated plan does not carry a consumable no_execute guard for execution; the rebalance loop reads allocation["targets"] and sells whenever target_values[symbol] - market_values[symbol] is below the threshold (application/execution_service.py:606, application/execution_service.py:693). Previously this input failed before any executable plan was produced, but now it can submit liquidation sells for those symbols. For a no-execute fallback, keep current market values as targets or add an execution block that downstream code actually honors.

Useful? React with 👍 / 👎.

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,
*,
Expand All @@ -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,
)
Expand Down
28 changes: 28 additions & 0 deletions tests/test_decision_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=(),
Expand Down