Skip to content

PartialFillExchange: remove a resting order that is filled for exactly its leaves_qty - #331

Open
rux-eth wants to merge 1 commit into
nkaz001:masterfrom
rux-eth:fix/exact-fill-order-removal
Open

rux-eth wants to merge 1 commit into
nkaz001:masterfrom
rux-eth:fix/exact-fill-order-removal

Conversation

@rux-eth

@rux-eth rux-eth commented Sep 17, 2026

Copy link
Copy Markdown

What happens

In PartialFillExchange, when a trade fills a resting order for exactly its remaining
quantity, check_if_buy_filled / check_if_sell_filled take the else branch of

let exec_qty = if filled_qty > order.leaves_qty {
    self.filled_orders.push(order.order_id);
    order.leaves_qty
} else {
    filled_qty
};

(hftbacktest/src/backtest/proc/partialfillexchange.rs, two sites). fill() then sets
leaves_qty to 0 and the status to Filled, but the order is never pushed to filled_orders,
so remove_filled_orders() leaves it in self.orders and in its price-level set. The next event
that matches the level — another trade at the price, or a best-price sweep in
on_best_bid_update / on_best_ask_update — calls fill() on a Filled order, which returns
BacktestError::InvalidOrderStatus, and the elapse aborts (return code 14 through the Python
binding). The backtest cannot be resumed past that event.

Fix

Use >= at both sites so an exact fill is removed like an over-fill. Two characters; no other
behaviour changes (an exact fill already produced the Filled status and the response).

Reproduction

Real Hyperliquid L2 + tape data (2025-10-10), HashMapMarketDepthBacktest, RiskAdverseQueueModel,
PartialFillExchange, constant order latency: with one-lot post-only orders across 182 assets the
abort fires about once per 20,000 orders. A minimal synthetic case: a resting bid of 1.0 at a price
with 2.0 displayed ahead of it, then a sell trade of 3.0 at that price (fills the 2.0 ahead and
exactly the 1.0), then any later trade at the same price → InvalidOrderStatus.

Notes

  • Found while building a research harness on 2.4.4 (py-v2.4.4); the fix branch is one commit on
    top of that tag. Happy to rebase onto master.
  • The Filled-status guard in fill() is what surfaces the stale order; the guard is right, the
    bookkeeping before it is what this PR corrects.

…aves_qty

When a trade fills a resting order for exactly its remaining quantity, the
comparison 'filled_qty > order.leaves_qty' takes the else branch: fill()
sets leaves_qty to 0 and the status to Filled, but the order is never pushed
to filled_orders, so it stays in 'orders' and in its price-level set. The next
event that matches the level (another trade at the price, or a best-price
sweep in on_best_bid_update / on_best_ask_update) calls fill() on a Filled
order, which returns InvalidOrderStatus and aborts the elapse.

Use '>=' so an exact fill is removed like an over-fill. Reproduced on real
Hyperliquid L2 + tape data with one-lot post-only orders (about 1 hit per
20k orders); a minimal case is a resting bid of 1.0 behind 2.0 of displayed
depth, then a 3.0 sell print at the price followed by any later print at the
same price.
@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity · -17 duplication

Metric Results
Complexity 0
Duplication -17

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant