|
7 | 7 |
|
8 | 8 | from quant_platform_kit.common.models import OrderIntent |
9 | 9 | from quant_platform_kit.common.ports import ExecutionPort, MarketDataPort |
| 10 | +try: |
| 11 | + from quant_platform_kit.common.small_account_compatibility import ( |
| 12 | + project_unbuyable_value_targets_to_cash, |
| 13 | + ) |
| 14 | +except ImportError: # pragma: no cover - compatibility with older pinned shared wheels |
| 15 | + def project_unbuyable_value_targets_to_cash( |
| 16 | + target_values, |
| 17 | + prices, |
| 18 | + *, |
| 19 | + symbols=None, |
| 20 | + quantity_step=1.0, |
| 21 | + ): |
| 22 | + adjusted = { |
| 23 | + str(symbol or "").strip().upper(): float(value or 0.0) |
| 24 | + for symbol, value in dict(target_values or {}).items() |
| 25 | + } |
| 26 | + step = max(0.0, float(quantity_step or 0.0)) |
| 27 | + if step <= 0.0: |
| 28 | + return adjusted, () |
| 29 | + candidate_symbols = ( |
| 30 | + tuple(adjusted) |
| 31 | + if symbols is None |
| 32 | + else tuple(dict.fromkeys(str(symbol or "").strip().upper() for symbol in symbols)) |
| 33 | + ) |
| 34 | + normalized_prices = { |
| 35 | + str(symbol or "").strip().upper(): float(price or 0.0) |
| 36 | + for symbol, price in dict(prices or {}).items() |
| 37 | + } |
| 38 | + substituted = [] |
| 39 | + for symbol in candidate_symbols: |
| 40 | + target_value = max(0.0, float(adjusted.get(symbol, 0.0) or 0.0)) |
| 41 | + price = max(0.0, float(normalized_prices.get(symbol, 0.0) or 0.0)) |
| 42 | + if price > 0.0 and 0.0 < target_value < (price * step): |
| 43 | + adjusted[symbol] = 0.0 |
| 44 | + substituted.append(symbol) |
| 45 | + return adjusted, tuple(dict.fromkeys(substituted)) |
10 | 46 |
|
11 | 47 |
|
12 | 48 | @dataclass(frozen=True) |
@@ -88,6 +124,33 @@ def _quote_price(market_data_port: MarketDataPort, symbol: str) -> float | None: |
88 | 124 | return price if price > 0 else None |
89 | 125 |
|
90 | 126 |
|
| 127 | +def _apply_small_account_whole_share_compatibility( |
| 128 | + plan: dict[str, Any], |
| 129 | + *, |
| 130 | + market_data_port: MarketDataPort, |
| 131 | +) -> dict[str, Any]: |
| 132 | + adjusted_plan = dict(plan or {}) |
| 133 | + allocation = dict(adjusted_plan.get("allocation") or {}) |
| 134 | + targets = dict(allocation.get("targets") or {}) |
| 135 | + symbols = tuple(allocation.get("strategy_symbols") or tuple(targets)) |
| 136 | + prices = {} |
| 137 | + for symbol in symbols: |
| 138 | + price = _quote_price(market_data_port, str(symbol).strip().upper()) |
| 139 | + if price is not None: |
| 140 | + prices[str(symbol).strip().upper()] = price |
| 141 | + adjusted_targets, substituted = project_unbuyable_value_targets_to_cash( |
| 142 | + targets, |
| 143 | + prices, |
| 144 | + symbols=symbols, |
| 145 | + quantity_step=1.0, |
| 146 | + ) |
| 147 | + allocation["targets"] = adjusted_targets |
| 148 | + if substituted: |
| 149 | + allocation["small_account_whole_share_substituted_symbols"] = substituted |
| 150 | + adjusted_plan["allocation"] = allocation |
| 151 | + return adjusted_plan |
| 152 | + |
| 153 | + |
91 | 154 | def _submit_order( |
92 | 155 | execution_port: ExecutionPort, |
93 | 156 | *, |
@@ -134,6 +197,10 @@ def execute_value_target_plan( |
134 | 197 | plan, |
135 | 198 | threshold_usd=safe_haven_cash_substitute_threshold_usd, |
136 | 199 | ) |
| 200 | + plan = _apply_small_account_whole_share_compatibility( |
| 201 | + plan, |
| 202 | + market_data_port=market_data_port, |
| 203 | + ) |
137 | 204 | allocation = dict(plan.get("allocation") or {}) |
138 | 205 | portfolio = dict(plan.get("portfolio") or {}) |
139 | 206 | execution = dict(plan.get("execution") or {}) |
|
0 commit comments