Skip to content

Backtest stops with InvalidOrderStatus (14) when using partial_fill_exchange #301

@Robin-Guilliou

Description

@Robin-Guilliou

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions