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
Example #2
0
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