|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | from collections.abc import Mapping |
| 4 | +from dataclasses import replace |
4 | 5 | from typing import Any |
5 | 6 |
|
6 | 7 | from quant_platform_kit.strategy_contracts import ( |
@@ -34,6 +35,50 @@ def _default_threshold_value(total_equity: float) -> float: |
34 | 35 | return max(_DEFAULT_MIN_TRADE_FLOOR, float(total_equity) * _DEFAULT_REBALANCE_THRESHOLD_RATIO) |
35 | 36 |
|
36 | 37 |
|
| 38 | +def _resolve_platform_reserved_cash( |
| 39 | + *, |
| 40 | + total_equity: float, |
| 41 | + runtime_metadata: Mapping[str, Any] | None, |
| 42 | +) -> float: |
| 43 | + raw_policy = (runtime_metadata or {}).get("firstrade_execution_policy") |
| 44 | + if not isinstance(raw_policy, Mapping): |
| 45 | + return 0.0 |
| 46 | + reserved_cash_floor_usd = max(0.0, float(raw_policy.get("reserved_cash_floor_usd", 0.0) or 0.0)) |
| 47 | + reserved_cash_ratio = float(raw_policy.get("reserved_cash_ratio", 0.0) or 0.0) |
| 48 | + reserved_cash_ratio = max(0.0, min(1.0, reserved_cash_ratio)) |
| 49 | + return max(reserved_cash_floor_usd, max(0.0, float(total_equity)) * reserved_cash_ratio) |
| 50 | + |
| 51 | + |
| 52 | +def _apply_reserved_cash_policy( |
| 53 | + annotations: ValueTargetExecutionAnnotations, |
| 54 | + *, |
| 55 | + portfolio_inputs, |
| 56 | + runtime_metadata: Mapping[str, Any] | None, |
| 57 | +) -> ValueTargetExecutionAnnotations: |
| 58 | + reserved_cash = max( |
| 59 | + float(annotations.reserved_cash or 0.0), |
| 60 | + _resolve_platform_reserved_cash( |
| 61 | + total_equity=float(portfolio_inputs.total_equity), |
| 62 | + runtime_metadata=runtime_metadata, |
| 63 | + ), |
| 64 | + ) |
| 65 | + base_investable_cash = annotations.investable_cash |
| 66 | + if base_investable_cash is None: |
| 67 | + base_investable_cash = max( |
| 68 | + 0.0, |
| 69 | + float(portfolio_inputs.liquid_cash) - float(annotations.reserved_cash or 0.0), |
| 70 | + ) |
| 71 | + investable_cash = min( |
| 72 | + max(0.0, float(base_investable_cash)), |
| 73 | + max(0.0, float(portfolio_inputs.liquid_cash) - reserved_cash), |
| 74 | + ) |
| 75 | + return replace( |
| 76 | + annotations, |
| 77 | + reserved_cash=reserved_cash, |
| 78 | + investable_cash=investable_cash, |
| 79 | + ) |
| 80 | + |
| 81 | + |
37 | 82 | def _build_hold_current_value_decision(portfolio_inputs, *, diagnostics: Mapping[str, Any]) -> StrategyDecision: |
38 | 83 | positions = [] |
39 | 84 | for symbol, market_value in sorted(portfolio_inputs.market_values.items()): |
@@ -210,6 +255,11 @@ def map_strategy_decision_to_plan( |
210 | 255 | normalized_decision, |
211 | 256 | portfolio_inputs=portfolio_inputs, |
212 | 257 | ) |
| 258 | + annotations = _apply_reserved_cash_policy( |
| 259 | + annotations, |
| 260 | + portfolio_inputs=portfolio_inputs, |
| 261 | + runtime_metadata=runtime_metadata, |
| 262 | + ) |
213 | 263 | plan = build_value_target_runtime_plan( |
214 | 264 | normalized_decision, |
215 | 265 | strategy_profile=canonical_profile, |
|
0 commit comments