Skip to content

Commit 4f4e018

Browse files
committed
Apply small-account whole-share compatibility
1 parent 64397fb commit 4f4e018

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

application/execution_service.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@
77

88
from quant_platform_kit.common.models import OrderIntent
99
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))
1046

1147

1248
@dataclass(frozen=True)
@@ -88,6 +124,33 @@ def _quote_price(market_data_port: MarketDataPort, symbol: str) -> float | None:
88124
return price if price > 0 else None
89125

90126

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+
91154
def _submit_order(
92155
execution_port: ExecutionPort,
93156
*,
@@ -134,6 +197,10 @@ def execute_value_target_plan(
134197
plan,
135198
threshold_usd=safe_haven_cash_substitute_threshold_usd,
136199
)
200+
plan = _apply_small_account_whole_share_compatibility(
201+
plan,
202+
market_data_port=market_data_port,
203+
)
137204
allocation = dict(plan.get("allocation") or {})
138205
portfolio = dict(plan.get("portfolio") or {})
139206
execution = dict(plan.get("execution") or {})

tests/test_execution_service.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,35 @@ def test_execute_value_target_plan_leaves_small_safe_haven_target_as_cash():
152152
threshold_usd=1000.0,
153153
)
154154
assert adjusted_plan["allocation"]["targets"]["BOXX"] == 0.0
155+
156+
157+
def test_execute_value_target_plan_projects_unbuyable_value_target_to_zero():
158+
execution_port = FakeExecutionPort()
159+
result = execute_value_target_plan(
160+
plan={
161+
"allocation": {
162+
"strategy_symbols": ("SOXL", "SOXX", "BOXX"),
163+
"risk_symbols": ("SOXL", "SOXX"),
164+
"safe_haven_symbols": ("BOXX",),
165+
"targets": {"SOXL": 541.58, "SOXX": 154.74, "BOXX": 77.37},
166+
},
167+
"portfolio": {
168+
"market_values": {"SOXL": 0.0, "SOXX": 536.88, "BOXX": 0.0},
169+
"sellable_quantities": {"SOXX": 1.0},
170+
"liquid_cash": 236.81,
171+
"cash_sweep_symbol": "BOXX",
172+
},
173+
"execution": {"current_min_trade": 7.74, "investable_cash": 213.60},
174+
},
175+
market_data_port=FakeMarketDataPort({"SOXL": 191.15, "SOXX": 536.88, "BOXX": 100.0}),
176+
execution_port=execution_port,
177+
dry_run_only=True,
178+
max_order_notional_usd=1000.0,
179+
safe_haven_cash_substitute_threshold_usd=1000.0,
180+
)
181+
182+
assert result.action_done is True
183+
assert [(order.side, order.symbol, order.quantity) for order in execution_port.orders] == [
184+
("sell", "SOXX", 1.0),
185+
("buy", "SOXL", 1.0),
186+
]

0 commit comments

Comments
 (0)