def main(): # Define the data cache path. hummingsim.set_data_path(os.path.join(os.environ["PWD"], "data")) # Define the parameters for the backtest. start = pd.Timestamp("2019-01-01", tz="UTC") end = pd.Timestamp("2019-01-02", tz="UTC") binance_trading_pair = ("ETHUSDT", "ETH", "USDT") # ddex_trading_pair = ("WETH-DAI", "WETH", "DAI") binance_market = BacktestMarket() ddex_market = BacktestMarket() binance_market.config = MarketConfig(AssetType.BASE_CURRENCY, 0.001, AssetType.QUOTE_CURRENCY, 0.001, {}) ddex_market.config = MarketConfig(AssetType.BASE_CURRENCY, 0.001, AssetType.QUOTE_CURRENCY, 0.001, {}) binance_loader = BinanceOrderBookLoaderV2(*binance_trading_pair) #ddex_loader = DDEXOrderBookLoader(*ddex_trading_pair) binance_market.add_data(binance_loader) #ddex_market.add_data(ddex_loader) binance_market.set_quantization_param( QuantizationParams("ETHUSDT", 5, 3, 5, 3)) #ddex_market.set_quantization_param(QuantizationParams("WETH-DAI", 5, 3, 5, 3)) market_pair = PureMarketPair(*([binance_market] + list(binance_trading_pair))) strategy = PureMarketMakingStrategy( [market_pair], order_size=50000, bid_place_threshold=0.003, ask_place_threshold=0.003, logging_options=PureMarketMakingStrategy.OPTION_LOG_ALL) clock = Clock(ClockMode.BACKTEST, tick_size=60, start_time=start.timestamp(), end_time=end.timestamp()) clock.add_iterator(binance_market) #clock.add_iterator(ddex_market) clock.add_iterator(strategy) binance_market.set_balance("ETH", 100000.0) binance_market.set_balance("USDT", 100000000.0) ddex_market.set_balance("WETH", 100000.0) ddex_market.set_balance("DAI", 1000.0) current = start.timestamp() step = 60 while current <= end.timestamp(): current += step clock.backtest_til(current) print("clock ticked") binance_loader.close()
class PyTimeIteratorUnitTest(unittest.TestCase): start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp() end_timestamp: float = pd.Timestamp("2022-01-01 01:00:00", tz="UTC").timestamp() tick_size: int = 10 def setUp(self): self.py_time_iterator = MockPyTimeIterator() self.clock = Clock(ClockMode.BACKTEST, self.tick_size, self.start_timestamp, self.end_timestamp) self.clock.add_iterator(self.py_time_iterator) def test_current_timestamp(self): # On initialization, current_timestamp should be NaN self.assertTrue(math.isnan(self.py_time_iterator.current_timestamp)) self.py_time_iterator.start(self.clock) self.clock.backtest_til(self.start_timestamp) self.assertEqual(self.start_timestamp, self.py_time_iterator.current_timestamp) def test_clock(self): # On initialization, clock should be None self.assertTrue(self.py_time_iterator.clock is None) self.py_time_iterator.start(self.clock) self.assertEqual(self.clock, self.py_time_iterator.clock) def test_start(self): self.py_time_iterator.start(self.clock) self.assertEqual(self.clock, self.py_time_iterator.clock) self.assertEqual(self.start_timestamp, self.py_time_iterator.current_timestamp) def test_stop(self): self.py_time_iterator.start(self.clock) self.assertEqual(self.clock, self.py_time_iterator.clock) self.assertEqual(self.start_timestamp, self.py_time_iterator.current_timestamp) self.py_time_iterator.stop(self.clock) self.assertTrue(math.isnan(self.py_time_iterator.current_timestamp)) self.assertTrue(self.py_time_iterator.clock is None) def test_tick(self): self.py_time_iterator.start(self.clock) self.assertEqual(self.start_timestamp, self.py_time_iterator.current_timestamp) # c_tick is called within Clock self.clock.backtest_til(self.start_timestamp + self.tick_size) self.assertEqual(self.start_timestamp + self.tick_size, self.py_time_iterator.current_timestamp) self.assertEqual(self.start_timestamp + self.tick_size, self.py_time_iterator.mock_variable)
0.025, logging_options=ArbitrageStrategy.OPTION_LOG_CREATE_ORDER) clock = Clock(ClockMode.BACKTEST, start_time=start.timestamp(), end_time=end.timestamp()) clock.add_iterator(binance_market) clock.add_iterator(ddex_market) clock.add_iterator(strategy) binance_market.set_balance("ETH", 100.0) binance_market.set_balance("USDT", 10000.0) ddex_market.set_balance("WETH", 100.0) ddex_market.set_balance("DAI", 10000.0) clock.backtest_til(start.timestamp() + 1) ddex_weth_price = ddex_market.get_price("WETH-DAI", False) binance_eth_price = binance_market.get_price("ETHUSDT", False) start_ddex_portfolio_value = ddex_market.get_balance( "DAI") + ddex_market.get_balance("WETH") * ddex_weth_price start_binance_portfolio_value = binance_market.get_balance( "USDT") + binance_market.get_balance("ETH") * binance_eth_price print(f"start DDEX portfolio value: {start_ddex_portfolio_value}\n" f"start Binance portfolio value: {start_binance_portfolio_value}") clock.backtest_til(end.timestamp()) ddex_weth_price = ddex_market.get_price("WETH-DAI", False) binance_eth_price = binance_market.get_price("ETHUSDT", False) ddex_portfolio_value = ddex_market.get_balance(
class CompositeOrderBookTest(unittest.TestCase): start: pd.Timestamp = pd.Timestamp("2019-01-25", tz="UTC") end: pd.Timestamp = pd.Timestamp("2019-01-26", tz="UTC") def setUp(self): self.weth_dai_data = DDEXOrderBookLoader("WETH-DAI", "WETH", "DAI") self.clock = Clock(ClockMode.BACKTEST, 1.0, self.start.timestamp(), self.end.timestamp()) self.market = BacktestMarket() self.market.add_data(self.weth_dai_data) self.market.set_balance("WETH", 200.0) self.market.set_balance("DAI", 20000.0) self.clock.add_iterator(self.market) def tearDown(self): self.weth_dai_data.close() def verify_filled_order_recorded(self, recorded_filled_events, composite_order_book): bid_dict = { entry.price: (entry.amount, entry.update_id) for entry in composite_order_book.traded_order_book.bid_entries() } ask_dict = { entry.price: (entry.amount, entry.update_id) for entry in composite_order_book.traded_order_book.ask_entries() } for index, fill_event in recorded_filled_events.iterrows(): if fill_event.trade_type is TradeType.SELL: self.assertTrue(fill_event.price in bid_dict) self.assertTrue( bid_dict[fill_event.price][0] == fill_event.amount) self.assertTrue( bid_dict[fill_event.price][1] == fill_event.timestamp) elif fill_event.trade_type is TradeType.BUY: self.assertTrue(fill_event.price in ask_dict) self.assertTrue( ask_dict[fill_event.price][0] == fill_event.amount) self.assertTrue( ask_dict[fill_event.price][1] == fill_event.timestamp) def verify_composite_order_book_correctness(self, composite_order_book): filled_bid_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.traded_order_book.bid_entries() } filled_ask_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.traded_order_book.ask_entries() } composite_bid_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.bid_entries() } composite_ask_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.ask_entries() } original_bid_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.original_bid_entries() } original_ask_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.original_ask_entries() } for filled_bid_price, filled_bid_amount in filled_bid_dict.items(): if filled_bid_price in original_bid_dict: if (original_bid_dict[filled_bid_price] - filled_bid_amount) <= 0: self.assertTrue(filled_bid_price not in composite_bid_dict) else: self.assertTrue(composite_bid_dict[filled_bid_price] == original_bid_dict[filled_bid_price] - filled_bid_amount) for filled_ask_price, filled_ask_amount in filled_ask_dict.items(): if filled_ask_price in original_ask_dict: if (original_bid_dict[filled_ask_price] - filled_ask_amount) <= 0: self.assertTrue(filled_ask_price not in composite_ask_dict) else: self.assertTrue(composite_bid_dict[filled_ask_price] == original_bid_dict[filled_ask_price] - filled_ask_amount) def verify_composite_order_book_cleanup(self, recorded_filled_events, composite_order_book): """ Recorded fill order should be cleaned up when the original order book no longer contain that price entry """ filled_bid_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.traded_order_book.bid_entries() } filled_ask_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.traded_order_book.ask_entries() } original_bid_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.original_bid_entries() } original_ask_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.original_ask_entries() } for index, fill_event in recorded_filled_events.iterrows(): if fill_event.trade_type is TradeType.SELL: if fill_event.price not in original_bid_dict: self.assertTrue(fill_event.price not in filled_bid_dict) elif fill_event.trade_type is TradeType.BUY: if fill_event.price not in original_ask_dict: self.assertTrue(fill_event.price not in filled_ask_dict) def verify_composite_order_book_adjustment(self, composite_order_book): """ Recorded fill order sohuld adjust it's amount to no larger than the original price entries' amount """ filled_bid_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.traded_order_book.bid_entries() } filled_ask_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.traded_order_book.ask_entries() } original_bid_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.original_bid_entries() } original_ask_dict = { o.price: (o.amount, o.update_id) for o in composite_order_book.original_ask_entries() } for filled_bid_price, filled_bid_entry in filled_bid_dict.items(): if filled_bid_price in original_bid_dict: self.assertTrue(original_bid_dict[filled_bid_price][0] >= filled_bid_entry[0]) for filled_ask_price, filled_ask_entry in filled_ask_dict.items(): if filled_ask_price in original_ask_dict: self.assertTrue(original_ask_dict[filled_ask_price][0] >= filled_ask_entry[0]) def test_market_order(self): trades = { pd.Timestamp("2019-01-25 00:00:10+00:00").timestamp(): [("WETH-DAI", "buy", 5.0), ("WETH-DAI", "sell", 5.0)] } strategy: CompositeOrderBookTestStrategy = CompositeOrderBookTestStrategy( self.market, trades) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start.timestamp() + 10) self.verify_filled_order_recorded( strategy.order_filled_events, self.market.get_order_book("WETH-DAI")) self.verify_composite_order_book_correctness( self.market.get_order_book("WETH-DAI")) self.clock.backtest_til(self.start.timestamp() + 70) self.verify_composite_order_book_cleanup( strategy.order_filled_events, self.market.get_order_book("WETH-DAI")) def test_composite_order_book_adjustment(self): trades = { pd.Timestamp("2019-01-25 00:02:15+00:00").timestamp(): [("WETH-DAI", "sell", 93.53 + 23.65)] } strategy: CompositeOrderBookTestStrategy = CompositeOrderBookTestStrategy( self.market, trades) self.clock.add_iterator(strategy) self.clock.backtest_til(self.start.timestamp() + 60 * 2 + 15) self.clock.backtest_til(self.start.timestamp() + 60 * 2 + 25) self.verify_composite_order_book_adjustment( self.market.get_order_book("WETH-DAI"))
class OrderExpirationTest(unittest.TestCase): start: pd.Timestamp = pd.Timestamp("2019-01-24", tz="UTC") end: pd.Timestamp = pd.Timestamp("2019-01-26", tz="UTC") market_name = "ETHUSDT" quote = "ETH" base = "USDT" def setUp(self): #self.weth_dai_data = DDEXOrderBookLoader("WETH-DAI", "WETH", "DAI") self.pair_data = BinanceOrderBookLoaderV2(self.market_name, "ETH", "USDT") #self.pair_data = HuobiOrderBookLoader(self.market_name, "", "") self.clock = Clock(ClockMode.BACKTEST, 1.0, self.start.timestamp(), self.end.timestamp()) self.market = BacktestMarket() #self.market.add_data(self.weth_dai_data) self.market.add_data(self.pair_data) self.market.set_balance(self.quote, 200.0) self.market.set_balance(self.base, 20000.0) self.clock.add_iterator(self.market) def tearDown(self): #self.weth_dai_data.close() #self.eth_usd_data.close() self.pair_data.close() def verify_expired_order_cleanup(self, order_expired_events, limit_orders): """ Recorded order expired event should indicate that these orders are no longer in the limit orders """ limit_order_dict = {o.client_order_id: o for o in limit_orders} for index, order_expired_event in order_expired_events.iterrows(): self.assertTrue( order_expired_event.order_id not in limit_order_dict) def test_ask_order_expiration_clean_up(self): ts_1 = pd.Timestamp("2019-01-24 00:02:15+00:00").timestamp() ts_2 = pd.Timestamp("2019-01-24 00:02:20+00:00").timestamp() trades = { ts_1: [(self.market_name, "sell", 1302, 255, { "expiration_ts": ts_1 + 9 })], ts_2: [(self.market_name, "sell", 1302, 250, { "expiration_ts": ts_2 + 9 })] } strategy: OrderExpirationTestStrategy = OrderExpirationTestStrategy( self.market, trades) self.clock.add_iterator(strategy) # first limit order made self.clock.backtest_til(self.start.timestamp() + 60 * 2 + 15) first_order_id = self.market.limit_orders[0].client_order_id self.assertTrue(len(self.market.limit_orders) == 1) self.assertTrue(first_order_id in {o.order_id: o for o in self.market.order_expirations}) # second limit order made self.clock.backtest_til(self.start.timestamp() + 60 * 2 + 20) self.assertTrue(len(self.market.limit_orders) == 2) # first limit order expired self.clock.backtest_til(self.start.timestamp() + 60 * 2 + 25) # check if order expired event is fired self.assertTrue(first_order_id in [ evt.order_id for i, evt in strategy.order_expired_events.iterrows() ]) # check if the expired limit order is cleaned up self.verify_expired_order_cleanup(strategy.order_expired_events, self.market.limit_orders) self.assertTrue(len(self.market.limit_orders) == 1) second_order_id = self.market.limit_orders[0].client_order_id self.assertTrue(second_order_id in {o.order_id: o for o in self.market.order_expirations}) # second limit order expired self.clock.backtest_til(self.start.timestamp() + 60 * 2 + 30) # check if order expired event is fired self.assertTrue(second_order_id in [ evt.order_id for i, evt in strategy.order_expired_events.iterrows() ]) # check if the expired limit order is cleaned up self.verify_expired_order_cleanup(strategy.order_expired_events, self.market.limit_orders) def test_bid_order_expiration_clean_up(self): ts_1 = pd.Timestamp("2019-01-24 00:12:15+00:00").timestamp() ts_2 = pd.Timestamp("2019-01-24 00:12:20+00:00").timestamp() trades = { ts_1: [(self.market_name, "buy", 100, 55, { "expiration_ts": ts_1 + 9 })], ts_2: [(self.market_name, "buy", 100, 50, { "expiration_ts": ts_2 + 9 }), (self.market_name, "buy", 100, 55, { "expiration_ts": ts_2 + 9 })] } strategy: OrderExpirationTestStrategy = OrderExpirationTestStrategy( self.market, trades) self.clock.add_iterator(strategy) # first limit order made self.clock.backtest_til(self.start.timestamp() + 60 * 12 + 15) first_order_id = self.market.limit_orders[0].client_order_id self.assertTrue(len(self.market.limit_orders) == 1) self.assertTrue(first_order_id in {o.order_id: o for o in self.market.order_expirations}) # second limit order made self.clock.backtest_til(self.start.timestamp() + 60 * 12 + 20) self.assertTrue(len(self.market.limit_orders) == 3) # first limit order expired self.clock.backtest_til(self.start.timestamp() + 60 * 12 + 25) # check if order expired event is fired self.assertTrue(first_order_id in [ evt.order_id for i, evt in strategy.order_expired_events.iterrows() ]) # check if the expired limit order is cleaned up self.verify_expired_order_cleanup(strategy.order_expired_events, self.market.limit_orders) self.assertTrue(len(self.market.limit_orders) == 2) second_order_id_1 = self.market.limit_orders[0].client_order_id second_order_id_2 = self.market.limit_orders[1].client_order_id self.assertTrue(second_order_id_1 in {o.order_id: o for o in self.market.order_expirations}) self.assertTrue(second_order_id_2 in {o.order_id: o for o in self.market.order_expirations}) # second limit order expired self.clock.backtest_til(self.start.timestamp() + 60 * 12 + 30) # check if order expired event is fired self.assertTrue(second_order_id_1 in [ evt.order_id for i, evt in strategy.order_expired_events.iterrows() ]) self.assertTrue(second_order_id_2 in [ evt.order_id for i, evt in strategy.order_expired_events.iterrows() ]) # check if the expired limit order is cleaned up self.verify_expired_order_cleanup(strategy.order_expired_events, self.market.limit_orders)
class ClockUnitTest(unittest.TestCase): backtest_start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp() backtest_end_timestamp: float = pd.Timestamp("2021-01-01 01:00:00", tz="UTC").timestamp() tick_size: int = 1 @classmethod def setUpClass(cls): super().setUpClass() cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() def setUp(self): super().setUp() self.realtime_start_timestamp = time.time() self.realtime_end_timestamp = self.realtime_start_timestamp + 2.0 # self.clock_realtime = Clock(ClockMode.REALTIME, self.tick_size, self.realtime_start_timestamp, self.realtime_end_timestamp) self.clock_backtest = Clock(ClockMode.BACKTEST, self.tick_size, self.backtest_start_timestamp, self.backtest_end_timestamp) def test_clock_mode(self): self.assertEqual(ClockMode.REALTIME, self.clock_realtime.clock_mode) self.assertEqual(ClockMode.BACKTEST, self.clock_backtest.clock_mode) def test_start_time(self): self.assertEqual(self.realtime_start_timestamp, self.clock_realtime.start_time) self.assertEqual(self.backtest_start_timestamp, self.clock_backtest.start_time) def test_tick_time(self): self.assertEqual(self.tick_size, self.clock_realtime.tick_size) self.assertEqual(self.tick_size, self.clock_backtest.tick_size) def test_child_iterators(self): # Tests child_iterators property after initialization. See also test_add_iterator self.assertEqual(0, len(self.clock_realtime.child_iterators)) self.assertEqual(0, len(self.clock_backtest.child_iterators)) def test_current_timestamp(self): self.assertEqual(self.backtest_start_timestamp, self.clock_backtest.current_timestamp) self.assertAlmostEqual( (self.realtime_start_timestamp // self.tick_size) * self.tick_size, self.clock_realtime.current_timestamp) self.clock_backtest.backtest() self.clock_realtime.backtest() self.assertEqual(self.backtest_end_timestamp, self.clock_backtest.current_timestamp) self.assertLessEqual(self.realtime_end_timestamp, self.clock_realtime.current_timestamp) def test_add_iterator(self): self.assertEqual(0, len(self.clock_realtime.child_iterators)) self.assertEqual(0, len(self.clock_backtest.child_iterators)) time_iterator: TimeIterator = TimeIterator() self.clock_realtime.add_iterator(time_iterator) self.clock_backtest.add_iterator(time_iterator) self.assertEqual(1, len(self.clock_realtime.child_iterators)) self.assertEqual(time_iterator, self.clock_realtime.child_iterators[0]) self.assertEqual(1, len(self.clock_backtest.child_iterators)) self.assertEqual(time_iterator, self.clock_backtest.child_iterators[0]) def test_remove_iterator(self): self.assertEqual(0, len(self.clock_realtime.child_iterators)) self.assertEqual(0, len(self.clock_backtest.child_iterators)) time_iterator: TimeIterator = TimeIterator() self.clock_realtime.add_iterator(time_iterator) self.clock_backtest.add_iterator(time_iterator) self.assertEqual(1, len(self.clock_realtime.child_iterators)) self.assertEqual(time_iterator, self.clock_realtime.child_iterators[0]) self.assertEqual(1, len(self.clock_backtest.child_iterators)) self.assertEqual(time_iterator, self.clock_backtest.child_iterators[0]) self.clock_realtime.remove_iterator(time_iterator) self.clock_backtest.remove_iterator(time_iterator) self.assertEqual(0, len(self.clock_realtime.child_iterators)) self.assertEqual(0, len(self.clock_backtest.child_iterators)) def test_run(self): # Note: Technically you do not execute `run()` when in BACKTEST mode # Tests EnvironmentError raised when not runnning within a context with self.assertRaises(EnvironmentError): self.ev_loop.run_until_complete(self.clock_realtime.run()) # Note: run() will essentially run indefinitely hence the enforced timeout. with self.assertRaises(asyncio.TimeoutError), self.clock_realtime: self.ev_loop.run_until_complete( asyncio.wait_for(self.clock_realtime.run(), 1)) self.assertLess(self.realtime_start_timestamp, self.clock_realtime.current_timestamp) def test_run_til(self): # Note: Technically you do not execute `run_til()` when in BACKTEST mode # Tests EnvironmentError raised when not runnning within a context with self.assertRaises(EnvironmentError): self.ev_loop.run_until_complete( self.clock_realtime.run_til(self.realtime_end_timestamp)) with self.clock_realtime: self.ev_loop.run_until_complete( self.clock_realtime.run_til(self.realtime_end_timestamp)) self.assertGreaterEqual(self.clock_realtime.current_timestamp, self.realtime_end_timestamp) def test_backtest(self): # Note: Technically you do not execute `backtest()` when in REALTIME mode self.clock_backtest.backtest() self.assertGreaterEqual(self.clock_backtest.current_timestamp, self.backtest_end_timestamp) def test_backtest_til(self): # Note: Technically you do not execute `backtest_til()` when in REALTIME mode self.clock_backtest.backtest_til(self.backtest_start_timestamp + self.tick_size) self.assertGreater(self.clock_backtest.current_timestamp, self.clock_backtest.start_time) self.assertLess(self.clock_backtest.current_timestamp, self.backtest_end_timestamp)