class OrderBook(TaskManager): """ OrderBook is used for searching through all the orders and giving an indication to the user of what other offers are out there. """ def __init__(self, message_repository): super(OrderBook, self).__init__() self._logger = logging.getLogger(self.__class__.__name__) assert isinstance(message_repository, MessageRepository), type(message_repository) self.message_repository = message_repository self._bids = Side() self._asks = Side() def timeout_ask(self, order_id): ask = self.get_ask(order_id).tick self.remove_tick(order_id) return ask def timeout_bid(self, order_id): bid = self.get_bid(order_id).tick self.remove_tick(order_id) return bid def on_timeout_error(self, _): pass def on_invalid_tick_insert(self, _): self._logger.warning("Invalid tick inserted in order book.") def insert_ask(self, ask): """ :type ask: Ask """ assert isinstance(ask, Ask), type(ask) if not self._asks.tick_exists(ask.order_id) and ask.is_valid(): self._asks.insert_tick(ask) timeout_delay = float(ask.timestamp) + float( ask.timeout) - time.time() task = deferLater(reactor, timeout_delay, self.timeout_ask, ask.order_id) self.register_task("ask_%s_timeout" % ask.order_id, task) return task.addErrback(self.on_timeout_error) return fail(Failure(RuntimeError("ask invalid"))).addErrback( self.on_invalid_tick_insert) def remove_ask(self, order_id): """ :type order_id: OrderId """ assert isinstance(order_id, OrderId), type(order_id) if self._asks.tick_exists(order_id): self.cancel_pending_task("ask_%s_timeout" % order_id) self._asks.remove_tick(order_id) def insert_bid(self, bid): """ :type bid: Bid """ assert isinstance(bid, Bid), type(bid) if not self._bids.tick_exists(bid.order_id) and bid.is_valid(): self._bids.insert_tick(bid) timeout_delay = float(bid.timestamp) + float( bid.timeout) - time.time() task = deferLater(reactor, timeout_delay, self.timeout_bid, bid.order_id) self.register_task("bid_%s_timeout" % bid.order_id, task) return task.addErrback(self.on_timeout_error) return fail(Failure(RuntimeError("bid invalid"))).addErrback( self.on_invalid_tick_insert) def remove_bid(self, order_id): """ :type order_id: OrderId """ assert isinstance(order_id, OrderId), type(order_id) if self._bids.tick_exists(order_id): self.cancel_pending_task("bid_%s_timeout" % order_id) self._bids.remove_tick(order_id) def trade_tick(self, order_id, recipient_order_id, quantity, end_transaction_timestamp): """ :type order_id: OrderId :type recipient_order_id: OrderId :type quantity: Quantity :type end_transaction_timestamp: Timestamp """ assert isinstance(order_id, OrderId), type(order_id) assert isinstance(recipient_order_id, OrderId), type(recipient_order_id) assert isinstance(quantity, Quantity), type(quantity) self._logger.debug( "Trading tick in order book for own order %s vs order %s (quantity: %s)", str(order_id), str(recipient_order_id), str(quantity)) if self.tick_exists(order_id): tick = self.get_tick(order_id) tick.quantity -= quantity if self.tick_exists(recipient_order_id): tick = self.get_tick(recipient_order_id) if tick.tick.timestamp < end_transaction_timestamp: tick.quantity -= quantity def tick_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the tick exists, False otherwise :rtype: bool """ assert isinstance(order_id, OrderId), type(order_id) is_ask = self._asks.tick_exists(order_id) is_bid = self._bids.tick_exists(order_id) return is_ask or is_bid def get_ask(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ assert isinstance(order_id, OrderId), type(order_id) return self._asks.get_tick(order_id) def get_bid(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ assert isinstance(order_id, OrderId), type(order_id) return self._bids.get_tick(order_id) def get_tick(self, order_id): """ Return a tick with the specified order id. :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ assert isinstance(order_id, OrderId), type(order_id) return self._bids.get_tick(order_id) or self._asks.get_tick(order_id) def ask_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the ask exists, False otherwise :rtype: bool """ assert isinstance(order_id, OrderId), type(order_id) return self._asks.tick_exists(order_id) def bid_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the bid exists, False otherwise :rtype: bool """ assert isinstance(order_id, OrderId), type(order_id) return self._bids.tick_exists(order_id) def remove_tick(self, order_id): """ :type order_id: OrderId """ assert isinstance(order_id, OrderId), type(order_id) self.remove_ask(order_id) self.remove_bid(order_id) @property def asks(self): """ Return the asks side :rtype: Side """ return self._asks @property def bids(self): """ Return the bids side :rtype: Side """ return self._bids def get_bid_price(self, price_wallet_id, quantity_wallet_id): """ Return the price an ask needs to have to make a trade :rtype: Price """ return self._bids.get_max_price(price_wallet_id, quantity_wallet_id) def get_ask_price(self, price_wallet_id, quantity_wallet_id): """ Return the price a bid needs to have to make a trade :rtype: Price """ return self._asks.get_min_price(price_wallet_id, quantity_wallet_id) def get_bid_ask_spread(self, price_wallet_id, quantity_wallet_id): """ Return the spread between the bid and the ask price :rtype: Price """ return self.get_ask_price(price_wallet_id, quantity_wallet_id) - \ self.get_bid_price(price_wallet_id, quantity_wallet_id) def get_mid_price(self, price_wallet_id, quantity_wallet_id): """ Return the price in between the bid and the ask price :rtype: Price """ ask_price = int(self.get_ask_price(price_wallet_id, quantity_wallet_id)) bid_price = int(self.get_bid_price(price_wallet_id, quantity_wallet_id)) return Price((ask_price + bid_price) / 2, price_wallet_id) def bid_side_depth(self, price): """ Return the depth of the price level with the given price on the bid side :param price: The price for the price level :type price: Price :return: The depth at that price level :rtype: Quantity """ assert isinstance(price, Price), type(price) return self._bids.get_price_level(price).depth def ask_side_depth(self, price): """ Return the depth of the price level with the given price on the ask side :param price: The price for the price level :type price: Price :return: The depth at that price level :rtype: Quantity """ assert isinstance(price, Price), type(price) return self._asks.get_price_level(price).depth def get_bid_side_depth_profile(self, price_wallet_id, quantity_wallet_id): """ format: [(<price>, <depth>), (<price>, <depth>), ...] :return: The depth profile :rtype: list """ profile = [] for key, value in self._bids.get_price_level_list( price_wallet_id, quantity_wallet_id).items(): profile.append((key, value.depth)) return profile def get_ask_side_depth_profile(self, price_wallet_id, quantity_wallet_id): """ format: [(<price>, <depth>), (<price>, <depth>), ...] :return: The depth profile :rtype: list """ profile = [] for key, value in self._asks.get_price_level_list( price_wallet_id, quantity_wallet_id).items(): profile.append((key, value.depth)) return profile def bid_relative_price(self, price): """ :param price: The price to be relative to :type price: Price :return: The relative price :rtype: Price """ assert isinstance(price, Price), type(price) return self.get_bid_price('BTC', 'MC') - price def ask_relative_price(self, price): """ :param price: The price to be relative to :type price: Price :return: The relative price :rtype: Price """ assert isinstance(price, Price), type(price) return self.get_ask_price('BTC', 'MC') - price def relative_tick_price(self, tick): """ :param tick: The tick with the price to be relative to :type tick: Tick :return: The relative price :rtype: Price """ assert isinstance(tick, Tick), type(tick) if tick.is_ask(): return self.ask_relative_price(tick.price) else: return self.bid_relative_price(tick.price) def get_bid_price_level(self, price_wallet_id, quantity_wallet_id): """ Return the price level that an ask has to match to make a trade :rtype: PriceLevel """ return self._bids.get_max_price_list(price_wallet_id, quantity_wallet_id) def get_ask_price_level(self, price_wallet_id, quantity_wallet_id): """ Return the price level that a bid has to match to make a trade :rtype: PriceLevel """ return self._asks.get_min_price_list(price_wallet_id, quantity_wallet_id) def get_order_ids(self): """ Return all IDs of the orders in the orderbook, both asks and bids. The returned list is sorted. :rtype: [OrderId] """ ids = [] for price_wallet_id, quantity_wallet_id in self.asks.get_price_level_list_wallets( ): for _, price_level in self.asks.get_price_level_list( price_wallet_id, quantity_wallet_id).items(): for ask in price_level: ids.append(ask.tick.order_id) for price_wallet_id, quantity_wallet_id in self.bids.get_price_level_list_wallets( ): for _, price_level in self.bids.get_price_level_list( price_wallet_id, quantity_wallet_id).items(): for bid in price_level: ids.append(bid.tick.order_id) return sorted(ids) def __str__(self): res_str = '' res_str += "------ Bids -------\n" for price_wallet_id, quantity_wallet_id in self.bids.get_price_level_list_wallets( ): for _, value in self._bids.get_price_level_list( price_wallet_id, quantity_wallet_id).items(reverse=True): res_str += '%s' % value res_str += "\n------ Asks -------\n" for price_wallet_id, quantity_wallet_id in self.asks.get_price_level_list_wallets( ): for _, value in self._asks.get_price_level_list( price_wallet_id, quantity_wallet_id).items(): res_str += '%s' % value res_str += "\n" return res_str
class OrderBook(TaskManager): """ OrderBook is used for searching through all the orders and giving an indication to the user of what other offers are out there. """ def __init__(self): super(OrderBook, self).__init__() self._logger = logging.getLogger(self.__class__.__name__) self._bids = Side() self._asks = Side() self.completed_orders = [] def timeout_ask(self, order_id): ask = self.get_ask(order_id).tick self.remove_tick(order_id) return ask def timeout_bid(self, order_id): bid = self.get_bid(order_id).tick self.remove_tick(order_id) return bid def on_timeout_error(self, _): pass def on_invalid_tick_insert(self, _): self._logger.warning("Invalid tick inserted in order book.") def insert_ask(self, ask): """ :type ask: Ask """ assert isinstance(ask, Ask), type(ask) if not self._asks.tick_exists(ask.order_id) and ask.order_id not in self.completed_orders and ask.is_valid(): self._asks.insert_tick(ask) timeout_delay = float(ask.timestamp) + float(ask.timeout) - time.time() task = deferLater(reactor, timeout_delay, self.timeout_ask, ask.order_id) self.register_task("ask_%s_timeout" % ask.order_id, task) return task.addErrback(self.on_timeout_error) return fail(Failure(RuntimeError("ask invalid"))).addErrback(self.on_invalid_tick_insert) def remove_ask(self, order_id): """ :type order_id: OrderId """ assert isinstance(order_id, OrderId), type(order_id) if self._asks.tick_exists(order_id): self.cancel_pending_task("ask_%s_timeout" % order_id) self._asks.remove_tick(order_id) def insert_bid(self, bid): """ :type bid: Bid """ assert isinstance(bid, Bid), type(bid) if not self._bids.tick_exists(bid.order_id) and bid.order_id not in self.completed_orders and bid.is_valid(): self._bids.insert_tick(bid) timeout_delay = float(bid.timestamp) + float(bid.timeout) - time.time() task = deferLater(reactor, timeout_delay, self.timeout_bid, bid.order_id) self.register_task("bid_%s_timeout" % bid.order_id, task) return task.addErrback(self.on_timeout_error) return fail(Failure(RuntimeError("bid invalid"))).addErrback(self.on_invalid_tick_insert) def remove_bid(self, order_id): """ :type order_id: OrderId """ assert isinstance(order_id, OrderId), type(order_id) if self._bids.tick_exists(order_id): self.cancel_pending_task("bid_%s_timeout" % order_id) self._bids.remove_tick(order_id) def update_ticks(self, ask_order_dict, bid_order_dict, traded_quantity, unreserve=True): """ Update ticks according to a TradeChain block containing the status of the ask/bid orders. :type ask_order_dict: dict :type bid_order_dict: dict :type traded_quantity: Quantity :type unreserve: bool """ assert isinstance(ask_order_dict, dict), type(ask_order_dict) assert isinstance(bid_order_dict, dict), type(bid_order_dict) assert isinstance(traded_quantity, Quantity), type(traded_quantity) assert isinstance(unreserve, bool), type(unreserve) ask_order_id = OrderId(TraderId(ask_order_dict["trader_id"]), OrderNumber(ask_order_dict["order_number"])) bid_order_id = OrderId(TraderId(bid_order_dict["trader_id"]), OrderNumber(bid_order_dict["order_number"])) self._logger.debug("Updating ticks in order book: %s and %s (traded quantity: %s)", str(ask_order_id), str(bid_order_id), str(traded_quantity)) # Update ask tick new_ask_quantity = Quantity(ask_order_dict["quantity"] - ask_order_dict["traded_quantity"], ask_order_dict["quantity_type"]) if self.tick_exists(ask_order_id) and new_ask_quantity <= self.get_tick(ask_order_id).quantity: tick = self.get_tick(ask_order_id) tick.quantity = new_ask_quantity if unreserve: tick.release_for_matching(traded_quantity) if tick.quantity <= Quantity(0, ask_order_dict["quantity_type"]): self.remove_tick(tick.order_id) self.completed_orders.append(tick.order_id) elif not self.tick_exists(ask_order_id) and new_ask_quantity > Quantity(0, ask_order_dict["quantity_type"]): ask = Ask(ask_order_id, Price(ask_order_dict["price"], ask_order_dict["price_type"]), new_ask_quantity, Timeout(ask_order_dict["timeout"]), Timestamp(ask_order_dict["timestamp"])) self.insert_ask(ask) # Update bid tick new_bid_quantity = Quantity(bid_order_dict["quantity"] - bid_order_dict["traded_quantity"], bid_order_dict["quantity_type"]) if self.tick_exists(bid_order_id) and new_bid_quantity <= self.get_tick(bid_order_id).quantity: tick = self.get_tick(bid_order_id) tick.quantity = new_bid_quantity if unreserve: tick.release_for_matching(traded_quantity) if tick.quantity <= Quantity(0, bid_order_dict["quantity_type"]): self.remove_tick(tick.order_id) self.completed_orders.append(tick.order_id) elif not self.tick_exists(bid_order_id) and new_bid_quantity > Quantity(0, bid_order_dict["quantity_type"]): bid = Bid(bid_order_id, Price(bid_order_dict["price"], bid_order_dict["price_type"]), new_bid_quantity, Timeout(bid_order_dict["timeout"]), Timestamp(bid_order_dict["timestamp"])) self.insert_bid(bid) def tick_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the tick exists, False otherwise :rtype: bool """ assert isinstance(order_id, OrderId), type(order_id) is_ask = self._asks.tick_exists(order_id) is_bid = self._bids.tick_exists(order_id) return is_ask or is_bid def get_ask(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ assert isinstance(order_id, OrderId), type(order_id) return self._asks.get_tick(order_id) def get_bid(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ assert isinstance(order_id, OrderId), type(order_id) return self._bids.get_tick(order_id) def get_tick(self, order_id): """ Return a tick with the specified order id. :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ assert isinstance(order_id, OrderId), type(order_id) return self._bids.get_tick(order_id) or self._asks.get_tick(order_id) def ask_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the ask exists, False otherwise :rtype: bool """ assert isinstance(order_id, OrderId), type(order_id) return self._asks.tick_exists(order_id) def bid_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the bid exists, False otherwise :rtype: bool """ assert isinstance(order_id, OrderId), type(order_id) return self._bids.tick_exists(order_id) def remove_tick(self, order_id): """ :type order_id: OrderId """ assert isinstance(order_id, OrderId), type(order_id) self._logger.debug("Removing tick %s from order book", order_id) self.remove_ask(order_id) self.remove_bid(order_id) @property def asks(self): """ Return the asks side :rtype: Side """ return self._asks @property def bids(self): """ Return the bids side :rtype: Side """ return self._bids def get_bid_price(self, price_wallet_id, quantity_wallet_id): """ Return the price an ask needs to have to make a trade :rtype: Price """ return self._bids.get_max_price(price_wallet_id, quantity_wallet_id) def get_ask_price(self, price_wallet_id, quantity_wallet_id): """ Return the price a bid needs to have to make a trade :rtype: Price """ return self._asks.get_min_price(price_wallet_id, quantity_wallet_id) def get_bid_ask_spread(self, price_wallet_id, quantity_wallet_id): """ Return the spread between the bid and the ask price :rtype: Price """ return self.get_ask_price(price_wallet_id, quantity_wallet_id) - \ self.get_bid_price(price_wallet_id, quantity_wallet_id) def get_mid_price(self, price_wallet_id, quantity_wallet_id): """ Return the price in between the bid and the ask price :rtype: Price """ ask_price = int(self.get_ask_price(price_wallet_id, quantity_wallet_id)) bid_price = int(self.get_bid_price(price_wallet_id, quantity_wallet_id)) return Price((ask_price + bid_price) / 2, price_wallet_id) def bid_side_depth(self, price): """ Return the depth of the price level with the given price on the bid side :param price: The price for the price level :type price: Price :return: The depth at that price level :rtype: Quantity """ assert isinstance(price, Price), type(price) return self._bids.get_price_level(price).depth def ask_side_depth(self, price): """ Return the depth of the price level with the given price on the ask side :param price: The price for the price level :type price: Price :return: The depth at that price level :rtype: Quantity """ assert isinstance(price, Price), type(price) return self._asks.get_price_level(price).depth def get_bid_side_depth_profile(self, price_wallet_id, quantity_wallet_id): """ format: [(<price>, <depth>), (<price>, <depth>), ...] :return: The depth profile :rtype: list """ profile = [] for key, value in self._bids.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): profile.append((key, value.depth)) return profile def get_ask_side_depth_profile(self, price_wallet_id, quantity_wallet_id): """ format: [(<price>, <depth>), (<price>, <depth>), ...] :return: The depth profile :rtype: list """ profile = [] for key, value in self._asks.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): profile.append((key, value.depth)) return profile def bid_relative_price(self, price): """ :param price: The price to be relative to :type price: Price :return: The relative price :rtype: Price """ assert isinstance(price, Price), type(price) return self.get_bid_price('BTC', 'MC') - price def ask_relative_price(self, price): """ :param price: The price to be relative to :type price: Price :return: The relative price :rtype: Price """ assert isinstance(price, Price), type(price) return self.get_ask_price('BTC', 'MC') - price def relative_tick_price(self, tick): """ :param tick: The tick with the price to be relative to :type tick: Tick :return: The relative price :rtype: Price """ assert isinstance(tick, Tick), type(tick) if tick.is_ask(): return self.ask_relative_price(tick.price) else: return self.bid_relative_price(tick.price) def get_bid_price_level(self, price_wallet_id, quantity_wallet_id): """ Return the price level that an ask has to match to make a trade :rtype: PriceLevel """ return self._bids.get_max_price_list(price_wallet_id, quantity_wallet_id) def get_ask_price_level(self, price_wallet_id, quantity_wallet_id): """ Return the price level that a bid has to match to make a trade :rtype: PriceLevel """ return self._asks.get_min_price_list(price_wallet_id, quantity_wallet_id) def get_order_ids(self): """ Return all IDs of the orders in the orderbook, both asks and bids. The returned list is sorted. :rtype: [OrderId] """ return sorted(self.get_bid_ids() + self.get_ask_ids()) def get_ask_ids(self): ids = [] for price_wallet_id, quantity_wallet_id in self.asks.get_price_level_list_wallets(): for _, price_level in self.asks.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): for ask in price_level: ids.append(ask.tick.order_id) return sorted(ids) def get_bid_ids(self): ids = [] for price_wallet_id, quantity_wallet_id in self.bids.get_price_level_list_wallets(): for _, price_level in self.bids.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): for bid in price_level: ids.append(bid.tick.order_id) return sorted(ids) def __str__(self): res_str = '' res_str += "------ Bids -------\n" for price_wallet_id, quantity_wallet_id in self.bids.get_price_level_list_wallets(): for _, value in self._bids.get_price_level_list(price_wallet_id, quantity_wallet_id).items(reverse=True): res_str += '%s' % value res_str += "\n------ Asks -------\n" for price_wallet_id, quantity_wallet_id in self.asks.get_price_level_list_wallets(): for _, value in self._asks.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): res_str += '%s' % value res_str += "\n" return res_str def cancel_all_pending_tasks(self): super(OrderBook, self).cancel_all_pending_tasks() for order_id in self.get_order_ids(): self.get_tick(order_id).cancel_all_pending_tasks()
class SideTestSuite(unittest.TestCase): """Side test cases.""" def setUp(self): # Object creation self.tick = Tick(OrderId(TraderId('0'), OrderNumber(1)), Price(400, 'BTC'), Quantity(30, 'MC'), Timeout(float("inf")), Timestamp(float("inf")), True) self.tick2 = Tick(OrderId(TraderId('1'), OrderNumber(2)), Price(800, 'BTC'), Quantity(30, 'MC'), Timeout(float("inf")), Timestamp(float("inf")), True) self.side = Side() def test_max_price(self): # Test max price (list) self.assertEquals(None, self.side.get_max_price('BTC', 'MC')) self.assertEquals(None, self.side.get_max_price_list('BTC', 'MC')) self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.assertEquals('30.000000 MC\t@\t800.000000 BTC\n', str(self.side.get_max_price_list('BTC', 'MC'))) self.assertEquals(Price(800, 'BTC'), self.side.get_max_price('BTC', 'MC')) def test_min_price(self): # Test min price (list) self.assertEquals(None, self.side.get_min_price_list('BTC', 'MC')) self.assertEquals(None, self.side.get_min_price('BTC', 'MC')) self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.assertEquals('30.000000 MC\t@\t400.000000 BTC\n', str(self.side.get_min_price_list('BTC', 'MC'))) self.assertEquals(Price(400, 'BTC'), self.side.get_min_price('BTC', 'MC')) def test_insert_tick(self): # Test insert tick self.assertEquals(0, len(self.side)) self.assertFalse( self.side.tick_exists(OrderId(TraderId('0'), OrderNumber(1)))) self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.assertEquals(2, len(self.side)) self.assertTrue( self.side.tick_exists(OrderId(TraderId('0'), OrderNumber(1)))) def test_remove_tick(self): # Test remove tick self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.side.remove_tick(OrderId(TraderId('0'), OrderNumber(1))) self.assertEquals(1, len(self.side)) self.side.remove_tick(OrderId(TraderId('1'), OrderNumber(2))) self.assertEquals(0, len(self.side)) def test_get_price_level_list_wallets(self): """ Test the price level lists of wallets of a side """ self.assertFalse(self.side.get_price_level_list_wallets()) self.side.insert_tick(self.tick) self.assertTrue(self.side.get_price_level_list_wallets()) def test_get_list_representation(self): """ Testing the list representation of a side """ self.assertFalse(self.side.get_list_representation()) self.side.insert_tick(self.tick) list_rep = self.side.get_list_representation() self.assertTrue(list_rep)
class SideTestSuite(unittest.TestCase): """Side test cases.""" def setUp(self): # Object creation self.tick = Tick(OrderId(TraderId('0'), OrderNumber(1)), AssetPair(AssetAmount(60, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now(), True) self.tick2 = Tick(OrderId(TraderId('1'), OrderNumber(2)), AssetPair(AssetAmount(120, 'BTC'), AssetAmount(30, 'MB')), Timeout(100), Timestamp.now(), True) self.side = Side() def test_max_price(self): # Test max price (list) self.assertEquals(None, self.side.get_max_price('MB', 'BTC')) self.assertEquals(None, self.side.get_max_price_list('MB', 'BTC')) self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.assertEquals(Price(0.5, 'MB', 'BTC'), self.side.get_max_price('MB', 'BTC')) def test_min_price(self): # Test min price (list) self.assertEquals(None, self.side.get_min_price_list('MB', 'BTC')) self.assertEquals(None, self.side.get_min_price('MB', 'BTC')) self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.assertEquals(Price(0.25, 'MB', 'BTC'), self.side.get_min_price('MB', 'BTC')) def test_insert_tick(self): # Test insert tick self.assertEquals(0, len(self.side)) self.assertFalse(self.side.tick_exists(OrderId(TraderId('0'), OrderNumber(1)))) self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.assertEquals(2, len(self.side)) self.assertTrue(self.side.tick_exists(OrderId(TraderId('0'), OrderNumber(1)))) def test_remove_tick(self): # Test remove tick self.side.insert_tick(self.tick) self.side.insert_tick(self.tick2) self.side.remove_tick(OrderId(TraderId('0'), OrderNumber(1))) self.assertEquals(1, len(self.side)) self.side.remove_tick(OrderId(TraderId('1'), OrderNumber(2))) self.assertEquals(0, len(self.side)) def test_get_price_level_list_wallets(self): """ Test the price level lists of wallets of a side """ self.assertFalse(self.side.get_price_level_list_wallets()) self.side.insert_tick(self.tick) self.assertTrue(self.side.get_price_level_list_wallets()) def test_get_list_representation(self): """ Testing the list representation of a side """ self.assertFalse(self.side.get_list_representation()) self.side.insert_tick(self.tick) list_rep = self.side.get_list_representation() self.assertTrue(list_rep)
class OrderBook(TaskManager): """ OrderBook is used for searching through all the orders and giving an indication to the user of what other offers are out there. """ def __init__(self): super(OrderBook, self).__init__() self._logger = logging.getLogger(self.__class__.__name__) self._bids = Side() self._asks = Side() self.completed_orders = set() def timeout_ask(self, order_id): ask = self.get_ask(order_id).tick self.remove_tick(order_id) return ask def timeout_bid(self, order_id): bid = self.get_bid(order_id).tick self.remove_tick(order_id) return bid def on_timeout_error(self, _): pass def on_invalid_tick_insert(self, _): self._logger.warning("Invalid tick inserted in order book.") def insert_ask(self, ask): """ :type ask: Ask """ if not self._asks.tick_exists(ask.order_id) and ask.order_id not in self.completed_orders and ask.is_valid(): self._asks.insert_tick(ask) timeout_delay = float(ask.timestamp) + int(ask.timeout) - time.time() task = deferLater(reactor, timeout_delay, self.timeout_ask, ask.order_id) self.register_task("ask_%s_timeout" % ask.order_id, task) return task.addErrback(self.on_timeout_error) return fail(Failure(RuntimeError("ask invalid"))).addErrback(self.on_invalid_tick_insert) def remove_ask(self, order_id): """ :type order_id: OrderId """ if self._asks.tick_exists(order_id): self.cancel_pending_task("ask_%s_timeout" % order_id) self._asks.remove_tick(order_id) def insert_bid(self, bid): """ :type bid: Bid """ if not self._bids.tick_exists(bid.order_id) and bid.order_id not in self.completed_orders and bid.is_valid(): self._bids.insert_tick(bid) timeout_delay = float(bid.timestamp) + int(bid.timeout) - time.time() task = deferLater(reactor, timeout_delay, self.timeout_bid, bid.order_id) self.register_task("bid_%s_timeout" % bid.order_id, task) return task.addErrback(self.on_timeout_error) return fail(Failure(RuntimeError("bid invalid"))).addErrback(self.on_invalid_tick_insert) def remove_bid(self, order_id): """ :type order_id: OrderId """ if self._bids.tick_exists(order_id): self.cancel_pending_task("bid_%s_timeout" % order_id) self._bids.remove_tick(order_id) def update_ticks(self, ask_order_dict, bid_order_dict, traded_quantity, unreserve=True): """ Update ticks according to a TrustChain block containing the status of the ask/bid orders. :type ask_order_dict: dict :type bid_order_dict: dict :type traded_quantity: int :type unreserve: bool """ ask_order_id = OrderId(TraderId(ask_order_dict["trader_id"]), OrderNumber(ask_order_dict["order_number"])) bid_order_id = OrderId(TraderId(bid_order_dict["trader_id"]), OrderNumber(bid_order_dict["order_number"])) self._logger.debug("Updating ticks in order book: %s and %s (traded quantity: %s)", str(ask_order_id), str(bid_order_id), str(traded_quantity)) # Update ask tick ask_exists = self.tick_exists(ask_order_id) if ask_exists and ask_order_dict["traded"] >= self.get_tick(ask_order_id).traded: tick = self.get_tick(ask_order_id) tick.traded = ask_order_dict["traded"] if unreserve: tick.release_for_matching(traded_quantity) if tick.traded >= tick.assets.first.amount: self.remove_tick(tick.order_id) self.completed_orders.add(tick.order_id) elif not ask_exists and ask_order_dict["traded"] < ask_order_dict["assets"]["first"]["amount"] and \ ask_order_id not in self.completed_orders: new_pair = AssetPair.from_dictionary(ask_order_dict["assets"]) ask = Ask(ask_order_id, new_pair, Timeout(ask_order_dict["timeout"]), Timestamp(ask_order_dict["timestamp"]), traded=ask_order_dict["traded"]) self.insert_ask(ask) elif not ask_exists and ask_order_dict["traded"] >= ask_order_dict["assets"]["first"]["amount"]: self.completed_orders.add(ask_order_id) # Update bid tick bid_exists = self.tick_exists(bid_order_id) if bid_exists and bid_order_dict["traded"] >= self.get_tick(bid_order_id).traded: tick = self.get_tick(bid_order_id) tick.traded = bid_order_dict["traded"] if unreserve: tick.release_for_matching(traded_quantity) if tick.traded >= tick.assets.first.amount: self.remove_tick(tick.order_id) self.completed_orders.add(tick.order_id) elif not bid_exists and bid_order_dict["traded"] < bid_order_dict["assets"]["first"]["amount"] and \ bid_order_id not in self.completed_orders: new_pair = AssetPair.from_dictionary(bid_order_dict["assets"]) bid = Bid(bid_order_id, new_pair, Timeout(bid_order_dict["timeout"]), Timestamp(bid_order_dict["timestamp"]), traded=bid_order_dict["traded"]) self.insert_bid(bid) elif not bid_exists and bid_order_dict["traded"] >= bid_order_dict["assets"]["first"]["amount"]: self.completed_orders.add(bid_order_id) def tick_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the tick exists, False otherwise :rtype: bool """ is_ask = self._asks.tick_exists(order_id) is_bid = self._bids.tick_exists(order_id) return is_ask or is_bid def get_ask(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ return self._asks.get_tick(order_id) def get_bid(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ return self._bids.get_tick(order_id) def get_tick(self, order_id): """ Return a tick with the specified order id. :param order_id: The order id to search for :type order_id: OrderId :rtype: TickEntry """ return self._bids.get_tick(order_id) or self._asks.get_tick(order_id) def ask_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the ask exists, False otherwise :rtype: bool """ return self._asks.tick_exists(order_id) def bid_exists(self, order_id): """ :param order_id: The order id to search for :type order_id: OrderId :return: True if the bid exists, False otherwise :rtype: bool """ return self._bids.tick_exists(order_id) def remove_tick(self, order_id): """ :type order_id: OrderId """ self._logger.debug("Removing tick %s from order book", order_id) self.remove_ask(order_id) self.remove_bid(order_id) @property def asks(self): """ Return the asks side :rtype: Side """ return self._asks @property def bids(self): """ Return the bids side :rtype: Side """ return self._bids def get_bid_price(self, price_wallet_id, quantity_wallet_id): """ Return the price an ask needs to have to make a trade :rtype: Price """ return self._bids.get_max_price(price_wallet_id, quantity_wallet_id) def get_ask_price(self, price_wallet_id, quantity_wallet_id): """ Return the price a bid needs to have to make a trade :rtype: Price """ return self._asks.get_min_price(price_wallet_id, quantity_wallet_id) def get_bid_ask_spread(self, price_wallet_id, quantity_wallet_id): """ Return the spread between the bid and the ask price :rtype: Price """ spread = self.get_ask_price(price_wallet_id, quantity_wallet_id).amount - \ self.get_bid_price(price_wallet_id, quantity_wallet_id).amount return Price(spread, price_wallet_id, quantity_wallet_id) def get_mid_price(self, price_wallet_id, quantity_wallet_id): """ Return the price in between the bid and the ask price :rtype: Price """ ask_price = self.get_ask_price(price_wallet_id, quantity_wallet_id).amount bid_price = self.get_bid_price(price_wallet_id, quantity_wallet_id).amount return Price((ask_price + bid_price) / 2, price_wallet_id, quantity_wallet_id) def bid_side_depth(self, price): """ Return the depth of the price level with the given price on the bid side :param price: The price for the price level :type price: Price :return: The depth at that price level :rtype: Quantity """ return self._bids.get_price_level(price).depth def ask_side_depth(self, price): """ Return the depth of the price level with the given price on the ask side :param price: The price for the price level :type price: Price :return: The depth at that price level :rtype: Quantity """ return self._asks.get_price_level(price).depth def get_bid_side_depth_profile(self, price_wallet_id, quantity_wallet_id): """ format: [(<price>, <depth>), (<price>, <depth>), ...] :return: The depth profile :rtype: list """ profile = [] for price_level in self._bids.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): profile.append((price_level.price, price_level.depth)) return profile def get_ask_side_depth_profile(self, price_wallet_id, quantity_wallet_id): """ format: [(<price>, <depth>), (<price>, <depth>), ...] :return: The depth profile :rtype: list """ profile = [] for price_level in self._asks.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): profile.append((price_level.price, price_level.depth)) return profile def get_bid_price_level(self, price_wallet_id, quantity_wallet_id): """ Return the price level that an ask has to match to make a trade :rtype: PriceLevel """ return self._bids.get_max_price_list(price_wallet_id, quantity_wallet_id) def get_ask_price_level(self, price_wallet_id, quantity_wallet_id): """ Return the price level that a bid has to match to make a trade :rtype: PriceLevel """ return self._asks.get_min_price_list(price_wallet_id, quantity_wallet_id) def get_order_ids(self): """ Return all IDs of the orders in the orderbook, both asks and bids. :rtype: [OrderId] """ return self.get_bid_ids() + self.get_ask_ids() def get_ask_ids(self): ids = [] for price_wallet_id, quantity_wallet_id in self.asks.get_price_level_list_wallets(): for price_level in self.asks.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): for ask in price_level: ids.append(ask.tick.order_id) return ids def get_bid_ids(self): ids = [] for price_wallet_id, quantity_wallet_id in self.bids.get_price_level_list_wallets(): for price_level in self.bids.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): for bid in price_level: ids.append(bid.tick.order_id) return ids def __str__(self): res_str = '' res_str += "------ Bids -------\n" for price_wallet_id, quantity_wallet_id in self.bids.get_price_level_list_wallets(): for price_level in self._bids.get_price_level_list(price_wallet_id, quantity_wallet_id).items(reverse=True): res_str += '%s' % price_level res_str += "\n------ Asks -------\n" for price_wallet_id, quantity_wallet_id in self.asks.get_price_level_list_wallets(): for price_level in self._asks.get_price_level_list(price_wallet_id, quantity_wallet_id).items(): res_str += '%s' % price_level res_str += "\n" return res_str def cancel_all_pending_tasks(self): super(OrderBook, self).cancel_all_pending_tasks() for order_id in self.get_order_ids(): self.get_tick(order_id).cancel_all_pending_tasks()