diff --git a/decision_mapper.py b/decision_mapper.py index 105d12f..9190e2f 100644 --- a/decision_mapper.py +++ b/decision_mapper.py @@ -21,6 +21,17 @@ _INCOME_SYMBOLS = frozenset({"QQQI", "SPYI"}) _DEFAULT_MIN_TRADE_FLOOR = 100.0 _DEFAULT_REBALANCE_THRESHOLD_RATIO = 0.01 +_SNAPSHOT_DIAGNOSTIC_FIELDS = ( + "snapshot_manifest_price_as_of", + "snapshot_manifest_universe_as_of", + "snapshot_manifest_source_input_status", + "snapshot_manifest_source_input_fallback_used", + "snapshot_manifest_source_input_fallback_reason", + "snapshot_manifest_source_input_fallback_streak", + "snapshot_manifest_source_input_manifest_path", + "snapshot_manifest_source_refresh_run_id", + "snapshot_manifest_source_refresh_generated_at", +) def _build_portfolio_inputs( @@ -106,6 +117,22 @@ def _resolve_platform_reserved_cash( return max(reserved_cash_floor_usd, max(0.0, float(total_equity)) * reserved_cash_ratio) +def _attach_snapshot_diagnostics( + plan: dict[str, Any], + *, + decision: StrategyDecision, + runtime_metadata: Mapping[str, Any] | None, +) -> None: + execution = plan.get("execution") + if not isinstance(execution, dict): + return + diagnostics = {**dict(runtime_metadata or {}), **dict(decision.diagnostics)} + for field in _SNAPSHOT_DIAGNOSTIC_FIELDS: + value = diagnostics.get(field) + if value is not None and value != "": + execution[field] = value + + def _apply_reserved_cash_policy( annotations: ValueTargetExecutionAnnotations, *, @@ -498,4 +525,9 @@ def map_strategy_decision_to_plan( cash_by_currency = _cash_by_currency_from_snapshot(snapshot) if cash_by_currency: plan["portfolio"]["cash_by_currency"] = cash_by_currency + _attach_snapshot_diagnostics( + plan, + decision=normalized_decision, + runtime_metadata=runtime_metadata, + ) return plan diff --git a/tests/test_decision_mapper.py b/tests/test_decision_mapper.py index 2214285..39ae5fc 100644 --- a/tests/test_decision_mapper.py +++ b/tests/test_decision_mapper.py @@ -212,6 +212,41 @@ 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_carries_snapshot_manifest_diagnostics_to_execution(self): + decision = StrategyDecision( + positions=(), + risk_flags=("no_execute",), + diagnostics={"signal_description": "monthly cadence"}, + ) + snapshot = PortfolioSnapshot( + as_of=datetime.now(timezone.utc), + total_equity=10000.0, + buying_power=10000.0, + positions=(), + metadata={"account_hash": "longbridge-snapshot"}, + ) + + plan = map_strategy_decision_to_plan( + decision, + snapshot=snapshot, + strategy_profile="mega_cap_leader_rotation_top50_balanced", + runtime_metadata={ + "snapshot_manifest_price_as_of": "2026-06-01", + "snapshot_manifest_universe_as_of": "2026-04-29", + "snapshot_manifest_source_input_status": "universe_fallback", + "snapshot_manifest_source_input_fallback_used": True, + "snapshot_manifest_source_input_fallback_streak": 1, + "snapshot_manifest_source_refresh_run_id": "26785047433", + }, + ) + + self.assertEqual(plan["execution"]["snapshot_manifest_price_as_of"], "2026-06-01") + self.assertEqual(plan["execution"]["snapshot_manifest_universe_as_of"], "2026-04-29") + self.assertEqual(plan["execution"]["snapshot_manifest_source_input_status"], "universe_fallback") + self.assertIs(plan["execution"]["snapshot_manifest_source_input_fallback_used"], True) + self.assertEqual(plan["execution"]["snapshot_manifest_source_input_fallback_streak"], 1) + self.assertEqual(plan["execution"]["snapshot_manifest_source_refresh_run_id"], "26785047433") + def test_platform_reserved_cash_policy_does_not_lower_strategy_reserve(self): decision = StrategyDecision( positions=(PositionTarget(symbol="TQQQ", target_value=5000.0),),