def __init__(self, market, side, price, visible_qty, hidden_qty=0): """ An individual quote expressing a desire to buy or sell a Product for a Price. Visible qty is required and hidden qty is optional, defaulting to 0 because many markets don't support hidden quantity. :param market: MarketObjects.Market.Market. :param side: Side. :param price: Price. :param visible_qty: int. Must be greater than 0 :param hidden_qty: int. Must be 0 or more. Optional. Defaults to 0. """ assert isinstance(price, Price) or isinstance(price, str) assert isinstance(side, Side) assert isinstance(market, Market) assert isinstance(visible_qty, int) assert isinstance(hidden_qty, int) assert visible_qty > 0, "A quote's visible qty must be greater than 0" assert hidden_qty >= 0, "A quote's hidden qty must be greater than or equal to 0" use_price = price if not isinstance(price, Price): use_price = Price(price) assert market.is_valid_price(use_price), \ "%s is not a valid price for a product with minimum price increment %s" % \ (str(use_price), str(market.mpi())) self._side = side self._market = market self._price_level = PriceLevel(use_price, visible_qty, hidden_qty, num_orders=1)
def test_worse_or_same_as(): pl1 = PriceLevel(Price("1.6"), 2, 3, 2) pl2 = PriceLevel(Price("2.1"), 2, 3, 2) pl3 = PriceLevel(Price("1.6"), 24, 4, 22) assert pl1.worse_or_same_as(pl2, BID_SIDE) assert pl2.worse_or_same_as(pl1, BID_SIDE) is False assert pl1.worse_or_same_as(pl2, ASK_SIDE) is False assert pl2.worse_or_same_as(pl1, ASK_SIDE) assert pl1.worse_or_same_as(pl3, BID_SIDE) assert pl1.worse_or_same_as(pl3, ASK_SIDE)
def test_better_or_same_as(): pl1 = PriceLevel(Price("1.4"), 2, 3, 2) pl2 = PriceLevel(Price("1.1"), 2, 3, 2) pl3 = PriceLevel(Price("1.4"), 24, 4, 22) assert pl1.better_or_same_as(pl2, BID_SIDE) assert pl2.better_or_same_as(pl1, BID_SIDE) is False assert pl1.better_or_same_as(pl2, ASK_SIDE) is False assert pl2.better_or_same_as(pl1, ASK_SIDE) assert pl1.better_or_same_as(pl3, BID_SIDE) assert pl1.better_or_same_as(pl3, ASK_SIDE)
def test_worse_than(): pl1 = PriceLevel(Price("1.6"), 2, 3, 2) pl2 = PriceLevel(Price("2.1"), 2, 3, 2) assert pl1.worse_than(pl2, BID_SIDE) assert pl2.worse_than(pl1, BID_SIDE) is False assert pl1.worse_than(pl2, ASK_SIDE) is False assert pl2.worse_than(pl1, ASK_SIDE)
def test_better_than(): pl1 = PriceLevel(Price("1.4"), 2, 3, 2) pl2 = PriceLevel(Price("1.1"), 2, 3, 2) assert pl1.better_than(pl2, BID_SIDE) assert pl2.better_than(pl1, BID_SIDE) is False assert pl1.better_than(pl2, ASK_SIDE) is False assert pl2.better_than(pl1, ASK_SIDE)
def test_parital_passive_fill(): ob = build_base_order_book() # get baseline chain_aggressed_into = ob.best_priority_chain(ASK_SIDE) visible_qty = ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) hidden_qty = ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) num_orders = ob.num_orders_at_price(ASK_SIDE, Price("34.52")) best_price = ob.best_price(ASK_SIDE) # the ack'd order getting aggresed into: # a1_ack = AcknowledgementReport(4, 1234000.888, 1002, "user_b", MARKET, a1, Price("34.52"), 35, 10) agg_new = NewOrderCommand(101, 1234002.123, 1008, "user_z", MARKET, BID_SIDE, FAR, Price("34.52"), 5) pf = PartialFillReport(102, 1234002.123, 1002, "user_b", MARKET, agg_new, 5, Price('34.52'), ASK_SIDE, 3333, 30) chain_aggressed_into.apply_partial_fill_report(pf) ob.handle_partial_fill_report(pf, chain_aggressed_into) # now test for changes # ask visible should be 5 less assert ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) == visible_qty - 5 # ask hidden should stay the same assert hidden_qty == ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) # best price should be the same assert ob.best_ask_price() == best_price == Price("34.52") # num orders should stay the same assert num_orders == ob.num_orders_at_price(ASK_SIDE, Price("34.52")) # best chain on the ask should be the same assert ob.best_priority_chain(ASK_SIDE).chain_id() == chain_aggressed_into.chain_id() == 1002 # nothing on bid should have changed bid_prices = ob.bid_prices() assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2)
def build_base_order_book(): ob = OrderLevelBook(MARKET, LOGGER) b1 = NewOrderCommand(1, 1234000.123, 1001, "user_a", MARKET, BID_SIDE, FAR, Price("34.50"), 50) bid_oec1 = OrderEventChain(b1, LOGGER, SUBCHAIN_ID_GENERATOR) b1_ack = AcknowledgementReport(2, 1234000.123, 1001, "user_a", MARKET, b1, Price("34.50"), 50, 50) bid_oec1.apply_acknowledgement_report(b1_ack) ob.handle_acknowledgement_report(b1_ack, bid_oec1) a1 = NewOrderCommand(3, 1234000.888, 1002, "user_b", MARKET, ASK_SIDE, FAR, Price("34.52"), 35, 10) ask_oec1 = OrderEventChain(a1, LOGGER, SUBCHAIN_ID_GENERATOR) a1_ack = AcknowledgementReport(4, 1234000.888, 1002, "user_b", MARKET, a1, Price("34.52"), 35, 10) ask_oec1.apply_acknowledgement_report(a1_ack) ob.handle_acknowledgement_report(a1_ack, ask_oec1) a2 = NewOrderCommand(5, 1234001.888, 1003, "user_c", MARKET, ASK_SIDE, FAR, Price("34.52"), 20, 20) ask_oec2 = OrderEventChain(a2, LOGGER, SUBCHAIN_ID_GENERATOR) a2_ack = AcknowledgementReport(6, 1234001.888, 1003, "user_b", MARKET, a2, Price("34.52"), 20, 20) ask_oec2.apply_acknowledgement_report(a2_ack) ob.handle_acknowledgement_report(a2_ack, ask_oec2) b2 = NewOrderCommand(9, 1234000.123, 1004, "user_z", MARKET, BID_SIDE, FAR, Price("34.50"), 70) bid_oec2 = OrderEventChain(b2, LOGGER, SUBCHAIN_ID_GENERATOR) b2_ack = AcknowledgementReport(10, 1234000.123, 1004, "user_z", MARKET, b2, Price("34.50"), 70, 70) bid_oec2.apply_acknowledgement_report(b2_ack) ob.handle_acknowledgement_report(b2_ack, bid_oec2) # do some asserts to ensure we built book as expected bid_prices = ob.prices(BID_SIDE) print(ob.best_bid_price()) print(bid_prices) assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2) assert ob.best_priority_chain(BID_SIDE) == bid_oec1 ask_prices = ob.ask_prices() assert len(ask_prices) == 1 assert ask_prices[0] == Price("34.52") assert ob.best_ask_price() == Price("34.52") assert ob.best_ask_level() == PriceLevel(Price("34.52"), 30, 25, 2) assert ob.best_priority_chain(ASK_SIDE) == ask_oec1 return ob
def test_acking_fak(): # should not allow -- so no changes to order book ob = build_base_order_book() a = NewOrderCommand(56, 1234002.123, 1008, "user_z", MARKET, ASK_SIDE, FAK, Price("34.51"), 35) ask_oec = OrderEventChain(a, LOGGER, SUBCHAIN_ID_GENERATOR) a_ack = AcknowledgementReport(57, 1234002.123, 1008, "user_z", MARKET, a, Price("34.51"), 35, 35) ask_oec.apply_acknowledgement_report(a_ack) ob.handle_acknowledgement_report(a_ack, ask_oec) # NOTHING SHOULD HAVE CHANGED bid_prices = ob.bid_prices() assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2) ask_prices = ob.ask_prices() assert len(ask_prices) == 1 assert ask_prices[0] == Price("34.52") assert ob.best_ask_price() == Price("34.52") assert ob.best_ask_level() == PriceLevel(Price("34.52"), 30, 25, 2)
def test_establish_new_ask_tob(): ob = build_base_order_book() a = NewOrderCommand(56, 1234002.123, 1008, "user_z", MARKET, ASK_SIDE, FAR, Price("34.51"), 35) ask_oec = OrderEventChain(a, LOGGER, SUBCHAIN_ID_GENERATOR) a_ack = AcknowledgementReport(57, 1234002.123, 1008, "user_z", MARKET, a, Price("34.51"), 35, 35) ask_oec.apply_acknowledgement_report(a_ack) ob.handle_acknowledgement_report(a_ack, ask_oec) # ask should have changed ask_prices = ob.ask_prices() assert len(ask_prices) == 2 # now 2 prices assert ob.best_priority_chain(ASK_SIDE) == ask_oec # the bid order chain above should now be best assert ask_prices[0] == Price("34.51") # should be new best price assert ob.best_ask_price() == Price("34.51") # should be new best price assert ob.best_ask_level() == PriceLevel(Price("34.51"), 35, 0, 1) # should have new best price level with only 1 order # bid should not have changed bid_prices = ob.bid_prices() assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2)
def test_equality(): pl1 = PriceLevel(Price("1.1"), 2, 3, 2) pl2 = PriceLevel(Price("1.1"), 2, 3, 2) pl_price_different = PriceLevel(Price("1.2"), 2, 3, 2) pl_visible_qty_different = PriceLevel(Price("1.1"), 3, 3, 2) pl_hidden_qty_different = PriceLevel(Price("1.1"), 2, 332, 2) pl_num_orders_different = PriceLevel(Price("1.1"), 2, 3, 3) pl_num_orders_none = PriceLevel(Price("1.1"), 2, 3, None) assert pl1 == pl2 assert pl1 != pl_price_different assert pl1 != pl_visible_qty_different assert pl1 != pl_hidden_qty_different assert pl1 != pl_num_orders_different assert pl1 != pl_num_orders_none pl1_num_orders_none = PriceLevel(Price("1.1"), 2, 3) pl2_num_orders_none = PriceLevel(Price("1.1"), 2, 3) assert pl1_num_orders_none == pl2_num_orders_none assert pl1 != pl2_num_orders_none assert pl2 != pl1_num_orders_none
def level_at_price(self, side, price): """ Gets the PriceLevel at a price for a given side. Returns None if the price does not exist on that Side. :param side: MarketObjects.Side.Side :param price: MarketObjects.Price.Price :return: MarketObjects.PriceLevel.PriceLevel. Can be None """ level = (self._bid_price_to_level if side.is_bid() else self._ask_price_to_level).get(price) if level is None: return None return PriceLevel(price, self.visible_qty_at_price(side, price), self.hidden_qty_at_price(side, price), self.num_orders_at_price(side, price))
def test_partial_passive_fill_for_all_qty(): # a partial fill for eveything should balk with some logs but should still be successful. # NOTE: this scenario is crazy and shouldn't actually happen (hidden getting filled before visible) # but what the hell, let's cut corners on a test ob = build_base_order_book() # get baseline chain_aggressed_into = ob.best_priority_chain(ASK_SIDE) second_ask_back = ob.order_chains_at_price(ASK_SIDE, Price("34.52"))[1] visible_qty = ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) hidden_qty = ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) num_orders = ob.num_orders_at_price(ASK_SIDE, Price("34.52")) best_price = ob.best_price(ASK_SIDE) # the ack'd order getting aggresed into: # a1_ack = AcknowledgementReport(4, 1234000.888, 1002, "user_b", MARKET, a1, Price("34.52"), 35, 10) agg_new = NewOrderCommand(101, 1234002.123, 1008, "user_z", MARKET, BID_SIDE, FAR, Price("34.52"), 35) pf = PartialFillReport(102, 1234002.123, 1002, "user_b", MARKET, agg_new, 35, Price('34.52'), ASK_SIDE, 3333, 25) chain_aggressed_into.apply_partial_fill_report(pf) ob.handle_partial_fill_report(pf, chain_aggressed_into) # now test for changes # ask visible qty should be 10 less (the rest came from hidden) assert ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) == visible_qty - 10 # ask hidden should be 25 (35-10 visible) less assert hidden_qty - 25 == ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) # best price should be the same assert ob.best_ask_price() == best_price == Price("34.52") # should be 1 less order assert num_orders - 1 == ob.num_orders_at_price(ASK_SIDE, Price("34.52")) # best chain on the ask should be what was the second back assert ob.best_priority_chain(ASK_SIDE).chain_id() == second_ask_back.chain_id() # chain should no longer exist in book chains = ob.order_chains_at_price(ASK_SIDE, Price("34.52")) found = False for chain in chains: if chain.chain_id() == chain_aggressed_into.chain_id(): found = True assert not found # nothing on bid should have changed bid_prices = ob.bid_prices() assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2)
def test_cancel_confirm_second_order(): ob = build_base_order_book() first_order_chain = ob.best_priority_chain(ASK_SIDE) second_ask_back = ob.order_chains_at_price(ASK_SIDE, Price("34.52"))[1] visible_qty = ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) hidden_qty = ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) num_orders = ob.num_orders_at_price(ASK_SIDE, Price("34.52")) best_price = ob.best_price(ASK_SIDE) cancel_command = CancelCommand(101, 1234002.123, second_ask_back.chain_id(), second_ask_back.user_id(), MARKET, USER_CANCEL) second_ask_back.apply_cancel_command(cancel_command) cancel_report = CancelReport(102, 1234002.123, second_ask_back.chain_id(), second_ask_back.user_id(), MARKET, cancel_command, USER_CANCEL) second_ask_back.apply_cancel_report(cancel_report) ob.handle_cancel_report(cancel_report, second_ask_back) # the 20 visible are now gone assert ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) == visible_qty - 20 # hidden should stay the same assert hidden_qty == ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) # best price should be the same assert ob.best_ask_price() == best_price == Price("34.52") # should be 1 less order assert num_orders - 1 == ob.num_orders_at_price(ASK_SIDE, Price("34.52")) # best chain on the ask should be the original first assert ob.best_priority_chain(ASK_SIDE).chain_id() == first_order_chain.chain_id() # chain should no longer exist in book chains = ob.order_chains_at_price(ASK_SIDE, Price("34.52")) found = False for chain in chains: if chain.chain_id() == second_ask_back.chain_id(): found = True assert not found # bids shouldn't change bid_prices = ob.bid_prices() assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2)
def test_partial_passive_fill_resets_visible_qty(): ob = build_base_order_book() # get baseline chain_aggressed_into = ob.best_priority_chain(ASK_SIDE) second_ask_back = ob.order_chains_at_price(ASK_SIDE, Price("34.52"))[1] visible_qty = ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) hidden_qty = ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) num_orders = ob.num_orders_at_price(ASK_SIDE, Price("34.52")) best_price = ob.best_price(ASK_SIDE) # the ack'd order getting aggresed into: # a1_ack = AcknowledgementReport(4, 1234000.888, 1002, "user_b", MARKET, a1, Price("34.52"), 35, 10) agg_new = NewOrderCommand(101, 1234002.123, 1008, "user_z", MARKET, BID_SIDE, FAR, Price("34.52"), 10) pf = PartialFillReport(102, 1234002.123, 1002, "user_b", MARKET, agg_new, 10, Price('34.52'), ASK_SIDE, 3333, 25) chain_aggressed_into.apply_partial_fill_report(pf) ob.handle_partial_fill_report(pf, chain_aggressed_into) # now test for changes # ask visible qty should stay the same because the 10 was filled and then replenished due to iceberg activity assert ob.visible_qty_at_price(ASK_SIDE, Price("34.52")) == visible_qty # ask hidden should be 10 less becaue of replenishment of iceberg peak assert hidden_qty - 10 == ob.hidden_qty_at_price(ASK_SIDE, Price("34.52")) # best price should be the same assert ob.best_ask_price() == best_price == Price("34.52") # num orders should stay the same assert num_orders == ob.num_orders_at_price(ASK_SIDE, Price("34.52")) # best chain on the ask should be what was the second back assert ob.best_priority_chain(ASK_SIDE).chain_id() == second_ask_back.chain_id() # worst chain on the ask should be what was the first assert ob.order_chains_at_price(ASK_SIDE, Price("34.52"))[-1].chain_id() == chain_aggressed_into.chain_id() # nothing on bid should have changed bid_prices = ob.bid_prices() assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2)
def test_cancel_confirm_entire_price_level(): ob = build_base_order_book() # first add an order at a second price (34.53) a = NewOrderCommand(56, 1234002.123, 1009, "user_x", MARKET, ASK_SIDE, FAR, Price("34.53"), 20) ask_oec = OrderEventChain(a, LOGGER, SUBCHAIN_ID_GENERATOR) a_ack = AcknowledgementReport(57, 1234002.123, 1009, "user_z", MARKET, a, Price("34.53"), 20, 20) ask_oec.apply_acknowledgement_report(a_ack) ob.handle_acknowledgement_report(a_ack, ask_oec) # cancel the orderchains at best price id_generator = MonotonicIntID() for i, chain in enumerate(ob.order_chains_at_price(ASK_SIDE, ob.best_price(ASK_SIDE))): cancel_command = CancelCommand(id_generator.id(), 1234002.123, chain.chain_id(), chain.user_id(), MARKET, USER_CANCEL) chain.apply_cancel_command(cancel_command) cancel_report = CancelReport(id_generator.id(), 1234002.123, chain.chain_id(), chain.user_id(), MARKET, cancel_command, USER_CANCEL) chain.apply_cancel_report(cancel_report) ob.handle_cancel_report(cancel_report, chain) assert ob.best_priority_chain(ASK_SIDE).chain_id() == ask_oec.chain_id() # best price should now be 34.53 assert ob.best_ask_price() == Price('34.53') # visible qty is 20 assert ob.visible_qty_at_price(ASK_SIDE, ob.best_ask_price()) == 20 # hidden qty is 0 assert ob.hidden_qty_at_price(ASK_SIDE, ob.best_ask_price()) == 0 # num orders is 1 assert ob.num_orders_at_price(ASK_SIDE, ob.best_ask_price()) == 1 # bids shouldn't change bid_prices = ob.bid_prices() assert len(bid_prices) == 1 assert bid_prices[0] == Price("34.50") assert ob.best_bid_price() == Price("34.50") assert ob.best_bid_level() == PriceLevel(Price("34.50"), 120, 0, 2)
def test_negative_hidden_qty_failes(): with pytest.raises(AssertionError): PriceLevel(Price("23.33"), 3, -3)
def test_cannot_have_more_orders_than_qty(): with pytest.raises(AssertionError): PriceLevel(Price("1.1"), 3, 2, 8)
def test_pricelevel_creation(): pl = PriceLevel(Price("84.5"), 12) assert pl.price() == Price("84.5") assert pl.number_of_orders() is None assert pl.visible_qty() == 12 assert pl.hidden_qty() == 0 pl = PriceLevel(Price("32.134"), 14, 23) assert pl.price() == Price("32.134") assert pl.number_of_orders() is None assert pl.visible_qty() == 14 assert pl.hidden_qty() == 23
def test_negative_num_orders_fails(): with pytest.raises(AssertionError): PriceLevel(Price("1.1"), 3, 4, -2)
def test_zero_num_orders_fails(): with pytest.raises(AssertionError): PriceLevel(Price("1.1"), 3, 4, 0)
def test_hidden_qty_must_be_int(): with pytest.raises(AssertionError): PriceLevel(Price("1.1"), 3, 4.0)
def test_visible_qty_must_be_int(): with pytest.raises(AssertionError): PriceLevel(Price("1.1"), 3.0)
def test_zero_visible_qty_fails(): with pytest.raises(AssertionError): PriceLevel(Price("23.33"), 0)
class Quote(object): def __init__(self, market, side, price, visible_qty, hidden_qty=0): """ An individual quote expressing a desire to buy or sell a Product for a Price. Visible qty is required and hidden qty is optional, defaulting to 0 because many markets don't support hidden quantity. :param market: MarketObjects.Market.Market. :param side: Side. :param price: Price. :param visible_qty: int. Must be greater than 0 :param hidden_qty: int. Must be 0 or more. Optional. Defaults to 0. """ assert isinstance(price, Price) or isinstance(price, str) assert isinstance(side, Side) assert isinstance(market, Market) assert isinstance(visible_qty, int) assert isinstance(hidden_qty, int) assert visible_qty > 0, "A quote's visible qty must be greater than 0" assert hidden_qty >= 0, "A quote's hidden qty must be greater than or equal to 0" use_price = price if not isinstance(price, Price): use_price = Price(price) assert market.is_valid_price(use_price), \ "%s is not a valid price for a product with minimum price increment %s" % \ (str(use_price), str(market.mpi())) self._side = side self._market = market self._price_level = PriceLevel(use_price, visible_qty, hidden_qty, num_orders=1) def side(self): """ Get the side of the market the quote is for :return: Side """ return self._side def market(self): """ Get the market the quote is for :return: MarketObjects.Market.Market """ return self._market def price(self): """ Get the price of the quote :return: Price. """ return self._price_level.price() def visible_qty(self): """ Get the visible qty of the quote :return: int """ return self._price_level.visible_qty() def hidden_qty(self): """ Get the hidden qty of the quote :return: int """ return self._price_level.hidden_qty() def total_qty(self): """ Get the total qty of the quote. :return: int. """ return self._price_level.hidden_qty() + self._price_level.visible_qty() def is_buy(self): """ Tells if you the Quote is a buy or not :return: bool """ return self._side.is_bid() def is_sell(self): """ Tells if you the Quote is a sell or not :return: bool """ return self._side.is_ask() def __eq__(self, other): if not isinstance(other, Quote): return False return self._price_level == other._price_level and \ self.side() == other.side() and \ self.market() == other.market() def __str__(self): return "%s Quote: %s %d (%d visible, %d hidden) at %s" % \ (str(self.market()), "Buy" if self.is_buy() else "Sell", self.total_qty(), self.visible_qty(), self.hidden_qty(), str(self.price()))
def test_zero_hidden_qty_works(): pl = PriceLevel(Price("23.33"), 3, 0) assert pl.hidden_qty() == 0
def test_negative_visible_qty_fails(): with pytest.raises(AssertionError): PriceLevel(Price("23.33"), -2)
def test_pricelevel_must_have_price(): with pytest.raises(AssertionError): PriceLevel(None, -2)