diff --git a/decision_mapper.py b/decision_mapper.py index 5d2127f..2b71e71 100644 --- a/decision_mapper.py +++ b/decision_mapper.py @@ -6,6 +6,7 @@ from quant_platform_kit.strategy_contracts import ( PositionTarget, + StrategyContractValidationError, StrategyDecision, ValueTargetExecutionAnnotations, build_value_target_execution_annotations, @@ -244,7 +245,23 @@ def _normalize_to_value_decision( def _build_annotations(decision: StrategyDecision, *, portfolio_inputs) -> ValueTargetExecutionAnnotations: - annotations = build_value_target_execution_annotations(decision) + try: + annotations = build_value_target_execution_annotations(decision) + except StrategyContractValidationError as exc: + if "requires trade_threshold_value" not in str(exc): + raise + diagnostics = dict(decision.diagnostics) + raw_annotations = diagnostics.get("execution_annotations") + execution_annotations = ( + dict(raw_annotations) if isinstance(raw_annotations, Mapping) else {} + ) + execution_annotations["trade_threshold_value"] = _default_threshold_value( + float(portfolio_inputs.total_equity) + ) + diagnostics["execution_annotations"] = execution_annotations + annotations = build_value_target_execution_annotations( + replace(decision, diagnostics=diagnostics) + ) investable_cash = annotations.investable_cash if investable_cash is None: investable_cash = max(0.0, float(portfolio_inputs.liquid_cash) - annotations.reserved_cash) diff --git a/tests/test_decision_mapper.py b/tests/test_decision_mapper.py index 6e61521..75ad1c4 100644 --- a/tests/test_decision_mapper.py +++ b/tests/test_decision_mapper.py @@ -70,3 +70,50 @@ def test_platform_reserved_cash_policy_does_not_lower_strategy_reserve(): assert plan["execution"]["reserved_cash"] == 1200.0 assert plan["execution"]["investable_cash"] == 1800.0 + + +def test_value_decision_without_threshold_uses_platform_default(): + decision = StrategyDecision( + positions=(PositionTarget(symbol="AAA", target_value=500.0),), + diagnostics={"signal_display": "hold AAA"}, + ) + snapshot = PortfolioSnapshot( + as_of=datetime.now(timezone.utc), + total_equity=20000.0, + buying_power=3000.0, + positions=(), + ) + + plan = map_strategy_decision_to_plan( + decision, + snapshot=snapshot, + strategy_profile="mega_cap_leader_rotation_top50_balanced", + ) + + assert plan["execution"]["trade_threshold_value"] == 200.0 + assert plan["execution"]["current_min_trade"] == 200.0 + assert plan["allocation"]["targets"]["AAA"] == 500.0 + + +def test_no_execute_decision_without_threshold_holds_current_positions(): + decision = StrategyDecision( + positions=(), + risk_flags=("no_execute",), + diagnostics={"signal_description": "no actionable signal"}, + ) + snapshot = PortfolioSnapshot( + as_of=datetime.now(timezone.utc), + total_equity=5000.0, + buying_power=1000.0, + positions=(Position(symbol="AAA", quantity=3, market_value=750.0),), + ) + + plan = map_strategy_decision_to_plan( + decision, + snapshot=snapshot, + strategy_profile="mega_cap_leader_rotation_top50_balanced", + ) + + assert plan["execution"]["trade_threshold_value"] == 100.0 + assert plan["execution"]["current_min_trade"] == 100.0 + assert plan["allocation"]["targets"]["AAA"] == 750.0