Skip to content

Commit 79ceae5

Browse files
committed
Use sellable quantity for Firstrade stale sell values
1 parent e4906b4 commit 79ceae5

2 files changed

Lines changed: 47 additions & 1 deletion

File tree

application/execution_service.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ def _floor_quantity(quantity: float) -> int:
2323
return max(0, int(float(quantity or 0.0)))
2424

2525

26+
def _sell_budget(
27+
*,
28+
delta_value: float,
29+
target_value: float,
30+
sellable_quantity: float,
31+
price: float,
32+
order_notional_cap: float,
33+
) -> float:
34+
sellable_notional = max(0.0, float(sellable_quantity or 0.0)) * max(0.0, float(price or 0.0))
35+
if sellable_notional <= 0.0:
36+
return 0.0
37+
value_delta_budget = max(0.0, abs(float(delta_value or 0.0)))
38+
position_budget = max(0.0, sellable_notional - max(0.0, float(target_value or 0.0)))
39+
return min(max(value_delta_budget, position_budget), sellable_notional, max(0.0, float(order_notional_cap or 0.0)))
40+
41+
2642
def _safe_haven_cash_symbols(*, portfolio: dict[str, Any], allocation: dict[str, Any]) -> tuple[str, ...]:
2743
symbols: list[str] = []
2844
for symbol in allocation.get("safe_haven_symbols", ()):
@@ -167,7 +183,13 @@ def execute_value_target_plan(
167183
for symbol, delta_value, price in [item for item in tradable_deltas if item[1] < 0]:
168184
if delta_value < 0:
169185
sellable = sellable_quantities.get(symbol, 0.0)
170-
sell_budget = min(abs(delta_value), sellable * price, order_notional_cap)
186+
sell_budget = _sell_budget(
187+
delta_value=delta_value,
188+
target_value=targets.get(symbol, 0.0),
189+
sellable_quantity=sellable,
190+
price=price,
191+
order_notional_cap=order_notional_cap,
192+
)
171193
quantity = _floor_quantity(sell_budget / price)
172194
if quantity <= 0:
173195
skipped.append(

tests/test_execution_service.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ def test_execute_value_target_plan_sells_before_buys_and_caps_order_notional():
6969
assert all(order.metadata["max_notional_usd"] == 25.0 for order in execution_port.orders)
7070

7171

72+
def test_execute_value_target_plan_uses_sellable_quantity_when_market_value_is_stale_below_quote():
73+
execution_port = FakeExecutionPort()
74+
result = execute_value_target_plan(
75+
plan={
76+
"allocation": {"targets": {"SOXL": 0.0}},
77+
"portfolio": {
78+
"market_values": {"SOXL": 524.10},
79+
"sellable_quantities": {"SOXL": 3.0},
80+
"liquid_cash": 0.0,
81+
},
82+
"execution": {"current_min_trade": 5.0, "investable_cash": 0.0},
83+
},
84+
market_data_port=FakeMarketDataPort({"SOXL": 175.42}),
85+
execution_port=execution_port,
86+
dry_run_only=True,
87+
max_order_notional_usd=1000.0,
88+
)
89+
90+
assert result.action_done is True
91+
assert [(order.side, order.symbol, order.quantity) for order in execution_port.orders] == [
92+
("sell", "SOXL", 3.0),
93+
]
94+
95+
7296
def test_execute_value_target_plan_skips_when_cap_cannot_buy_one_share():
7397
execution_port = FakeExecutionPort()
7498
result = execute_value_target_plan(

0 commit comments

Comments
 (0)