Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@

from quant_platform_kit.common.models import OrderIntent
from quant_platform_kit.common.ports import ExecutionPort, MarketDataPort
try:
from quant_platform_kit.common.small_account_compatibility import (
project_unbuyable_value_targets_to_cash,
)
except ImportError: # pragma: no cover - compatibility with older pinned shared wheels
def project_unbuyable_value_targets_to_cash(
target_values,
prices,
*,
symbols=None,
quantity_step=1.0,
):
adjusted = {
str(symbol or "").strip().upper(): float(value or 0.0)
for symbol, value in dict(target_values or {}).items()
}
step = max(0.0, float(quantity_step or 0.0))
if step <= 0.0:
return adjusted, ()
candidate_symbols = (
tuple(adjusted)
if symbols is None
else tuple(dict.fromkeys(str(symbol or "").strip().upper() for symbol in symbols))
)
normalized_prices = {
str(symbol or "").strip().upper(): float(price or 0.0)
for symbol, price in dict(prices or {}).items()
}
substituted = []
for symbol in candidate_symbols:
target_value = max(0.0, float(adjusted.get(symbol, 0.0) or 0.0))
price = max(0.0, float(normalized_prices.get(symbol, 0.0) or 0.0))
if price > 0.0 and 0.0 < target_value < (price * step):
adjusted[symbol] = 0.0
substituted.append(symbol)
return adjusted, tuple(dict.fromkeys(substituted))


@dataclass(frozen=True)
Expand Down Expand Up @@ -88,6 +124,43 @@ def _quote_price(market_data_port: MarketDataPort, symbol: str) -> float | None:
return price if price > 0 else None


def _apply_small_account_whole_share_compatibility(
plan: dict[str, Any],
*,
market_data_port: MarketDataPort,
) -> dict[str, Any]:
adjusted_plan = dict(plan or {})
allocation = dict(adjusted_plan.get("allocation") or {})
targets = dict(allocation.get("targets") or {})
candidate_symbols = tuple(
dict.fromkeys(
tuple(allocation.get("risk_symbols", ()))
+ tuple(allocation.get("income_symbols", ()))
)
)
if not candidate_symbols:
safe_haven_symbols = set(allocation.get("safe_haven_symbols", ()))
candidate_symbols = tuple(
symbol for symbol in targets if symbol not in safe_haven_symbols
)
prices = {}
for symbol in candidate_symbols:
price = _quote_price(market_data_port, str(symbol).strip().upper())
if price is not None:
prices[str(symbol).strip().upper()] = price
adjusted_targets, substituted = project_unbuyable_value_targets_to_cash(
targets,
prices,
symbols=candidate_symbols,
quantity_step=1.0,
)
allocation["targets"] = adjusted_targets
if substituted:
allocation["small_account_whole_share_substituted_symbols"] = substituted
adjusted_plan["allocation"] = allocation
return adjusted_plan


def _submit_order(
execution_port: ExecutionPort,
*,
Expand Down Expand Up @@ -134,6 +207,10 @@ def execute_value_target_plan(
plan,
threshold_usd=safe_haven_cash_substitute_threshold_usd,
)
plan = _apply_small_account_whole_share_compatibility(
plan,
market_data_port=market_data_port,
)
Comment on lines +200 to +203

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate whole-share-adjusted targets to caller-visible plan

Apply the whole-share compatibility layer before execution result reporting, not only inside execute_value_target_plan: these lines mutate plan locally for order generation, but run_strategy_cycle still reports allocation from the original pre-execution plan (application/rebalance_service.py uses its outer plan at result assembly). When a positive target is projected to zero here, submitted orders follow the adjusted targets while API/notification output still shows the old nonzero target, creating inconsistent and misleading cycle results.

Useful? React with 👍 / 👎.

allocation = dict(plan.get("allocation") or {})
portfolio = dict(plan.get("portfolio") or {})
execution = dict(plan.get("execution") or {})
Expand Down
32 changes: 32 additions & 0 deletions tests/test_execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,35 @@ def test_execute_value_target_plan_leaves_small_safe_haven_target_as_cash():
threshold_usd=1000.0,
)
assert adjusted_plan["allocation"]["targets"]["BOXX"] == 0.0


def test_execute_value_target_plan_projects_unbuyable_value_target_to_zero():
execution_port = FakeExecutionPort()
result = execute_value_target_plan(
plan={
"allocation": {
"strategy_symbols": ("SOXL", "SOXX", "BOXX"),
"risk_symbols": ("SOXL", "SOXX"),
"safe_haven_symbols": ("BOXX",),
"targets": {"SOXL": 541.58, "SOXX": 154.74, "BOXX": 77.37},
},
"portfolio": {
"market_values": {"SOXL": 0.0, "SOXX": 536.88, "BOXX": 0.0},
"sellable_quantities": {"SOXX": 1.0},
"liquid_cash": 236.81,
"cash_sweep_symbol": "BOXX",
},
"execution": {"current_min_trade": 7.74, "investable_cash": 213.60},
},
market_data_port=FakeMarketDataPort({"SOXL": 191.15, "SOXX": 536.88, "BOXX": 100.0}),
execution_port=execution_port,
dry_run_only=True,
max_order_notional_usd=1000.0,
safe_haven_cash_substitute_threshold_usd=1000.0,
)

assert result.action_done is True
assert [(order.side, order.symbol, order.quantity) for order in execution_port.orders] == [
("sell", "SOXX", 1.0),
("buy", "SOXL", 1.0),
]