Skip to content

Commit db883ca

Browse files
committed
fix(live): honor terminal exchange fill status
1 parent ba324e1 commit db883ca

2 files changed

Lines changed: 61 additions & 3 deletions

File tree

backend_api_python/app/services/pending_order_worker.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@
110110
ALPACA_FILL_DELTA_EPSILON = 1e-8
111111

112112

113+
def _is_final_fill(
114+
*,
115+
requested: float,
116+
filled: float,
117+
avg_price: float,
118+
status: Any = "",
119+
) -> bool:
120+
requested_qty = max(0.0, float(requested or 0.0))
121+
filled_qty = max(0.0, float(filled or 0.0))
122+
if requested_qty <= 0 or filled_qty <= 0 or float(avg_price or 0.0) <= 0:
123+
return False
124+
if normalize_live_order_status(status) == "filled":
125+
return True
126+
return filled_qty >= requested_qty * 0.999999
127+
128+
113129
def _broker_order_type(payload: Dict[str, Any], ref_price: float) -> Tuple[str, float]:
114130
order_type = str(payload.get("order_type") or "market").strip().lower()
115131
if order_type == "maker_then_market":
@@ -2174,7 +2190,12 @@ def _execute_live_order(self, *, order_id: int, order_row: Dict[str, Any], paylo
21742190
filled=filled,
21752191
avg_price=avg_price,
21762192
executed_at=executed_at if filled > 0 else None,
2177-
final_filled=bool(amount > 0 and filled >= amount * 0.999999),
2193+
final_filled=_is_final_fill(
2194+
requested=amount,
2195+
filled=filled,
2196+
avg_price=avg_price,
2197+
status=execution_result.status,
2198+
),
21782199
)
21792200
_console_print(f"[worker] order sent: strategy_id={strategy_id} pending_id={order_id} exchange={res.exchange_id} order_id={res.exchange_order_id} filled={filled} avg={avg_price}")
21802201
except Exception as e:
@@ -2371,7 +2392,12 @@ def _execute_ibkr_order(
23712392
filled=filled,
23722393
avg_price=avg_price,
23732394
executed_at=executed_at if filled > 0 else None,
2374-
final_filled=bool(amount > 0 and filled >= amount * 0.999999 and avg_price > 0),
2395+
final_filled=_is_final_fill(
2396+
requested=amount,
2397+
filled=filled,
2398+
avg_price=avg_price,
2399+
status=result.status,
2400+
),
23752401
)
23762402
_console_print(f"[worker] IBKR order sent: strategy_id={strategy_id} pending_id={order_id} order_id={exchange_order_id} filled={filled} avg={avg_price}")
23772403

@@ -2549,7 +2575,12 @@ def _execute_alpaca_order(
25492575
filled=filled,
25502576
avg_price=avg_price,
25512577
executed_at=executed_at if filled > 0 else None,
2552-
final_filled=bool(amount > 0 and filled >= amount * 0.999999 and avg_price > 0),
2578+
final_filled=_is_final_fill(
2579+
requested=amount,
2580+
filled=filled,
2581+
avg_price=avg_price,
2582+
status=result.status,
2583+
),
25532584
)
25542585
_console_print(
25552586
f"[worker] Alpaca order sent: strategy_id={strategy_id} pending_id={order_id} "

backend_api_python/tests/test_pending_order_worker_live_sync.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,33 @@ def test_live_sent_sync_keeps_terminal_fill_open_when_average_price_is_missing(m
141141
assert snapshots[0]["exchange_status"] == "fill_price_missing"
142142

143143

144+
def test_exchange_filled_status_accepts_quantity_precision_remainder():
145+
assert worker_module._is_final_fill(
146+
requested=0.1617456789,
147+
filled=0.161745,
148+
avg_price=58_705.0,
149+
status="FILLED",
150+
) is True
151+
152+
153+
def test_non_terminal_partial_fill_remains_open():
154+
assert worker_module._is_final_fill(
155+
requested=1.0,
156+
filled=0.75,
157+
avg_price=58_705.0,
158+
status="PARTIALLY_FILLED",
159+
) is False
160+
161+
162+
def test_terminal_fill_without_average_price_remains_open():
163+
assert worker_module._is_final_fill(
164+
requested=1.0,
165+
filled=1.0,
166+
avg_price=0.0,
167+
status="FILLED",
168+
) is False
169+
170+
144171
def test_ibkr_submission_never_fabricates_a_fill_from_requested_amount(monkeypatch):
145172
class Result:
146173
success = True

0 commit comments

Comments
 (0)