-
Notifications
You must be signed in to change notification settings - Fork 707
Description
Description
When running a simple market-making strategy with partial_fill_exchange, the backtest stops prematurely. hbt.elapse() returns status code 14 (InvalidOrderStatus).
The same strategy runs to completion without any issues when switching to no_partial_fill_exchange.
I have inspected the orders immediately before the error is returned, and everything appears consistent (no duplicated order IDs, no invalid price/quantity). This leads me to suspect the issue is related to how partially filled orders are handled.
Expected behavior
The backtest should continue running without returning InvalidOrderStatus, as it does when using no_partial_fill_exchange.
Actual behavior
The backtest stops early and hbt.elapse() returns status code 14.
Reproduction
The issue reproduces reliably with the following minimal example. I have tried multiple days with BTCUSDT-PERP data on Binance and always get the error after a few hundred trades.
(Note: cancellations and quoting on both sides are not logically required here; they are only used to generate many orders quickly.)
@njit
def simple_mm(hbt, recorder, order_qty=0.1):
asset_no = 0
tick_size = hbt.depth(asset_no).tick_size
order_id = 0
while True:
elapse_status = hbt.elapse(100_000_000)
if elapse_status != 0:
print(f'elapse_status: {elapse_status}')
break
hbt.clear_last_trades(asset_no)
hbt.clear_inactive_orders(asset_no)
depth = hbt.depth(asset_no)
orders = hbt.orders(asset_no)
best_bid_tick = depth.best_bid_tick
best_ask_tick = depth.best_ask_tick
bid_price = best_bid_tick * tick_size
ask_price = best_ask_tick * tick_size
is_active_bid = False
is_active_ask = False
order_values = orders.values()
while order_values.has_next():
order = order_values.get()
if order.side == BUY:
is_active_bid = True
elif order.side == SELL:
is_active_ask = True
if order.cancellable:
hbt.cancel(asset_no, order.order_id, False)
if not is_active_bid and np.isfinite(bid_price):
hbt.submit_buy_order(asset_no, order_id, bid_price, order_qty, GTX, LIMIT, False)
order_id += 1
if not is_active_ask and np.isfinite(ask_price):
hbt.submit_sell_order(asset_no, order_id, ask_price, order_qty, GTX, LIMIT, False)
order_id += 1
recorder.record(hbt)
Backtest setup:
asset = (
BacktestAsset()
.data([event_data])
.initial_snapshot(initial_snapshot)
.linear_asset(contract_size)
.constant_order_latency(10_000_000, 10_000_000)
.risk_adverse_queue_model()
.partial_fill_exchange()
.trading_value_fee_model(maker_fee, taker_fee)
.tick_size(tick_size)
.lot_size(lot_size)
.last_trades_capacity(10_000)
.roi_lb(px_lb_tick * tick_size)
.roi_ub(px_ub_tick * tick_size)
)
hbt = ROIVectorMarketDepthBacktest([asset])
recorder = Recorder(1, 5_000_000)
simple_mm(hbt, recorder.recorder, order_qty=0.01)
_ = hbt.close()
stats = LinearAssetRecord(recorder.get(0)).stats(book_size=100_000)
Switching partial_fill_exchange() to no_partial_fill_exchange() makes the issue disappear.