From ee214751dbbbe904c35e421dd56ca36e1942a58a Mon Sep 17 00:00:00 2001 From: nkaz001 Date: Thu, 18 May 2023 10:27:28 -0400 Subject: [PATCH] Fix the issue regarding order modification --- hftbacktest/__init__.py | 3 +- hftbacktest/backtest.py | 6 +-- hftbacktest/proc/local.py | 7 +-- hftbacktest/proc/nopartialfillexchange.py | 18 +++---- hftbacktest/proc/partialfillexchange.py | 59 ++++++++++++++++++----- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/hftbacktest/__init__.py b/hftbacktest/__init__.py index 6411fd9..fa07fe9 100644 --- a/hftbacktest/__init__.py +++ b/hftbacktest/__init__.py @@ -32,7 +32,7 @@ IdentityProbQueueModel as IdentityProbQueueModel_, SquareProbQueueModel as SquareProbQueueModel_ ) -from .order import BUY, SELL, NONE, NEW, EXPIRED, FILLED, CANCELED, GTC, GTX, Order, OrderBus +from .order import BUY, SELL, NONE, NEW, EXPIRED, FILLED, CANCELED, MODIFY, GTC, GTX, Order, OrderBus from .proc.local import Local from .proc.nopartialfillexchange import NoPartialFillExchange from .proc.partialfillexchange import PartialFillExchange @@ -87,6 +87,7 @@ 'EXPIRED', 'FILLED', 'CANCELED', + 'MODIFY', # Time-In-Force 'GTC', diff --git a/hftbacktest/backtest.py b/hftbacktest/backtest.py index 877aba8..10e85a8 100644 --- a/hftbacktest/backtest.py +++ b/hftbacktest/backtest.py @@ -264,9 +264,9 @@ def modify(self, order_id: int64, price: float64, qty: float64, wait: boolean = Modify the specified order. - If the adjusted total quantity(leaves_qty + executed_qty) is less than or equal to - the quantity already executed, the order will be considered expired. Be aware that this adjustment doesn't - - affect the remaining quantity in the market, it only changes the total quantity. - Modified orders will be reordered in the match queue. + the quantity already executed, the order will be considered expired. Be aware that this adjustment doesn't + affect the remaining quantity in the market, it only changes the total quantity. + - Modified orders will be reordered in the match queue. Args: order_id: Order ID to modify. diff --git a/hftbacktest/proc/local.py b/hftbacktest/proc/local.py index 0bd41c3..1cfc28a 100644 --- a/hftbacktest/proc/local.py +++ b/hftbacktest/proc/local.py @@ -137,13 +137,14 @@ def modify_order(self, order_id, price, qty, current_timestamp): if order.req != NONE: raise ValueError('the given order cannot be modified because there is a ongoing request.') + order.req = MODIFY + + order = order.copy() order.price_tick = round(price / self.depth.tick_size) order.qty = qty - order.req = MODIFY exch_recv_timestamp = current_timestamp + self.order_latency.entry(current_timestamp, order, self) - self.orders[order.order_id] = order - self.orders_to.append(order.copy(), exch_recv_timestamp) + self.orders_to.append(order, exch_recv_timestamp) def cancel(self, order_id, current_timestamp): order = self.orders.get(order_id) diff --git a/hftbacktest/proc/nopartialfillexchange.py b/hftbacktest/proc/nopartialfillexchange.py index edbf36d..21b2b94 100644 --- a/hftbacktest/proc/nopartialfillexchange.py +++ b/hftbacktest/proc/nopartialfillexchange.py @@ -77,7 +77,7 @@ def _process_recv_order(self, order, recv_timestamp, wait_resp, next_timestamp): resp_timestamp = self.__ack_new(order, recv_timestamp) # Process a modify order. - if order.req == MODIFY: + elif order.req == MODIFY: order.req = NONE resp_timestamp = self.__ack_modify(order, recv_timestamp) @@ -326,10 +326,11 @@ def __ack_modify(self, order, timestamp): if exch_order.side == BUY: # Check if the buy order price is greater than or equal to the current best ask. if exch_order.price_tick >= self.depth.best_ask_tick: + del self.buy_orders[prev_price_tick][exch_order.order_id] + del self.orders[exch_order.order_id] + if exch_order.time_in_force == GTX: exch_order.status = EXPIRED - del self.buy_orders[prev_price_tick][exch_order.order_id] - del self.orders[exch_order.order_id] else: # Take the market. return self.__fill( @@ -337,11 +338,10 @@ def __ack_modify(self, order, timestamp): timestamp, False, exec_price_tick=self.depth.best_ask_tick, - delete_order=True + delete_order=False ) else: # The exchange accepts this order. - self.orders[exch_order.order_id] = exch_order if prev_price_tick != exch_order.price_tick: del self.buy_orders[prev_price_tick][exch_order.order_id] o = self.buy_orders.setdefault( @@ -356,10 +356,11 @@ def __ack_modify(self, order, timestamp): else: # Check if the sell order price is less than or equal to the current best bid. if exch_order.price_tick <= self.depth.best_bid_tick: + del self.sell_orders[prev_price_tick][exch_order.order_id] + del self.orders[exch_order.order_id] + if exch_order.time_in_force == GTX: exch_order.status = EXPIRED - del self.sell_orders[prev_price_tick][exch_order.order_id] - del self.orders[exch_order.order_id] else: # Take the market. return self.__fill( @@ -367,11 +368,10 @@ def __ack_modify(self, order, timestamp): timestamp, False, exec_price_tick=self.depth.best_bid_tick, - delete_order=True + delete_order=False ) else: # The exchange accepts this order. - self.orders[exch_order.order_id] = order if prev_price_tick != exch_order.price_tick: del self.sell_orders[prev_price_tick][exch_order.order_id] o = self.sell_orders.setdefault( diff --git a/hftbacktest/proc/partialfillexchange.py b/hftbacktest/proc/partialfillexchange.py index 9e6c152..6be4d80 100644 --- a/hftbacktest/proc/partialfillexchange.py +++ b/hftbacktest/proc/partialfillexchange.py @@ -17,6 +17,7 @@ PARTIALLY_FILLED, MODIFY, GTX, + GTC, FOK, IOC, NONE, @@ -93,7 +94,7 @@ def _process_recv_order(self, order, recv_timestamp, wait_resp, next_timestamp): resp_timestamp = self.__ack_new(order, recv_timestamp) # Process a modify order. - if order.req == MODIFY: + elif order.req == MODIFY: order.req = NONE resp_timestamp = self.__ack_modify(order, recv_timestamp) @@ -514,22 +515,38 @@ def __ack_modify(self, order, timestamp): if exch_order.side == BUY: # Check if the buy order price is greater than or equal to the current best ask. if exch_order.price_tick >= self.depth.best_ask_tick: + del self.buy_orders[prev_price_tick][exch_order.order_id] + del self.orders[exch_order.order_id] + if exch_order.time_in_force == GTX: exch_order.status = EXPIRED - del self.buy_orders[prev_price_tick][exch_order.order_id] - del self.orders[exch_order.order_id] - else: + elif exch_order.time_in_force == GTC: # Take the market. + for t in range(self.depth.best_ask_tick, exch_order.price_tick): + exec_qty = min(self.depth.ask_depth[t], exch_order.qty) + local_recv_timestamp = self.__fill( + exch_order, + exec_qty, + timestamp, + False, + exec_price_tick=t, + delete_order=False + ) + if exch_order.status == FILLED: + return local_recv_timestamp + # The buy order cannot remain in the ask book, as it cannot affect the market depth during + # backtesting based on market-data replay. So, even though it simulates partial fill, if the order + # size is not small enough, it introduces unreality. return self.__fill( exch_order, + exch_order.leaves_qty, timestamp, False, - exec_price_tick=self.depth.best_ask_tick, - delete_order=True + exec_price_tick=exch_order.price_tick, + delete_order=False ) else: # The exchange accepts this order. - self.orders[exch_order.order_id] = exch_order if prev_price_tick != exch_order.price_tick: del self.buy_orders[prev_price_tick][exch_order.order_id] o = self.buy_orders.setdefault( @@ -544,22 +561,38 @@ def __ack_modify(self, order, timestamp): else: # Check if the sell order price is less than or equal to the current best bid. if exch_order.price_tick <= self.depth.best_bid_tick: + del self.sell_orders[prev_price_tick][exch_order.order_id] + del self.orders[exch_order.order_id] + if exch_order.time_in_force == GTX: exch_order.status = EXPIRED - del self.sell_orders[prev_price_tick][exch_order.order_id] - del self.orders[exch_order.order_id] - else: + elif exch_order.time_in_force == GTC: # Take the market. + for t in range(self.depth.best_bid_tick, exch_order.price_tick, -1): + exec_qty = min(self.depth.bid_depth[t], exch_order.qty) + local_recv_timestamp = self.__fill( + exch_order, + exec_qty, + timestamp, + False, + exec_price_tick=t, + delete_order=False + ) + if exch_order.status == FILLED: + return local_recv_timestamp + # The sell order cannot remain in the bid book, as it cannot affect the market depth during + # backtesting based on market-data replay. So, even though it simulates partial fill, if the order + # size is not small enough, it introduces unreality. return self.__fill( exch_order, + exch_order.leaves_qty, timestamp, False, - exec_price_tick=self.depth.best_bid_tick, - delete_order=True + exec_price_tick=exch_order.price_tick, + delete_order=False ) else: # The exchange accepts this order. - self.orders[exch_order.order_id] = order if prev_price_tick != exch_order.price_tick: del self.sell_orders[prev_price_tick][exch_order.order_id] o = self.sell_orders.setdefault(