class PMMUnitTest(unittest.TestCase): start: pd.Timestamp = pd.Timestamp("2019-01-01", tz="UTC") end: pd.Timestamp = pd.Timestamp("2019-01-01 01:00:00", tz="UTC") start_timestamp: float = start.timestamp() end_timestamp: float = end.timestamp() trading_pair = "HBOT-ETH" base_asset = trading_pair.split("-")[0] quote_asset = trading_pair.split("-")[1] def setUp(self): self.clock_tick_size = 1 self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp) self.market: BacktestMarket = BacktestMarket() self.book_data: MockOrderBookLoader = MockOrderBookLoader( self.trading_pair, self.base_asset, self.quote_asset) self.mid_price = 100 self.bid_spread = 0.01 self.ask_spread = 0.01 self.order_refresh_time = 30 self.book_data.set_balanced_order_book(mid_price=self.mid_price, min_price=1, max_price=200, price_step_size=1, volume_step_size=10) self.market.add_data(self.book_data) self.market.set_balance("HBOT", 500) self.market.set_balance("ETH", 5000) self.market.set_quantization_param( QuantizationParams(self.trading_pair, 6, 6, 6, 6)) self.market_info = MarketTradingPairTuple(self.market, self.trading_pair, self.base_asset, self.quote_asset) self.clock.add_iterator(self.market) self.order_fill_logger: EventLogger = EventLogger() self.cancel_order_logger: EventLogger = EventLogger() self.market.add_listener(MarketEvent.OrderFilled, self.order_fill_logger) self.market.add_listener(MarketEvent.OrderCancelled, self.cancel_order_logger) self.one_level_strategy = PureMarketMakingStrategy( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=5.0, filled_order_delay=5.0, order_refresh_tolerance_pct=-1, minimum_spread=-1, ) self.multi_levels_strategy = PureMarketMakingStrategy( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=5.0, filled_order_delay=5.0, order_refresh_tolerance_pct=-1, order_levels=3, order_level_spread=Decimal("0.01"), order_level_amount=Decimal("1"), minimum_spread=-1, ) self.order_override_strategy = PureMarketMakingStrategy( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=5.0, filled_order_delay=5.0, order_refresh_tolerance_pct=-1, order_levels=3, order_level_spread=Decimal("0.01"), order_level_amount=Decimal("1"), minimum_spread=-1, order_override={ "order_one": ["buy", 0.5, 0.7], "order_two": ["buy", 1.3, 1.1], "order_three": ["sell", 1.1, 2] }, ) self.ext_market: BacktestMarket = BacktestMarket() self.ext_data: MockOrderBookLoader = MockOrderBookLoader( self.trading_pair, self.base_asset, self.quote_asset) self.ext_market_info: MarketTradingPairTuple = MarketTradingPairTuple( self.ext_market, self.trading_pair, self.base_asset, self.quote_asset) self.ext_data.set_balanced_order_book(mid_price=50, min_price=1, max_price=400, price_step_size=1, volume_step_size=10) self.ext_market.add_data(self.ext_data) self.order_book_asset_del = OrderBookAssetPriceDelegate( self.ext_market, self.trading_pair) def simulate_maker_market_trade( self, is_buy: bool, quantity: Decimal, price: Decimal, market: Optional[BacktestMarket] = None, ): if market is None: market = self.market order_book = market.get_order_book(self.trading_pair) trade_event = OrderBookTradeEvent( self.trading_pair, self.clock.current_timestamp, TradeType.BUY if is_buy else TradeType.SELL, price, quantity) order_book.apply_trade(trade_event) def test_basic_one_level(self): strategy = self.one_level_strategy self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) buy_1 = strategy.active_buys[0] self.assertEqual(99, buy_1.price) self.assertEqual(1, buy_1.quantity) sell_1 = strategy.active_sells[0] self.assertEqual(101, sell_1.price) self.assertEqual(1, sell_1.quantity) # After order_refresh_time, a new set of orders is created self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.assertNotEqual(buy_1.client_order_id, strategy.active_buys[0].client_order_id) self.assertNotEqual(sell_1.client_order_id, strategy.active_sells[0].client_order_id) # Simulate buy order filled self.clock.backtest_til(self.start_timestamp + 8) self.simulate_maker_market_trade(False, 100, 98.9) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) # After filled_ore self.clock.backtest_til(self.start_timestamp + 14) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) def test_basic_one_level_price_type_own_last_trade(self): strategy = PureMarketMakingStrategy( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=5.0, filled_order_delay=5.0, order_refresh_tolerance_pct=-1, minimum_spread=-1, price_type='last_own_trade_price', ) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) buy_1 = strategy.active_buys[0] self.assertEqual(99, buy_1.price) self.assertEqual(1, buy_1.quantity) sell_1 = strategy.active_sells[0] self.assertEqual(101, sell_1.price) self.assertEqual(1, sell_1.quantity) # Simulate buy order filled self.simulate_maker_market_trade(False, 100, 98.9) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) # Order has been filled self.clock.backtest_til(self.start_timestamp + 7) buy_1 = strategy.active_buys[0] self.assertEqual(Decimal('98.01'), buy_1.price) self.assertEqual(1, buy_1.quantity) sell_1 = strategy.active_sells[0] self.assertEqual(Decimal('99.99'), sell_1.price) self.assertEqual(1, sell_1.quantity) def test_basic_one_level_price_type(self): strategies = [] for price_type in ["last_price", "best_bid", "best_ask"]: strategy = PureMarketMakingStrategy( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=5.0, filled_order_delay=5.0, order_refresh_tolerance_pct=-1, minimum_spread=-1, price_type=price_type, ) strategies.append(strategy) self.clock.add_iterator(strategy) last_strategy, bid_strategy, ask_strategy = strategies self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) self.assertEqual(1, len(last_strategy.active_buys)) self.assertEqual(1, len(last_strategy.active_sells)) buy_1 = last_strategy.active_buys[0] self.assertEqual(99, buy_1.price) self.assertEqual(1, buy_1.quantity) sell_1 = last_strategy.active_sells[0] self.assertEqual(101, sell_1.price) self.assertEqual(1, sell_1.quantity) # Simulate buy order filled self.simulate_maker_market_trade(False, 100, 98.9) self.assertEqual(0, len(last_strategy.active_buys)) self.assertEqual(1, len(last_strategy.active_sells)) # After filled_ore self.clock.backtest_til(self.start_timestamp + 7) buy_1 = last_strategy.active_buys[0] self.assertEqual(Decimal('97.911'), buy_1.price) self.assertEqual(1, buy_1.quantity) sell_1 = last_strategy.active_sells[0] self.assertEqual(Decimal('99.889'), sell_1.price) self.assertEqual(1, sell_1.quantity) buy_bid = bid_strategy.active_buys[0] buy_target = self.market_info.get_price_by_type( PriceType.BestBid) * Decimal("0.99") self.assertEqual(buy_target, buy_bid.price) sell_bid = bid_strategy.active_sells[0] sell_target = self.market_info.get_price_by_type( PriceType.BestBid) * Decimal("1.01") self.assertEqual(sell_target, sell_bid.price) buy_ask = ask_strategy.active_buys[0] buy_target = self.market_info.get_price_by_type( PriceType.BestAsk) * Decimal("0.99") self.assertEqual(buy_target, buy_ask.price) sell_ask = ask_strategy.active_sells[0] sell_target = self.market_info.get_price_by_type( PriceType.BestAsk) * Decimal("1.01") self.assertEqual(sell_target, sell_ask.price) def test_basic_multiple_levels(self): strategy = self.multi_levels_strategy self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) buys = strategy.active_buys sells = strategy.active_sells self.assertEqual(3, len(buys)) self.assertEqual(3, len(sells)) self.assertEqual(Decimal("99"), buys[0].price) self.assertEqual(Decimal("1"), buys[0].quantity) self.assertEqual(Decimal("98"), buys[1].price) self.assertEqual(Decimal("2"), buys[1].quantity) self.assertEqual(Decimal("101"), sells[0].price) self.assertEqual(Decimal("1"), sells[0].quantity) self.assertEqual(Decimal("103"), sells[2].price) self.assertEqual(Decimal("3"), sells[2].quantity) # After order_refresh_time, a new set of orders is created self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) self.assertNotEqual(buys[0].client_order_id, strategy.active_buys[0].client_order_id) self.assertNotEqual(sells[0].client_order_id, strategy.active_sells[0].client_order_id) # Simulate buy order filled self.clock.backtest_til(self.start_timestamp + 8) self.simulate_maker_market_trade(False, 100, 97.9) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) # After filled_ore self.clock.backtest_til(self.start_timestamp + 14) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) def test_apply_budget_constraint_to_proposal(self): strategy = self.multi_levels_strategy self.clock.add_iterator(strategy) self.market.set_balance("HBOT", Decimal("50")) self.market.set_balance("ETH", Decimal("0")) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) for order in strategy.active_sells: strategy.cancel_order(order.client_order_id) self.market.set_balance("HBOT", 0) self.market.set_balance("ETH", Decimal("5000")) self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) for order in strategy.active_buys: strategy.cancel_order(order.client_order_id) self.market.set_balance("HBOT", Decimal("6.0")) self.market.set_balance("ETH", Decimal("586.0")) self.clock.backtest_til(self.start_timestamp + 20) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) self.assertEqual(Decimal("97"), strategy.active_buys[-1].price) self.assertEqual(Decimal("3"), strategy.active_buys[-1].quantity) self.assertEqual(Decimal("103"), strategy.active_sells[-1].price) self.assertEqual(Decimal("3"), strategy.active_sells[-1].quantity) def test_order_quantity_available_balance(self): """ When balance is below the specified order amount, checks if orders created use the remaining available balance for the order size. """ strategy = PureMarketMakingStrategy(self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_refresh_time=5, order_amount=Decimal("100"), order_levels=3) self.clock.add_iterator(strategy) self.market.set_balance("HBOT", Decimal("10")) self.market.set_balance("ETH", Decimal("1000")) self.clock.backtest_til(self.start_timestamp + 1) # Check if order size on both sides is equal to the remaining balance self.assertEqual(Decimal("10.1010"), strategy.active_buys[0].quantity) self.assertEqual(Decimal("10"), strategy.active_sells[0].quantity) # Order levels created self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) strategy.cancel_order(strategy.active_buys[0].client_order_id) strategy.cancel_order(strategy.active_sells[0].client_order_id) # Do not create order on side with 0 balance self.market.set_balance("HBOT", 0) self.market.set_balance("ETH", Decimal("1000")) self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) def test_market_become_wider(self): strategy = self.one_level_strategy self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(Decimal("99"), strategy.active_buys[0].price) self.assertEqual(Decimal("101"), strategy.active_sells[0].price) self.assertEqual(Decimal("1.0"), strategy.active_buys[0].quantity) self.assertEqual(Decimal("1.0"), strategy.active_sells[0].quantity) simulate_order_book_widening(self.book_data.order_book, 90, 110) self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(2, len(self.cancel_order_logger.event_log)) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.assertEqual(Decimal("99"), strategy.active_buys[0].price) self.assertEqual(Decimal("101"), strategy.active_sells[0].price) self.assertEqual(Decimal("1.0"), strategy.active_buys[0].quantity) self.assertEqual(Decimal("1.0"), strategy.active_sells[0].quantity) def test_market_became_narrower(self): strategy = self.one_level_strategy self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(Decimal("99"), strategy.active_buys[0].price) self.assertEqual(Decimal("101"), strategy.active_sells[0].price) self.assertEqual(Decimal("1.0"), strategy.active_buys[0].quantity) self.assertEqual(Decimal("1.0"), strategy.active_sells[0].quantity) self.book_data.order_book.apply_diffs([OrderBookRow(99.5, 30, 2)], [OrderBookRow(100.5, 30, 2)], 2) self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(2, len(self.cancel_order_logger.event_log)) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.assertEqual(Decimal("99"), strategy.active_buys[0].price) self.assertEqual(Decimal("101"), strategy.active_sells[0].price) self.assertEqual(Decimal("1.0"), strategy.active_buys[0].quantity) self.assertEqual(Decimal("1.0"), strategy.active_sells[0].quantity) def test_price_band_price_ceiling_breach(self): strategy = self.multi_levels_strategy strategy.price_ceiling = Decimal("105") self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) simulate_order_book_widening( self.book_data.order_book, self.mid_price, 115, ) self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) def test_price_band_price_floor_breach(self): strategy = self.multi_levels_strategy strategy.price_floor = Decimal("95") self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) simulate_order_book_widening(self.book_data.order_book, 85, self.mid_price) self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) def test_add_transaction_costs(self): strategy = self.multi_levels_strategy strategy.add_transaction_costs_to_orders = True self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) # Todo: currently hummingsim market doesn't store fee in a percentage value, so we cannot test further on this. def test_filled_order_delay(self): strategy = self.one_level_strategy strategy.filled_order_delay = 60.0 self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.clock.backtest_til(self.start_timestamp + 7) # Ask is filled and due to delay is not replenished immediately self.simulate_maker_market_trade(True, 100, Decimal("101.1")) self.assertEqual(1, len(self.order_fill_logger.event_log)) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) self.clock.backtest_til(self.start_timestamp + 15) # After order_refresh_time, buy order gets canceled self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) # still no orders self.clock.backtest_til(self.start_timestamp + 30) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) # still no orders self.clock.backtest_til(self.start_timestamp + 45) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) # Orders are placed after replenish delay self.clock.backtest_til(self.start_timestamp + 69) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) # Prices are not adjusted according to filled price as per settings self.assertEqual(Decimal("99"), strategy.active_buys[0].price) self.assertEqual(Decimal("101"), strategy.active_sells[0].price) self.assertEqual(Decimal("1.0"), strategy.active_buys[0].quantity) self.assertEqual(Decimal("1.0"), strategy.active_sells[0].quantity) self.order_fill_logger.clear() def test_filled_order_delay_mulitiple_orders(self): strategy = self.multi_levels_strategy strategy.filled_order_delay = 10.0 self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) self.simulate_maker_market_trade(True, 100, Decimal("101.1")) # Ask is filled and due to delay is not replenished immediately self.clock.backtest_til(self.start_timestamp + 2) self.assertEqual(1, len(self.order_fill_logger.event_log)) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(2, len(strategy.active_sells)) # After order_refresh_time, buy order gets canceled self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) # Orders are placed after replenish delay self.clock.backtest_til(self.start_timestamp + 12) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) self.order_fill_logger.clear() def test_order_optimization(self): # Widening the order book, top bid is now 97.5 and top ask 102.5 simulate_order_book_widening(self.book_data.order_book, 98, 102) strategy = self.one_level_strategy strategy.order_optimization_enabled = True self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.assertEqual(Decimal("97.5001"), strategy.active_buys[0].price) self.assertEqual(Decimal("102.499"), strategy.active_sells[0].price) def test_hanging_orders(self): strategy = self.one_level_strategy strategy.order_refresh_time = 4.0 strategy.filled_order_delay = 8.0 strategy.hanging_orders_enabled = True strategy.hanging_orders_cancel_pct = Decimal("0.05") self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.simulate_maker_market_trade(False, 100, 98.9) # Bid is filled and due to delay is not replenished immediately # Ask order is now hanging but is active self.clock.backtest_til(self.start_timestamp + 2) self.assertEqual(1, len(self.order_fill_logger.event_log)) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.assertEqual(1, len(strategy.hanging_order_ids)) hanging_order_id = strategy.hanging_order_ids[0] # At order_refresh_time, hanging order remains. self.clock.backtest_til(self.start_timestamp + 5) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) # At filled_order_delay, a new set of bid and ask orders (one each) is created self.clock.backtest_til(self.start_timestamp + 10) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(2, len(strategy.active_sells)) self.assertIn( hanging_order_id, [order.client_order_id for order in strategy.active_sells]) simulate_order_book_widening(self.book_data.order_book, 80, 100) # As book bids moving lower, the ask hanging order price spread is now more than the hanging_orders_cancel_pct # Hanging order is canceled and removed from the active list self.clock.backtest_til(self.start_timestamp + 11 * self.clock_tick_size) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) self.assertNotIn(strategy.active_sells[0].client_order_id, strategy.hanging_order_ids) self.order_fill_logger.clear() def test_hanging_orders_multiple_orders(self): strategy = self.multi_levels_strategy strategy.order_refresh_time = 4.0 strategy.filled_order_delay = 8.0 strategy.hanging_orders_enabled = True strategy.hanging_orders_cancel_pct = Decimal("0.05") self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) self.simulate_maker_market_trade(False, 100, 98.9) # Bid is filled and due to delay is not replenished immediately # Ask order is now hanging but is active self.clock.backtest_til(self.start_timestamp + 2) self.assertEqual(1, len(self.order_fill_logger.event_log)) self.assertEqual(2, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) self.assertEqual(3, len(strategy.hanging_order_ids)) # At order_refresh_time, hanging order remains. self.clock.backtest_til(self.start_timestamp + 5) self.assertEqual(0, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) # At filled_order_delay, a new set of bid and ask orders (one each) is created self.clock.backtest_til(self.start_timestamp + 10) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(6, len(strategy.active_sells)) self.assertTrue( all(id in (order.client_order_id for order in strategy.active_sells) for id in strategy.hanging_order_ids)) simulate_order_book_widening(self.book_data.order_book, 80, 100) # As book bids moving lower, the ask hanging order price spread is now more than the hanging_orders_cancel_pct # Hanging order is canceled and removed from the active list self.clock.backtest_til(self.start_timestamp + 11 * self.clock_tick_size) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) self.assertFalse( any(o.client_order_id in strategy.hanging_order_ids for o in strategy.active_sells)) self.order_fill_logger.clear() def test_inventory_skew(self): strategy = self.one_level_strategy strategy.inventory_skew_enabled = True strategy.inventory_target_base_pct = Decimal("0.9") strategy.inventory_range_multiplier = Decimal("5.0") self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) first_bid_order = strategy.active_buys[0] first_ask_order = strategy.active_sells[0] self.assertEqual(Decimal("99"), first_bid_order.price) self.assertEqual(Decimal("101"), first_ask_order.price) self.assertEqual(Decimal("0.5"), first_bid_order.quantity) self.assertEqual(Decimal("1.5"), first_ask_order.quantity) self.simulate_maker_market_trade(True, 5.0, 101.1) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(0, len(strategy.active_sells)) self.clock.backtest_til(self.start_timestamp + 2) self.assertEqual(1, len(self.order_fill_logger.event_log)) maker_fill = self.order_fill_logger.event_log[0] self.assertEqual(TradeType.SELL, maker_fill.trade_type) self.assertAlmostEqual(101, maker_fill.price) self.assertAlmostEqual(Decimal("1.5"), Decimal(str(maker_fill.amount)), places=4) self.clock.backtest_til(self.start_timestamp + 7) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) first_bid_order = strategy.active_buys[0] first_ask_order = strategy.active_sells[0] self.assertEqual(Decimal("99"), first_bid_order.price) self.assertEqual(Decimal("101"), first_ask_order.price) self.assertEqual(Decimal("0.651349"), first_bid_order.quantity) self.assertEqual(Decimal("1.34865"), first_ask_order.quantity) def test_inventory_skew_multiple_orders(self): strategy = PureMarketMakingStrategy( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=5.0, filled_order_delay=5.0, order_refresh_tolerance_pct=-1, order_levels=5, order_level_spread=Decimal("0.01"), order_level_amount=Decimal("0.5"), inventory_skew_enabled=True, inventory_target_base_pct=Decimal("0.9"), inventory_range_multiplier=Decimal("0.5"), minimum_spread=-1, ) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(5, len(strategy.active_buys)) self.assertEqual(5, len(strategy.active_sells)) first_bid_order = strategy.active_buys[0] first_ask_order = strategy.active_sells[0] self.assertEqual(Decimal("99"), first_bid_order.price) self.assertEqual(Decimal("101"), first_ask_order.price) self.assertEqual(Decimal("0.5"), first_bid_order.quantity) self.assertEqual(Decimal("1.5"), first_ask_order.quantity) last_bid_order = strategy.active_buys[-1] last_ask_order = strategy.active_sells[-1] last_bid_price = Decimal(100 * (1 - 0.01 - (0.01 * 4))).quantize( Decimal("0.001")) last_ask_price = Decimal(100 * (1 + 0.01 + (0.01 * 4))).quantize( Decimal("0.001")) self.assertAlmostEqual(last_bid_price, last_bid_order.price, 3) self.assertAlmostEqual(last_ask_price, last_ask_order.price, 3) self.assertEqual(Decimal("1.5"), last_bid_order.quantity) self.assertEqual(Decimal("4.5"), last_ask_order.quantity) self.simulate_maker_market_trade(True, 5.0, 101.1) self.assertEqual(5, len(strategy.active_buys)) self.assertEqual(4, len(strategy.active_sells)) self.clock.backtest_til(self.start_timestamp + 3) self.assertEqual(1, len(self.order_fill_logger.event_log)) maker_fill = self.order_fill_logger.event_log[0] self.assertEqual(TradeType.SELL, maker_fill.trade_type) self.assertAlmostEqual(101, maker_fill.price) self.assertAlmostEqual(Decimal("1.5"), Decimal(str(maker_fill.amount)), places=4) # The default filled_order_delay is 60, so gotta wait 60 + 2 here. self.clock.backtest_til(self.start_timestamp + 7 * self.clock_tick_size + 1) self.assertEqual(5, len(strategy.active_buys)) self.assertEqual(5, len(strategy.active_sells)) first_bid_order = strategy.active_buys[0] first_ask_order = strategy.active_sells[0] last_bid_order = strategy.active_buys[-1] last_ask_order = strategy.active_sells[-1] self.assertEqual(Decimal("99"), first_bid_order.price) self.assertEqual(Decimal("101"), first_ask_order.price) self.assertEqual(Decimal("0.651349"), first_bid_order.quantity) self.assertEqual(Decimal("1.34865"), first_ask_order.quantity) last_bid_price = Decimal(100 * (1 - 0.01 - (0.01 * 4))).quantize( Decimal("0.001")) last_ask_price = Decimal(100 * (1 + 0.01 + (0.01 * 4))).quantize( Decimal("0.001")) self.assertAlmostEqual(last_bid_price, last_bid_order.price, 3) self.assertAlmostEqual(last_ask_price, last_ask_order.price, 3) self.assertEqual(Decimal("1.95404"), last_bid_order.quantity) self.assertEqual(Decimal("4.04595"), last_ask_order.quantity) def test_inventory_skew_multiple_orders_status(self): strategy = PureMarketMakingStrategy( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=5.0, filled_order_delay=5.0, order_refresh_tolerance_pct=-1, order_levels=5, order_level_spread=Decimal("0.01"), order_level_amount=Decimal("0.5"), inventory_skew_enabled=True, inventory_target_base_pct=Decimal("0.9"), inventory_range_multiplier=Decimal("0.5"), minimum_spread=-1, ) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(5, len(strategy.active_buys)) self.assertEqual(5, len(strategy.active_sells)) status_df: pd.DataFrame = strategy.inventory_skew_stats_data_frame() self.assertEqual("50.0%", status_df.iloc[4, 1]) self.assertEqual("150.0%", status_df.iloc[4, 2]) def test_order_book_asset_del(self): strategy = self.one_level_strategy strategy.asset_price_delegate = self.order_book_asset_del self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.simulate_maker_market_trade( is_buy=True, quantity=Decimal("1"), price=Decimal("123"), market=self.ext_market, ) bid = self.order_book_asset_del.get_price_by_type(PriceType.BestBid) ask = self.order_book_asset_del.get_price_by_type(PriceType.BestAsk) mid_price = self.order_book_asset_del.get_price_by_type( PriceType.MidPrice) last_trade = self.order_book_asset_del.get_price_by_type( PriceType.LastTrade) self.assertEqual((bid + ask) / 2, mid_price) self.assertEqual(last_trade, Decimal("123")) assert isinstance(bid, Decimal) assert isinstance(ask, Decimal) assert isinstance(mid_price, Decimal) assert isinstance(last_trade, Decimal) def test_external_exchange_price_source(self): strategy = self.one_level_strategy strategy.asset_price_delegate = self.order_book_asset_del self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(1, len(strategy.active_buys)) # There should be no sell order, since its price will be below first bid order on the order book. self.assertEqual(0, len(strategy.active_sells)) # check price data from external exchange is used for order placement bid_order = strategy.active_buys[0] self.assertEqual(Decimal("49.5"), bid_order.price) self.assertEqual(Decimal("1.0"), bid_order.quantity) def test_external_exchange_price_source_empty_orderbook(self): simulate_order_book_widening(self.book_data.order_book, 0, 10000) self.assertEqual(0, len(list(self.book_data.order_book.bid_entries()))) self.assertEqual(0, len(list(self.book_data.order_book.ask_entries()))) strategy = self.one_level_strategy strategy.asset_price_delegate = self.order_book_asset_del self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(1, len(strategy.active_buys)) self.assertEqual(1, len(strategy.active_sells)) # check price data from external exchange is used for order placement bid_order = strategy.active_buys[0] self.assertEqual(Decimal("49.5"), bid_order.price) self.assertEqual(Decimal("1.0"), bid_order.quantity) ask_order = strategy.active_sells[0] self.assertEqual(Decimal("50.5"), ask_order.price) self.assertEqual(Decimal("1.0"), ask_order.quantity) def test_multi_order_external_exchange_price_source(self): strategy = self.multi_levels_strategy strategy.asset_price_delegate = self.order_book_asset_del self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(3, len(strategy.active_buys)) # There should be no sell order, since its price will be below first bid order on the order book. self.assertEqual(0, len(strategy.active_sells)) # check price data from external exchange is used for order placement bid_order = strategy.active_buys[0] self.assertEqual(Decimal("49.5"), bid_order.price) self.assertEqual(Decimal("1.0"), bid_order.quantity) last_bid_order = strategy.active_buys[-1] last_bid_price = Decimal(50 * (1 - 0.01 - (0.01 * 2))).quantize( Decimal("0.001")) self.assertAlmostEqual(last_bid_price, last_bid_order.price, 3) self.assertEqual(Decimal("3.0"), last_bid_order.quantity) def test_multi_order_external_exchange_price_source_empty_order_book(self): simulate_order_book_widening(self.book_data.order_book, 0, 10000) self.assertEqual(0, len(list(self.book_data.order_book.bid_entries()))) self.assertEqual(0, len(list(self.book_data.order_book.ask_entries()))) strategy = self.multi_levels_strategy strategy.asset_price_delegate = self.order_book_asset_del self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) # check price data from external exchange is used for order placement bid_order = strategy.active_buys[0] self.assertEqual(Decimal("49.5"), bid_order.price) self.assertEqual(Decimal("1.0"), bid_order.quantity) last_bid_order = strategy.active_buys[-1] last_bid_price = Decimal(50 * (1 - 0.01 - (0.01 * 2))).quantize( Decimal("0.001")) self.assertAlmostEqual(last_bid_price, last_bid_order.price, 3) self.assertEqual(Decimal("3.0"), last_bid_order.quantity) def test_config_spread_on_the_fly_multiple_orders(self): strategy = self.multi_levels_strategy self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + 1) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) self.assertEqual(3, len(strategy.active_buys)) self.assertEqual(3, len(strategy.active_sells)) first_bid_order = strategy.active_buys[0] first_ask_order = strategy.active_sells[0] self.assertEqual(Decimal("99"), first_bid_order.price) self.assertEqual(Decimal("101"), first_ask_order.price) last_bid_order = strategy.active_buys[-1] last_ask_order = strategy.active_sells[-1] self.assertAlmostEqual(Decimal("97"), last_bid_order.price, 2) self.assertAlmostEqual(Decimal("103"), last_ask_order.price, 2) ConfigCommand.update_running_pure_mm(strategy, "bid_spread", Decimal('2')) ConfigCommand.update_running_pure_mm(strategy, "ask_spread", Decimal('2')) self.clock.backtest_til(self.start_timestamp + 7) first_bid_order = strategy.active_buys[0] first_ask_order = strategy.active_sells[0] self.assertEqual(Decimal("98"), first_bid_order.price) self.assertEqual(Decimal("102"), first_ask_order.price) last_bid_order = strategy.active_buys[-1] last_ask_order = strategy.active_sells[-1] self.assertAlmostEqual(Decimal("96"), last_bid_order.price, 2) self.assertAlmostEqual(Decimal("104"), last_ask_order.price, 2) def test_order_override(self): strategy = self.order_override_strategy self.clock.add_iterator(strategy) self.clock.backtest_til(self.start_timestamp + self.clock_tick_size) buys = strategy.active_buys sells = strategy.active_sells self.assertEqual(2, len(buys)) self.assertEqual(1, len(sells)) self.assertEqual(Decimal("99.5"), buys[0].price) self.assertEqual(Decimal("0.7"), buys[0].quantity) self.assertEqual(Decimal("98.7"), buys[1].price) self.assertEqual(Decimal("1.1"), buys[1].quantity) self.assertEqual(Decimal("101.1"), sells[0].price) self.assertEqual(Decimal("2"), sells[0].quantity)
class MarketTradingPairTupleUnitTest(unittest.TestCase): start: pd.Timestamp = pd.Timestamp("2019-01-01", tz="UTC") end: pd.Timestamp = pd.Timestamp("2019-01-01 01:00:00", tz="UTC") start_timestamp: float = start.timestamp() end_timestamp: float = end.timestamp() trading_pair: str = "COINALPHA-HBOT" base_asset, quote_asset = trading_pair.split("-") base_balance: int = 500 quote_balance: int = 5000 initial_mid_price: int = 100 clock_tick_size = 10 def setUp(self): self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp) self.market: MockPaperExchange = MockPaperExchange() self.market.set_balanced_order_book(trading_pair=self.trading_pair, mid_price=100, min_price=50, max_price=150, price_step_size=1, volume_step_size=10) self.market.set_balance("COINALPHA", self.base_balance) self.market.set_balance("HBOT", self.quote_balance) self.market.set_quantization_param( QuantizationParams(self.trading_pair, 6, 6, 6, 6)) self.market_info = MarketTradingPairTuple(self.market, self.trading_pair, self.base_asset, self.quote_asset) @staticmethod def simulate_limit_order_fill(market: MockPaperExchange, limit_order: LimitOrder, timestamp: float = 0): quote_currency_traded: Decimal = limit_order.price * limit_order.quantity base_currency_traded: Decimal = limit_order.quantity quote_currency: str = limit_order.quote_currency base_currency: str = limit_order.base_currency trade_event: OrderBookTradeEvent = OrderBookTradeEvent( trading_pair=limit_order.trading_pair, timestamp=timestamp, type=TradeType.BUY if limit_order.is_buy else TradeType.SELL, price=limit_order.price, amount=limit_order.quantity) market.get_order_book( limit_order.trading_pair).apply_trade(trade_event) if limit_order.is_buy: market.set_balance( quote_currency, market.get_balance(quote_currency) - quote_currency_traded) market.set_balance( base_currency, market.get_balance(base_currency) + base_currency_traded) market.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent(market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.BUY, OrderType.LIMIT, limit_order.price, limit_order.quantity, AddedToCostTradeFee(Decimal(0.0)))) market.trigger_event( MarketEvent.BuyOrderCompleted, BuyOrderCompletedEvent(market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency_traded, quote_currency_traded, OrderType.LIMIT)) else: market.set_balance( quote_currency, market.get_balance(quote_currency) + quote_currency_traded) market.set_balance( base_currency, market.get_balance(base_currency) - base_currency_traded) market.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent(market.current_timestamp, limit_order.client_order_id, limit_order.trading_pair, TradeType.SELL, OrderType.LIMIT, limit_order.price, limit_order.quantity, AddedToCostTradeFee(Decimal(0.0)))) market.trigger_event( MarketEvent.SellOrderCompleted, SellOrderCompletedEvent(market.current_timestamp, limit_order.client_order_id, base_currency, quote_currency, base_currency_traded, quote_currency_traded, OrderType.LIMIT)) @staticmethod def simulate_order_book_update(market_info: MarketTradingPairTuple, n: int, is_bid: bool): # Removes first n bid/ask entries update_id = int(time.time()) if is_bid: new_bids: List[OrderBookRow] = [ OrderBookRow(row.price, 0, row.update_id + 1) for i, row in enumerate(market_info.order_book.bid_entries()) if i < n ] new_asks = [] else: new_asks: List[OrderBookRow] = [ OrderBookRow(row.price, 0, row.update_id + 1) for i, row in enumerate(market_info.order_book.ask_entries()) if i < n ] new_bids = [] market_info.order_book.apply_diffs(new_bids, new_asks, update_id) def test_order_book(self): # Calculate expected OrderBook volume expected_bid_volume: Decimal = Decimal("0") expected_ask_volume: Decimal = Decimal("0") # Calculate bid volume current_price = 100 - 1 / 2 current_size = 10 while current_price >= 50: expected_bid_volume += Decimal(str(current_size)) current_price -= 1 current_size += 10 # Calculate ask volume current_price = 100 + 1 / 2 current_size = 10 while current_price <= 150: expected_ask_volume += Decimal(str(current_size)) current_price += 1 current_size += 10 # Check order book by comparing the total volume # TODO: Determine a better approach to comparing orderbooks current_bid_volume: Decimal = sum([ Decimal(entry.amount) for entry in self.market_info.order_book.bid_entries() ]) current_ask_volume: Decimal = sum([ Decimal(entry.amount) for entry in self.market_info.order_book.ask_entries() ]) self.assertEqual(expected_bid_volume, current_bid_volume) self.assertEqual(expected_ask_volume, current_ask_volume) def test_quote_balance(self): # Check initial balance expected_quote_balance = self.quote_balance self.assertEqual(self.quote_balance, self.market_info.quote_balance) # Simulate an order fill fill_order: LimitOrder = LimitOrder(client_order_id="test", trading_pair=self.trading_pair, is_buy=True, base_currency=self.base_asset, quote_currency=self.quote_asset, price=Decimal("101.0"), quantity=Decimal("10")) self.simulate_limit_order_fill(self.market_info.market, fill_order) # Updates expected quote balance expected_quote_balance = self.quote_balance - (fill_order.price * fill_order.quantity) self.assertNotEqual(self.quote_balance, self.market_info.quote_balance) self.assertEqual(expected_quote_balance, self.market_info.quote_balance) def test_base_balance(self): # Check initial balance expected_base_balance = self.base_balance self.assertEqual(self.base_balance, self.market_info.base_balance) # Simulate order fill fill_order: LimitOrder = LimitOrder(client_order_id="test", trading_pair=self.trading_pair, is_buy=True, base_currency=self.base_asset, quote_currency=self.quote_asset, price=Decimal("101.0"), quantity=Decimal("10")) self.simulate_limit_order_fill(self.market_info.market, fill_order) # Updates expected base balance expected_base_balance = self.base_balance + fill_order.quantity self.assertNotEqual(self.base_balance, self.market_info.base_balance) self.assertEqual(expected_base_balance, self.market_info.base_balance) def test_get_mid_price(self): # Check initial mid price self.assertIs self.assertEqual(Decimal(str(self.initial_mid_price)), self.market_info.get_mid_price()) # Calculate new mid price after removing first n bid entries in orderbook n_entires: int = 10 bid_entries, ask_entries = self.market_info.order_book.snapshot best_bid: Decimal = Decimal( bid_entries.drop(list(range(n_entires))).iloc[1]["price"]) best_ask: Decimal = Decimal(ask_entries.iloc[1]["price"]) expected_mid_price = (best_bid + best_ask) / Decimal("2") # Simulate n bid entries being removed self.simulate_order_book_update(self.market_info, n_entires, True) self.assertNotEqual(Decimal(str(self.initial_mid_price)), self.market_info.get_mid_price()) self.assertEqual(expected_mid_price, self.market_info.get_mid_price()) def test_get_price(self): # Check buy price expected_buy_price: Decimal = min([ entry.price for entry in self.market.order_book_ask_entries(self.trading_pair) ]) self.assertEqual(expected_buy_price, self.market_info.get_price(is_buy=True)) # Check sell price expected_sell_price: Decimal = max([ entry.price for entry in self.market.order_book_bid_entries(self.trading_pair) ]) self.assertEqual(expected_sell_price, self.market_info.get_price(is_buy=False)) def test_get_price_by_type(self): # Check PriceType.BestAsk expected_best_ask: Decimal = max([ entry.price for entry in self.market.order_book_bid_entries(self.trading_pair) ]) self.assertEqual(expected_best_ask, self.market_info.get_price_by_type(PriceType.BestBid)) # Check PriceType.BestAsk expected_best_ask: Decimal = min([ entry.price for entry in self.market.order_book_ask_entries(self.trading_pair) ]) self.assertEqual(expected_best_ask, self.market_info.get_price_by_type(PriceType.BestAsk)) # Check PriceType.MidPrice expected_mid_price: Decimal = Decimal(self.initial_mid_price) self.assertEqual( expected_mid_price, self.market_info.get_price_by_type(PriceType.MidPrice)) # Check initial PriceType.LastTrade self.assertTrue( math.isnan(self.market_info.get_price_by_type( PriceType.LastTrade))) # Simulate fill buy order expected_trade_price = Decimal("101.0") fill_order: LimitOrder = LimitOrder(client_order_id="test", trading_pair=self.trading_pair, is_buy=True, base_currency=self.base_asset, quote_currency=self.quote_asset, price=expected_trade_price, quantity=Decimal("10")) self.simulate_limit_order_fill(self.market_info.market, fill_order) # Check for updated trade price self.assertEqual( expected_trade_price, self.market_info.get_price_by_type(PriceType.LastTrade)) def test_vwap_for_volume(self): # Check VWAP on BUY sell order_volume: Decimal = Decimal("15") filled_orders: List[OrderBookRow] = self.market.get_order_book( self.trading_pair).simulate_buy(order_volume) expected_vwap: Decimal = sum( [Decimal(o.price) * Decimal(o.amount) for o in filled_orders]) / order_volume self.assertAlmostEqual( expected_vwap, self.market_info.get_vwap_for_volume(True, order_volume).result_price, 3) # Check VWAP on SELL side order_volume: Decimal = Decimal("15") filled_orders: List[OrderBookRow] = self.market.get_order_book( self.trading_pair).simulate_sell(order_volume) expected_vwap: Decimal = sum( [Decimal(o.price) * Decimal(o.amount) for o in filled_orders]) / order_volume self.assertAlmostEqual( expected_vwap, self.market_info.get_vwap_for_volume(False, order_volume).result_price, 3) def test_get_price_for_volume(self): # Check price on BUY sell order_volume: Decimal = Decimal("15") filled_orders: List[OrderBookRow] = self.market.get_order_book( self.trading_pair).simulate_buy(order_volume) expected_buy_price: Decimal = max( [Decimal(o.price) for o in filled_orders]) self.assertAlmostEqual( expected_buy_price, self.market_info.get_price_for_volume(True, order_volume).result_price, 3) # Check price on SELL side order_volume: Decimal = Decimal("15") filled_orders: List[OrderBookRow] = self.market.get_order_book( self.trading_pair).simulate_sell(order_volume) expected_sell_price: Decimal = min( [Decimal(o.price) for o in filled_orders]) self.assertAlmostEqual( expected_sell_price, self.market_info.get_price_for_volume(False, order_volume).result_price, 3) def test_order_book_bid_entries(self): # Check all entries. order_book: OrderBook = self.market.get_order_book(self.trading_pair) bid_entries: List[OrderBookRow] = order_book.bid_entries() self.assertTrue( set(bid_entries).intersection( set(self.market_info.order_book_bid_entries()))) def test_order_book_ask_entries(self): # Check all entries. order_book: OrderBook = self.market.get_order_book(self.trading_pair) ask_entries: List[OrderBookRow] = order_book.ask_entries() self.assertTrue( set(ask_entries).intersection( set(self.market_info.order_book_ask_entries())))