class Test_ExchangePairAccessor(TestCase): def setUp(self): """ It's patching time """ #http://www.voidspace.org.uk/python/mock/examples.html#mocking-imports-with-patch-dict self.settings_mock = MagicMock() self.settings_mock.DRY_RUN = False self.settings_mock.BACKTEST = True self.settings_mock.SYMBOL='XBTUSD' self.settings_mock.TICK_SIZE = {'XBTUSD': 0.5, 'ETHUSD': 0.05} def tearDown(self): """ Let's clean up """ @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect=mock_csv_reader_generator(bitmex_trades_file)) @patch('market_maker.backtest.exchangepairaccessor.open') def test_calls_timekeeper(self, new_open, reader_function): self.timekeeper = MagicMock() self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", settings = self.settings_mock) print(self.bt._trade_data) self.timekeeper.contribute_times.assert_called_with(timekeeper_parameters) new_open.assert_called_with("fake.csv") @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect=mock_csv_reader_generator(bitmex_trades_file)) @patch('market_maker.backtest.exchangepairaccessor.open') def test_EPA_check_timestamps_sync(self, new_open, reader_function): self.timekeeper = Timekeeper() self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", settings = self.settings_mock) #now load GDAX reader_function.side_effect = mock_csv_reader_generator(gdax_trades_file) self.gdax_accessor = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", settings = self.settings_mock) self.timekeeper.initialize() assert self.bt.current_timestamp() == self.gdax_accessor.current_timestamp() #increment time and check again self.timekeeper.increment_time() assert self.bt.current_timestamp() == self.gdax_accessor.current_timestamp() @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect=mock_csv_reader_generator(bitmex_trades_file)) @patch('market_maker.backtest.exchangepairaccessor.open') def test_EPA_fail_if_not_warm(self, new_open, reader_function): self.timekeeper = Timekeeper() self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", settings = self.settings_mock) #now load GDAX reader_function.side_effect = mock_csv_reader_generator(gdax_trades_file) self.gdax_accessor = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", settings = self.settings_mock) self.timekeeper.initialize() self.assertRaises(Exception, self.bt.recent_trades) @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect=mock_csv_reader_generator(bitmex_trades_file)) @patch('market_maker.backtest.exchangepairaccessor.open') def test_EPA_price_should_be_correct_at_time(self, new_open, reader_function): self.timekeeper = Timekeeper() self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", settings = self.settings_mock) #now load GDAX reader_function.side_effect = mock_csv_reader_generator(gdax_trades_file) self.gdax_accessor = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", settings = self.settings_mock) self.timekeeper.initialize() while not(self.bt.is_warm() & self.gdax_accessor.is_warm()): self.timekeeper.increment_time() assert self.bt.recent_trades()[-1]['price'] == 7017 assert self.gdax_accessor.recent_trades()[-1]['price'] == 7015.01 @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect= multiple_calls) @patch('market_maker.backtest.exchangepairaccessor.open') def test_EPA_should_return_trades_and_orderbook(self, new_open, reader_function): self.timekeeper = Timekeeper() self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", L2orderbook_filename = "fake2.csv", settings = self.settings_mock) print(self.settings_mock.EARLY_STOP_TIME is not None) self.timekeeper.initialize() # increment time to get warm while not self.bt.is_warm(): self.timekeeper.increment_time() assert self.bt.recent_trades() != [] assert self.bt.market_depth("") != [] assert isinstance(self.bt.market_depth("")[-1]['size'], float) assert isinstance(self.bt.market_depth("")[-1]['price'], float) assert self.bt.market_depth("")[-1]['side'] in ['Sell', 'Buy'] @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect= multiple_calls_generator()) @patch('market_maker.backtest.exchangepairaccessor.open') def test_EPA_early_stopping(self, new_open, reader_function): end_time = datetime.time(23, 59, 53, 978000) self.settings_mock.EARLY_STOP_TIME = "23:59:53.9780000" self.timekeeper = Timekeeper() self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", L2orderbook_filename = "fake2.csv", settings = self.settings_mock) self.timekeeper.initialize() saved_time = self.bt.current_timestamp() while True: try: self.timekeeper.increment_time() saved_time = self.bt.recent_trades()[-1]['time_object'] #print(saved_time) except: break print(end_time) print(saved_time) assert saved_time.time() <= end_time @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect= multiple_calls_generator()) @patch('market_maker.backtest.exchangepairaccessor.open') def test_EPA_values_still_accessible_after_EOF(self, new_open, reader_function): self.timekeeper = Timekeeper() self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", L2orderbook_filename = "fake2.csv", settings = self.settings_mock) print(self.settings_mock.EARLY_STOP_TIME is not None) self.timekeeper.initialize() # increment time to get warm while True: try: self.timekeeper.increment_time() except: break #Call a bunch of functions, should not raise exception assert self.bt.current_timestamp() assert self.bt.is_warm() assert self.bt.instrument() assert self.bt.ticker_data() assert self.bt.market_depth("") assert self.bt.recent_trades() #Call wait_update, should raise EOFError self.assertRaises(EOFError, self.bt.wait_update) @patch('market_maker.backtest.exchangepairaccessor.csv.reader', side_effect= multiple_calls_generator()) @patch('market_maker.backtest.exchangepairaccessor.open') def test_EPA_starts_after_start_time(self, new_open, reader_function): self.timekeeper = Timekeeper() start_time = datetime.time(23, 59, 53, 988000) self.settings_mock.get.return_value = "23:59:53.988000" self.settings_mock.START_TIME = "23:59:53.988000" self.bt = ExchangePairAccessor(timekeeper = self.timekeeper, trades_filename = "fake.csv", L2orderbook_filename = "fake2.csv", settings = self.settings_mock) print(self.settings_mock.START_TIME is not None) self.timekeeper.initialize() # increment time to get warm print(self.bt.current_timestamp()) assert self.bt.current_timestamp().time() >= start_time
class BacktestInterface: def __init__(self, timekeeper=None, trades_filename="", settings=None, logger=None, L2orderbook_filename="", name="", orderqty_type='USD'): self.settings = settings self.paper = paper_trading.PaperTrading(settings=self.settings, logger=logger) if timekeeper == None: self.timekeeper = Timekeeper() self.own_timekeeper = True else: self.own_timekeeper = False self.timekeeper = timekeeper self.accessor = ExchangePairAccessor(self.timekeeper, trades_filename, L2orderbook_filename, name=name, settings=self.settings) self.paper.provide_exchange(self.accessor) self.paper.reset() self.orderIDPrefix = "BT_" if self.own_timekeeper: self.timekeeper.initialize() self.symbol = self.settings.symbol self.last_order_time = None self.name = name self.orderqty_type = orderqty_type def is_warm(self): return self.accessor.is_warm() def get_orderqty_type(self): return self.orderqty_type def cancel_order(self, order): tickLog = self.get_instrument()['tickLog'] logger.info( "Canceling: %s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price'])) return self.paper.cancel_order(order['orderID']) def cancel_all_orders(self): logger.info( "Resetting current position. Canceling all existing orders.") return self.paper.cancel_all_orders() def get_portfolio(self): contracts = self.settings.CONTRACTS portfolio = {} for symbol in contracts: position = self.paper.get_position(symbol=symbol) instrument = self.accessor.instrument(symbol=symbol) if instrument['isQuanto']: future_type = "Quanto" elif instrument['isInverse']: future_type = "Inverse" elif not instrument['isQuanto'] and not instrument['isInverse']: future_type = "Linear" else: raise NotImplementedError( "Unknown future type; not quanto or inverse: %s" % instrument['symbol']) if instrument['underlyingToSettleMultiplier'] is None: multiplier = float(instrument['multiplier']) / float( instrument['quoteToSettleMultiplier']) else: multiplier = float(instrument['multiplier']) / float( instrument['underlyingToSettleMultiplier']) portfolio[symbol] = { "currentQty": float(position['currentQty']), "futureType": future_type, "multiplier": multiplier, "markPrice": float(instrument['markPrice']), "spot": float(instrument['indicativeSettlePrice']) } return portfolio def calc_delta(self): """Calculate currency delta for portfolio""" portfolio = self.get_portfolio() spot_delta = 0 mark_delta = 0 for symbol in portfolio: item = portfolio[symbol] if item['futureType'] == "Quanto": spot_delta += item['currentQty'] * item['multiplier'] * item[ 'spot'] mark_delta += item['currentQty'] * item['multiplier'] * item[ 'markPrice'] elif item['futureType'] == "Inverse": spot_delta += (item['multiplier'] / item['spot']) * item['currentQty'] mark_delta += (item['multiplier'] / item['markPrice']) * item['currentQty'] elif item['futureType'] == "Linear": spot_delta += item['multiplier'] * item['currentQty'] mark_delta += item['multiplier'] * item['currentQty'] basis_delta = mark_delta - spot_delta delta = { "spot": spot_delta, "mark_price": mark_delta, "basis": basis_delta } return delta def get_delta(self, symbol=None): return self.paper.current_contract() def get_instrument(self, symbol=None): if symbol is None: symbol = self.symbol return self.accessor.instrument(symbol) def get_margin(self): return self.paper.get_funds() def get_orders(self): return self.paper.get_orders() def get_highest_buy(self): buys = [o for o in self.get_orders() if o['side'] == 'Buy'] if not len(buys): return {'price': -2**32} highest_buy = max(buys or [], key=lambda o: o['price']) return highest_buy if highest_buy else {'price': -2**32} def get_lowest_sell(self): sells = [o for o in self.get_orders() if o['side'] == 'Sell'] if not len(sells): return {'price': 2**32} lowest_sell = min(sells or [], key=lambda o: o['price']) return lowest_sell if lowest_sell else { 'price': 2**32 } # ought to be enough for anyone def get_position(self, symbol=None): return self.paper.get_position(symbol) def get_ticker(self, symbol=None): return self.accessor.ticker_data(symbol) def get_orderbook_time(self): return self.accessor.get_orderbook_time() def is_open(self): return True def check_market_open(self): return True def check_if_orderbook_empty(self): """This function checks whether the order book is empty""" #let's force an order book if we own timekeeper ob = self.accessor.market_depth("") if self.own_timekeeper: while ob == []: self.timekeeper.increment_time() ob = self.accessor.market_depth("") else: return ob == [] def amend_bulk_orders(self, orders): self.last_order_time = self._current_timestamp() self.paper.cancel_all_orders() self.paper.track_orders_created(orders) def create_bulk_orders(self, orders): if not isinstance(orders, list): raise ValueError( "backtest_interface.create_bulk_orders expects a list of orders" ) self.last_order_time = self._current_timestamp() for order in orders: order['clOrdID'] = self.orderIDPrefix + base64.b64encode( uuid.uuid4().bytes).decode('utf8').rstrip('=\n') self.paper.track_orders_created(orders) return orders def cancel_bulk_orders(self, orders): self.last_order_time = self._current_timestamp() for order in orders: self.cancel_order(order) def recent_trades(self): return self.accessor.recent_trades() def market_deep(self): return self.accessor.market_depth("") def current_timestamp(self): return self.accessor.current_timestamp().timestamp() def contracts_this_run(self): return self.paper.contract_traded_this_run() def _current_timestamp(self): return self.accessor.current_timestamp().timestamp() def ok_to_enter_order(self): '''Used to rate limit the placement of orders. Should work in backtest.''' if self.last_order_time: time_since_last = self._current_timestamp() - self.last_order_time # force a 1 second wait for now if time_since_last > max(self.current_api_call_timing(), 1): self.last_order_time = self._current_timestamp() return True else: return False else: self.last_order_time = self._current_timestamp() return True def current_api_call_timing(self): '''calculates the recommended time until next API call''' return 0.0 def loop(self): self.accessor.wait_update() self.paper.loop_functions() def wait_update(self): self.paper.loop_functions() try: if self.own_timekeeper: self.timekeeper.increment_time() self.accessor.wait_update() except EOFError: raise except: logger.error( "Unknown error occurred in backtest_interface.wait_update") raise return True def exit_exchange(self): pass