class OrderManager:
    def __init__(self):
        self.exchange = ExchangeInterface(settings.DRY_RUN)
        # Once exchange is created, register exit handler that will always cancel orders on any error.
        atexit.register(self.exit)
        signal.signal(signal.SIGTERM, self.exit)
        logger.info("Using symbol %s." % self.exchange.symbol)
        # self.start_time = datetime.utcnow()
        self.instrument = self.exchange.get_instrument()
        self.starting_qty = self.exchange.get_currentQty()
        self.running_qty = self.starting_qty
        self.reset()

    def reset(self):
        self.exchange.cancel_all_orders()
        self.exchange.close_position()
        self.sanity_check()
        #self.print_status()
        if settings.DRY_RUN:
            sys.exit()

    def exit(self):
        """
        Function to execute when exit handle is called
        """
        logger.info("Shutting down. All open orders will be cancelled.")
        try:
            self.exchange.cancel_all_orders()
            self.exchange.close_position()
        except errors.AuthenticationError as e:
            logger.info("Was not authenticated; could not close orders.")
        except Exception as e:
            logger.info("Unable to terminate correctly: %s" % e)
        sys.exit()

    def print_status(self):
        logger.info("PRINT STATUS IS DEPRECATED. REPLACED BY TAPE LOGGING")
        """Print the current status"""
        margin = self.exchange.get_margin()
        position = self.exchange.get_simpleCost()
        if position and margin:
            logger.info("XBT Balance : %.6f" % margin)
            logger.info("Open Position: {}".format(position))

    def sanity_check(self):
        # Check if OB is empty - if so, can't quote.
        # self.exchange.check_if_orderbook_empty()
        # Ensure market is still open.
        self.exchange.check_market_open()

    def momentum(self, seria, periods, array_length):
        # for x in range 0 to array_length, mom becomes a list ...
        # @periods defines how many candles back to use for the momentum calculation
        mom = []
        for x in range(array_length):
            mom.append(seria[x] - seria[periods + x])
        return mom

    def mom_strategy(self, quotes_df):
        """
        If you are selling a stock, you are going to get the BID price, if you are buying a stock you are going to get the ASK price.
        """
        current_time = quotes_df.index.tolist()[0]  # extrapolate latest time
        # print(current_time)
        # rather than grabbing the quote from the last minute, use the current ticker
        open_position = self.exchange.get_simpleCost()

        # for logging purposes
        # full_position = self.exchange.get_position()
        # position = 0
        # print(full_position)
        # if (full_position['isOpen']):
        # position = full_position['simpleCost']

        unrealisedPnlPcnt = self.exchange.get_unrealisedPnlPcnt()

        # quote_bid = quotes_df['bidPrice'].tolist()[::-1] # reverse the list
        quote_bid = quotes_df['bidPrice'].tolist()
        quote_ask = quotes_df['askPrice'].tolist()
        action = 'Pass'  # default action is 'Pass'
        position_size = 0  # default position size is zero
        price = 0  # default price is zero
        order_size = 0
        # calculate MOMentum
        mom0_ask = self.momentum(quote_ask, 12, 2)  ## index starts form 0,
        mom1_ask = self.momentum(mom0_ask, 1, 1)
        mom0_bid = self.momentum(quote_bid, 12, 2)  ## index starts form 0,
        mom1_bid = self.momentum(mom0_bid, 1, 1)
        logger.info('Calculated momentum ASKs: mom0: {}\tmom1: {}'.format(
            mom0_ask[0], mom1_ask[0]))
        logger.info('Calculated momentum BIDs: mom0: {}\tmom1: {}'.format(
            mom0_bid[0], mom1_bid[0]))

        # check if momentum is up and if we have open sell positions to be closed
        #if(mom0_ask[0]>0 and mom1_ask[0]>0 and open_position <= 0 and unrealisedPnlPcnt >= 0):
        # if(mom0_ask[0]<0 and mom1_ask[0]<0 and open_position <= 0 and unrealisedPnlPcnt >= 0):
        if (mom0_ask[0] < 0 and mom1_ask[0] < 0 and open_position <= 0):
            #if(mom0_ask[0]<0 and mom1_ask[0]<0):
            # amount of open shorts to close + default order position
            action = 'Buy'
            position_size = -1 * open_position + settings.ORDER_QUANTITY
            self.exchange.cancel_all_orders(
            )  # cancels previous orders which might still be pendind
            buy_order = self.exchange.send_smart_order(side=action,
                                                       orderQty=position_size)
            if buy_order is not None:
                order_size = buy_order[0]['orderQty']
                price = buy_order[0]['price']
                logger.info("NEW BUY Order: {} contract @ {}".format(
                    order_size, price))
            else:
                logger.info("Attention! BUY ORDER IS NONE")
        else:
            pass
        #if(mom0_bid[0]<0 and mom1_bid[0]<0 and open_position >= 0 and unrealisedPnlPcnt >= 0):
        # if(mom0_bid[0]>0 and mom1_bid[0]>0 and open_position >= 0 and unrealisedPnlPcnt >= 0):
        if (mom0_bid[0] > 0 and mom1_bid[0] > 0 and open_position >= 0):
            #if(mom0_bid[0]>0 and mom1_bid[0]>0):
            # amount of open longs to close - default order position
            action = 'Sell'
            position_size = -1 * open_position - settings.ORDER_QUANTITY
            logger.info(
                'Momentum is down, SELLING new position of {} contracts'.
                format(position_size))
            self.exchange.cancel_all_orders()
            sell_order = self.exchange.send_smart_order(side=action,
                                                        orderQty=position_size)
            if sell_order is not None:
                order_size = sell_order[0]['orderQty']
                price = sell_order[0]['price']
                order_size = -1 * order_size
                logger.info("NEW SELL Order: {} contract @ {}".format(
                    order_size, price))
            else:
                loggel.info("Attention! SELL ORDER IS NONE")
        else:
            pass

        # a strategy should return the action performed, buy/sell or nothing and the amount of the position and the price at which was performed
        # return {'action:'buy|sell|pass', 'amount':int, 'price':float}
        # return {'timestamp':current_time,'action':action, 'amount':position_size, 'price':price}
        return {
            'timestamp': current_time,
            'ASK-mom0': str(mom0_ask[0]),
            'ASK-mom1': str(mom1_ask[0]),
            'BID-mom0': str(mom0_bid[0]),
            'BID-mom1': str(mom1_bid[0]),
            'order_size': str(order_size),
            'order_price': str(price),
            'position': str(open_position),
            'unrealisedPnlPcnt': str(unrealisedPnlPcnt)
        }

    def paul_strategy(self, quotes_df):
        """awesome strategy"""
        pass

    def pnl_strategy(self, quotes_df):
        """
        This strategy takes in consideration unrealised PNL. If the profit is 3% or more closes the trade to take profit. 
        Once the trade is successfully closed send another buy order in the same direction as winning bet. To determine the previous position we store it into the df

        NOTE: If you are selling a stock, you are going to get the BID price, if you are buying a stock you are going to get the ASK price.
        """
        current_time = quotes_df.index.tolist()[0]  # extrapolate latest time

        full_position = self.exchange.get_position()
        # position = 0
        #print(full_position)
        # if (full_position['isOpen']):
        # position = full_position['simpleCost']

        unrealisedPnlPcnt = self.exchange.get_unrealisedPnlPcnt()

        # quote_bid = quotes_df['bidPrice'].tolist()[::-1] # reverse the list
        quote_bid = quotes_df['bidPrice'].tolist()
        quote_ask = quotes_df['askPrice'].tolist()
        action = 'Pass'  # default action is 'Pass'
        position_size = 0  # default position size is zero
        price = 0  # default price is zero
        order_size = 0
        # calculate MOMentum
        #mom0_ask = self.momentum(quote_ask,12,2) ## index starts form 0,
        #mom1_ask = self.momentum(mom0_ask, 1, 1)
        #mom0_bid = self.momentum(quote_bid,12,2) ## index starts form 0,
        #mom1_bid = self.momentum(mom0_bid, 1, 1)
        #logger.info('Calculated momentum ASKs: mom0: {}\tmom1: {}'.format(mom0_ask[0],mom1_ask[0]))
        #logger.info('Calculated momentum BIDs: mom0: {}\tmom1: {}'.format(mom0_bid[0],mom1_bid[0]))

        # check if momentum is up and if we have open sell positions to be closed
        #if(mom0_ask[0]>0 and mom1_ask[0]>0 and open_position <= 0 and unrealisedPnlPcnt >= 0):
        # if(mom0_ask[0]<0 and mom1_ask[0]<0 and open_position <= 0 and unrealisedPnlPcnt >= 0):
        action_list = ['Buy', 'Sell']
        random_action = random.choice(action_list)

        # determine if is a long or a short
        open_position = self.exchange.get_simpleCost()
        if open_position < 0:
            action = 'Sell'
            opposite_action = 'Buy'  # set opposite position so we can close it with inverse action
        elif open_position > 0:
            action = 'Buy'
            opposite_action = 'Sell'
        else:
            action = random_action  # this is temporary, because I need to store previous winning trade.
            # this action is executed when there are no open positions, therefore at the beginning of
            # of the bot or after closing a winning/loosing trade

        # if PNL is above threshold take profit then open a new order in the same direction
        if (unrealisedPnlPcnt >= 0.03):

            position_size = open_position

            self.exchange.cancel_all_orders(
            )  # cancels previous orders which might still be pendind

            if action is not 'None':
                order = self.exchange.send_smart_order(side=opposite_action,
                                                       orderQty=position_size)

            # if order is not None:
            #     order_size = order[0]['orderQty']
            #     price = order[0]['price']
            #     logger.info("NEW Order: {} contract @ {}".format(order_size,price))
            # else:
            #     logger.info("Attention! ORDER IS NONE")

        elif (unrealisedPnlPcnt > -0.003 and unrealisedPnlPcnt < 0.03):

            # let the position run
            logger.info("PNL within range: {}".format(unrealisedPnlPcnt))

            if (open_position == 0):
                # open a new position
                order = self.exchange.send_smart_order(
                    side=random_action, orderQty=settings.ORDER_QUANTITY)

                # if order is not None:
                #     order_size = order[0]['orderQty']
                #     price = order[0]['price']
                #     logger.info("NEW Order: {} contract @ {}".format(order_size,price))
                # else:
                #     logger.info("Attention! ORDER IS NONE")

        elif (unrealisedPnlPcnt <= -0.003):

            # loss stop close position then open in opposite way
            position_size = open_position

            self.exchange.cancel_all_orders(
            )  # cancels previous orders which might still be pendind

            if action is not 'None':
                order = self.exchange.send_smart_order(side=opposite_action,
                                                       orderQty=position_size)

            # if order is not None:
            #     order_size = order[0]['orderQty']
            #     price = order[0]['price']
            #     logger.info("NEW Order: {} contract @ {}".format(order_size,price))
            # else:
            #     logger.info("Attention! ORDER IS NONE")

        return {
            'timestamp': current_time,
            'order_size': str(order_size),
            'order_price': str(price),
            'position': str(open_position),
            'unrealisedPnlPcnt': str(unrealisedPnlPcnt)
        }

    def trailstop_strategy(self, new_quote_df, tape):
        """
        LEGACY: this method should not be called and should be removed soon
        """

        logger.info("THIS METHOD SHOULD NOT BE CALLED. IS OUTDATED...")
        """
        @TOD: tape object is not being used, refactor and remove
        """
        """
        Span random orders then set trailing stop with ratio loss/profit.

        Procedure pseudo code:

        1) pick a random entry (buy or sell)
        2) send the random limit-order just above/below the price
        2) wait order to be filled. If order is not filled in time, swap the order around and retry in the other direction
        3) .. wait for confirmation of order is filled
        4) set a trailing stop to execute the opposite trade (set order_quantity and trail value)
        5) if either trailing stop or take profit is executed, cancel all pending orders


        NOTE: If you are selling a stock, you are going to get the BID price, if you are buying a stock you are going to get the ASK price.
        """

        ############################
        # random.seed(0) # remove when done debugging. <------------------------------------
        ############################

        random_int = random.randint(0, 1)
        action_list = ['Buy', 'Sell']
        random_action = action_list[random_int]
        logger.info("Random action: {}".format(random_action))
        clOrdLinkID = str(uuid.uuid4())  # generate unique ID

        # Send a new order
        current_time = new_quote_df.index.tolist()[
            0]  # extrapolate latest time
        new_order = self.exchange.send_smart_order(
            side=random_action, orderQty=settings.ORDER_QUANTITY)
        # logger.info('New order : {}'.format(new_order))

        if new_order:

            logger.info(
                "Order successfully filled. Setting up trailing stop and take profit orders..."
            )
            # logger.info("new_order is {}".format(new_order))

            # Compute ATR for stop limits
            quotes = self.exchange.get_latest_quote(
                count=200)  # count should be same as in ATR smoothing settings
            resample_time = {'1m': '1Min'}
            candles = quotes['askPrice'].resample(
                resample_time[settings.TIMEFRAME]).ohlc(
                )  # group again and sorts 1Min candles (just to make sure!)
            stock_df = stockstats.StockDataFrame.retype(candles)
            atr_df = stock_df['atr_200']
            # logger.info("\n {}".format(atr_df))
            atr = max(
                1,
                int(round(atr_df[-1:].tolist()[0])) *
                settings.OFFSET_MULTIPLIER)
            logger.info("atr: {}\ttrailstop_offset_value: {}".format(
                atr_df[-1:].tolist()[0], atr))

            trailstop_offset_value = atr
            takeprofit_offset_value = atr * settings.OFFSET_RATIO
            logger.info(
                "trailstop_offset_value\t{}".format(trailstop_offset_value))
            logger.info(
                "takeprofit_offset_value\t{}".format(takeprofit_offset_value))

            # Append order details to df (time, action buy, price bought at ?)
            price = new_order[0]['price']
            orderID = new_order[0]['orderID']
            order_quantity = new_order[0][
                'cumQty']  # use cumQty which return the quantity even on partial orders
            new_dict = {
                'timestamp': current_time,
                'action': random_action,
                'execution_price': price,
                'orderID': orderID,
                'atr': atr
            }
            new_df = pd.DataFrame.from_records(
                [new_dict], index='timestamp')  # convert to pandas

            # Generate two linked orders with OCO (one cancels the other)
            """
            IDEA: a position could be closed if A) a stop loss is triggered, or B) if a timer runs out. The thought behind this is that a stop loss can end in a loss.
            A timed close would only be sensible if timed against the payout times (3 times per day)
            """

            # Trailstop order
            """
            IDEA: for a custom trailstop which get paid green fees: use a stoplimit order, same as a takeprofit order, then on each loop iteraction move it closer to the price if 
            the new_price - old price is in the same direction as the trade, and leave it unchanged if the price has gone against the bid 
            """
            trailstop_order = self.exchange.send_trailing_order(
                clOrdLinkID=clOrdLinkID,
                original_side=random_action,
                pegOffsetValue=trailstop_offset_value,
                orderQty=settings.ORDER_QUANTITY)
            # this is a temporary limit order not a trailstop
            # trailstop_order = self.exchange.send_stoploss_order(clOrdLinkID=clOrdLinkID, original_side=random_action, takeprofitOffset=trailstop_offset_value, orderQty=settings.ORDER_QUANTITY)
            trailstop_order_id = trailstop_order[0]['orderID']
            trailstop_order_status = trailstop_order[0]['ordStatus']

            # Takeprofit order
            takeprofit_order = self.exchange.send_takeprofit_order(
                clOrdLinkID=clOrdLinkID,
                original_side=random_action,
                takeprofitOffset=takeprofit_offset_value,
                orderQty=settings.ORDER_QUANTITY)
            takeprofit_order_id = takeprofit_order[0]['orderID']
            takeprofit_order_status = takeprofit_order[0]['ordStatus']

        else:  # no new order was executed
            new_dict = {
                'timestamp': current_time,
                'action': float('nan'),
                'execution_price': float('nan'),
                'orderID': float('nan'),
                'atr': float('nan')
            }
            new_df = pd.DataFrame.from_records(
                [new_dict], index='timestamp')  # convert to pandas

        # now check if any trade was closed

        # concat latest quote with new_df
        latest_record = self.append_colums_df(new_quote_df, new_df)
        #logger.info("latest_record\n{}".format(latest_record))

        return latest_record

    def append_colums_df(self, df, new_df):
        """
        Returns the combined colums of all df. df need to be indexed with datetime
        """
        df = pd.concat([df, new_df], axis=1, join_axes=[df.index])
        return df

    def get_balance(self, quotes_df):
        """
        Return a df containing datetime object (index) and balance at the time of latest quote (as string). Use this method to feed the tape object
        """
        logger.info("GET_BALANCE IS DEPRECATED. DON'T USE THIS FUNCTION")
        current_time = quotes_df.index.tolist()[0]  # extrapolate latest time
        balance_record = {
            'timestamp': current_time,
            'balance': str(self.exchange.get_margin())
        }
        return balance_record

    def run_loop(self):
        """
        Method to run strategy in loop. Execution frequency is defined in the setting file as LOOP_INTERVAL.
        LOOP_INTERVAL should be smaller than the timeframe used for the strategy calculation,
        for example executing trades on a 1m scale should have a LOOP_INTERVAL of ~30 seconds or less.
        
        NOTE: Starting December 11th, 2017 at 12:00 UTC, the following limits will be applied:

        (1) Maximum 200 open orders per contract per account;
        (2) Maximum 10 stop orders per contract per account;
        (3) Maximum 10 contingent orders per contract per account.

        When placing a new order that causes these caps to be exceeded, it will be rejected with the message “Too many [open|stop|contingent] orders”.
        """

        previous_quote_time = datetime(2000, 1, 1, 0, 0, 0, 0, pytz.UTC)

        max_open_orders = 199
        max_stops = 9
        max_contingents = 2  # 2 to avoid having multiple orders open

        # initialize empty tape
        tape = Tape(dry_run=settings.DRY_RUN)

        # initialize csv
        start_time = datetime.utcnow()
        csv_file = './log_{:%d-%m-%Y_%H.%M.%S}.csv'.format(start_time)
        tape.reset_csv(csv_file)  # rest csv

        while True:

            sys.stdout.write("-----\n")
            sys.stdout.flush()

            self.sanity_check()
            """
            Check if is time to request a new quote...
            NOTE: the latest BitMex quote is already 1 minutes ahead of the current clock (this I am not sure why, should ask BitMex why such ofset)
            """
            now = datetime.now(timezone.utc)
            # logger.info('Now {}'.format(now))

            # previous quote time is looking to the next minute (!), so 'now' is smaller than previous_quote_time until the last second (59) of each minute
            if (now > previous_quote_time):

                logger.info("New cycle ...")

                ###########################################################################################
                # run a strategy. each strategy must returns a df line with the quote and columns to store
                #new_record = self.trailstop_strategy(latest_quote, tape)
                funding_rates_strategy = funding_rates.Strategy(self.exchange)
                new_record = funding_rates_strategy.run_strategy()
                # append record to tape object
                tape.append_record(new_record)
                # append latest record to disk
                tape.append_to_csv(new_record, csv_file)
                logger.info(
                    "Tape DF after strategy is appended ...\n{}".format(
                        tape.get_df()))
                ###########################################################################################

                # OPTIMIZATION: if the order was not filled try submitting again skipping the sleep
                # DON'T USE OPTIMIZATION IF USING FUNDING STRATEGY (because should not retry if the order is not open)
                """
                executed = new_record.executed.astype(str)[0]
                logger.info("Executed {}".format(executed))
                if executed == 'False':
                    logger.info('No orders detected for this timeframe. Bot will retry skipping the sleep')
                    continue
                else:
                    logger.info("Detected order: {}".format(executed,settings.LOOP_INTERVAL))
                    latest_quote_time = new_record.index.tolist()[0] # extrapolate latest time
                    previous_quote_time = latest_quote_time # update quote time
                """
                logger.info("... sleeping for {} [sec]".format(
                    settings.LOOP_INTERVAL))
                sleep(settings.LOOP_INTERVAL)
            else:
                if (now < previous_quote_time):
                    logger.info(
                        "Waiting for new candlestick. Bot will sleep for {}s".
                        format(executed, settings.LOOP_INTERVAL))
                    sleep(settings.LOOP_INTERVAL)

    def collect_fundingRates(self):
        tape = Tape(dry_run=settings.DRY_RUN)
        # rest csv
        csv_file = './test.csv'
        tape.reset_csv(csv_file)

        while True:
            sys.stdout.write("-----\n")
            sys.stdout.flush()

            new_record = self.exchange.get_latest_quote_with_funding()
            # append record to the tape object
            tape.append_record(new_record)

            # append latest record to disk
            tape.append_to_csv(new_record, csv_file)
            logger.info("\nTape DF after strategy is appended ...\n{}".format(
                tape.get_df()))
            logger.info(
                "Waiting for new quote ... System is sleeping for {}s".format(
                    settings.LOOP_INTERVAL))
            sleep(settings.LOOP_INTERVAL)

    def tmp(self):
        # print(self.get_balance(quotes_df))
        # random.seed(3) # remove when done debugging. <------------------------------------
        # current_time = quotes_df.index.tolist()[0] # extrapolate latest time
        # random_int = random.randint(0,1)
        # action_list = ['Buy','Sell']
        # random_action = action_list[random_int]
        # logger.info("Random action: {}".format(random_action))
        # clOrdLinkID = str(uuid.uuid4()) # generate unique ID
        logger.info("%%%%%%%%%")

        # # set main order
        # order = self.exchange.send_smart_order(side=random_action, orderQty=settings.ORDER_QUANTITY)
        self.bitmex = bitmex.bitmex(test=settings.USE_TESTNET,
                                    api_key=settings.API_KEY_TEST,
                                    api_secret=settings.API_SECRET_TEST)
        logger.info("Connected to TESTNET")
        # order = self.bitmex.Order.Order_new(symbol=settings.SYMBOL, side='Buy', orderQty=100, ordType='Market').result()
        order = self.bitmex.Order.Order_new(
            symbol=settings.SYMBOL,
            side='Buy',
            orderQty=100,
            ordType='Limit',
            execInst='ParticipateDoNotInitiate',
            price='8700').result()

        order_id = order[0]['orderID']

        # logger.info(order)

        # logger.info('%%%$$$$$')
        logger.info(self.exchange.cancel_order_by_id(order_id))
Ejemplo n.º 2
0
class OrderManager:
    def __init__(self, orders_logging_file=None, settings=None, exchange=None):
        self.settings = settings
        if exchange == None:
            self.exchange = ExchangeInterface(self.settings.DRY_RUN,
                                              settings=self.settings)
        else:
            self.exchange = exchange
        #if not self.settings.BACKTEST:
        #    self.coinbase = OrderBook(product_id='BTC-USD')
        #    self.coinbase.start()
        # Once exchange is created, register exit handler that will always cancel orders
        # on any error.
        atexit.register(self.exit)
        signal.signal(signal.SIGTERM, self.exit)

        logger.info("Using symbol %s." % self.exchange.symbol)

        if self.settings.DRY_RUN:
            logger.info(
                "Initializing dry run. Orders printed below represent what would be posted to BitMEX."
            )
        else:
            logger.info(
                "Order Manager initializing, connecting to BitMEX. Live run: executing real trades."
            )
            compare_logger.info(
                "Order Manager initializing, connecting to BitMEX. Live run: executing real trades."
            )

        #self.start_time = self.exchange.current_timestep()
        self.instrument = self.exchange.get_instrument()
        if self.settings.compare is True:
            self.starting_qty = self.exchange.get_delta()[0]
        else:
            self.starting_qty = self.exchange.get_delta()
        self.running_qty = self.starting_qty
        self.reset()
        self.amend_error_counter = 0
        self.cancelled_orders = []

    def close_log_files(self):
        #handlers = self.pt_logger.handlers[:]
        #for handler in handlers:
        #    handler.close()
        #    self.pt_logger.removeHandler(handler)
        pass

    def reset(self):
        self.exchange.cancel_all_orders()
        if self.settings.get('SANITY_CHECK', True):
            self.sanity_check()
        self.print_status()

        # Create orders and converge.
        # Suspect that creating orders outside of loop is causing issues in backtests
        # self.place_orders()

    def ceilNearest(self, amount, roundAmount):
        return ceil(amount * (1.0 / roundAmount)) / (1.0 / roundAmount)

    def floorNearest(self, amount, roundAmount):
        return floor(amount * (1.0 / roundAmount)) / (1.0 / roundAmount)

    def print_status(self):
        #don't print status if backtesting
        if self.settings.BACKTEST is True:
            return
        """Print the current MM status."""

        margin = self.exchange.get_margin()
        position = self.exchange.get_position()
        self.running_qty = self.exchange.get_delta()
        tickLog = self.exchange.get_instrument()['tickLog']
        self.start_XBt = margin["marginBalance"]

        logger.info("Current XBT Balance: %.6f" % XBt_to_XBT(self.start_XBt))
        logger.info("Current Contract Position: %d" % self.running_qty)
        if self.settings.CHECK_POSITION_LIMITS:
            logger.info(
                "Position limits: %d/%d" %
                (self.settings.MIN_POSITION, self.settings.MAX_POSITION))
        if position['currentQty'] != 0:
            logger.info("Avg Cost Price: %.*f" %
                        (tickLog, float(position['avgCostPrice'])))
            logger.info("Avg Entry Price: %.*f" %
                        (tickLog, float(position['avgEntryPrice'])))
        logger.info("Contracts Traded This Run: %d" %
                    (self.exchange.contracts_this_run() - self.starting_qty))
        logger.info("Total Contract Delta: %.4f XBT" %
                    self.exchange.calc_delta()['spot'])

    def get_ticker(self):
        ticker = self.exchange.get_ticker()
        tickLog = self.exchange.get_instrument()['tickLog']

        # Set up our buy & sell positions as the smallest possible unit above and below the current spread
        # and we'll work out from there. That way we always have the best price but we don't kill wide
        # and potentially profitable spreads.
        self.start_position_buy = ticker["buy"] + self.instrument['tickSize']
        self.start_position_sell = ticker["sell"] - self.instrument['tickSize']

        # If we're maintaining spreads and we already have orders in place,
        # make sure they're not ours. If they are, we need to adjust, otherwise we'll
        # just work the orders inward until they collide.
        if self.settings.MAINTAIN_SPREADS:
            if ticker['buy'] == self.exchange.get_highest_buy()['price']:
                self.start_position_buy = ticker["buy"]
            if ticker['sell'] == self.exchange.get_lowest_sell()['price']:
                self.start_position_sell = ticker["sell"]

        # Back off if our spread is too small.
        if self.start_position_buy * (
                1.00 + self.settings.MIN_SPREAD) > self.start_position_sell:
            self.start_position_buy *= (1.00 - (self.settings.MIN_SPREAD / 2))
            self.start_position_sell *= (1.00 + (self.settings.MIN_SPREAD / 2))

        # Midpoint, used for simpler order placement.
        self.start_position_mid = ticker["mid"]
        logger.info("%s Ticker: Buy: %.*f, Sell: %.*f" %
                    (self.instrument['symbol'], tickLog, ticker["buy"],
                     tickLog, ticker["sell"]))
        compare_logger.info("%s Ticker: Buy: %.*f, Sell: %.*f" %
                            (self.instrument['symbol'], tickLog, ticker["buy"],
                             tickLog, ticker["sell"]))
        logger.info(
            'Start Positions: Buy: %.*f, Sell: %.*f, Mid: %.*f' %
            (tickLog, self.start_position_buy, tickLog,
             self.start_position_sell, tickLog, self.start_position_mid))
        compare_logger.info(
            'Start Positions: Buy: %.*f, Sell: %.*f, Mid: %.*f' %
            (tickLog, self.start_position_buy, tickLog,
             self.start_position_sell, tickLog, self.start_position_mid))
        return ticker

    def get_price_offset(self, index):
        """Given an index (1, -1, 2, -2, etc.) return the price for that side of the book.
           Negative is a buy, positive is a sell."""
        # Maintain existing spreads for max profit
        if self.settings.MAINTAIN_SPREADS:
            start_position = self.start_position_buy if index < 0 else self.start_position_sell
            # First positions (index 1, -1) should start right at start_position, others should branch from there
            index = index + 1 if index < 0 else index - 1
        else:
            # Offset mode: ticker comes from a reference exchange and we define an offset.
            start_position = self.start_position_buy if index < 0 else self.start_position_sell

            # If we're attempting to sell, but our sell price is actually lower than the buy,
            # move over to the sell side.
            if index > 0 and start_position < self.start_position_buy:
                start_position = self.start_position_sell
            # Same for buys.
            if index < 0 and start_position > self.start_position_sell:
                start_position = self.start_position_buy

        return math.toNearest(
            start_position * (1 + self.settings.INTERVAL)**index,
            self.instrument['tickSize'])

    ###
    # Orders
    ###

    def place_orders(self):
        """Create order items for use in convergence."""

        buy_orders = []
        sell_orders = []
        # Create orders from the outside in. This is intentional - let's say the inner order gets taken;
        # then we match orders from the outside in, ensuring the fewest number of orders are amended and only
        # a new order is created in the inside. If we did it inside-out, all orders would be amended
        # down and a new order would be created at the outside.
        for i in reversed(range(1, self.settings.ORDER_PAIRS + 1)):
            if not self.long_position_limit_exceeded():
                buy_orders.append(self.prepare_order(-i))
            if not self.short_position_limit_exceeded():
                sell_orders.append(self.prepare_order(i))

        return self.converge_orders(buy_orders, sell_orders)

    def prepare_order(self, index):
        """Create an order object."""

        if self.settings.RANDOM_ORDER_SIZE is True:
            quantity = random.randint(self.settings.MIN_ORDER_SIZE,
                                      self.settings.MAX_ORDER_SIZE)
        else:
            quantity = self.settings.ORDER_START_SIZE + (
                (abs(index) - 1) * self.settings.ORDER_STEP_SIZE)

        price = self.get_price_offset(index)

        return {
            'price': price,
            'orderQty': quantity,
            'side': "Buy" if index < 0 else "Sell"
        }

    def get_order_with_role(self, orders, role):
        '''Return first order with role.'''
        for order in orders:
            if order.get('side', "") == role and \
                order.get('orderID', "") not in self.cancelled_orders and \
                order.get('ordStatus', "") not in  ['Filled', 'Canceled']:
                return order
        return None

    def get_all_orders_with_role(self, orders, role):
        '''Return all orders with role.'''
        ret_orders = []
        for order in orders:
            if order.get('side', "") == role and \
                order.get('orderID', "") not in self.cancelled_orders and \
                order.get('ordStatus', "") not in  ['Filled', 'Canceled']:
                ret_orders.append(order)
        return ret_orders

    def cancel_all_orders(self, existing_orders):
        for order in existing_orders:
            if 'orderID' not in order:
                logger.warning("Can't Cancel - No orderID in order: %s" %
                               json.dumps(order))
                continue
            if isinstance(order['orderID'], int):
                logger.warning(
                    "Can't Cancel - OrderID length must be 36 characters: %s" %
                    json.dumps(order))
                continue
            self.exchange.cancel_order(order)
            self.cancelled_orders.append(order['orderID'])
        self.exchange.cancel_all_orders()

    def cancel_orders(self, orders):
        for order in orders:
            if 'orderID' not in order:
                logger.warning("Can't Cancel - No orderID in order: %s" %
                               json.dumps(order))
                continue
            if isinstance(order['orderID'], int):
                logger.warning("Can't Cancel - OrderID must be a string (perhaps you are canceling a promised order: %s" % \
                            json.dumps(order))
                continue
            self.exchange.cancel_order(order)
            self.cancelled_orders.append(order['orderID'])

    def create_cancel_orders_from_orders(self, orders):
        to_cancel = []
        for order in orders:
            if not self.is_live_order(order):
                logger.warning("Waiting to cancel order: %s" %
                               json.dumps(order))
                continue
            the_keys = ['orderID', 'side', 'orderQty', 'price']
            order_to_cancel = dict((key, value) for key, value in \
                                   order.items() if key in the_keys)
            to_cancel.append(order_to_cancel)
        return to_cancel

    def is_live_order(self, order):
        ''' Checks order for liveness. Liveness means that it is an order confirmed
        by the exchange to be live, and is not a promise created by exchange_interface to
        record an expected live order.'''
        if order.get('ordStatus', "") not in ['Filled', 'Canceled'] and \
                'submission_time' not in order and 'ordStatus' in order:
            return True
        else:
            return False

    def live_orders(self, orders):
        '''Tries to determine orders that are live on the exchange.'''
        ret_orders = []
        for order in orders:
            if self.is_live_order(order):
                ret_orders.append(order)
        return ret_orders

    def desired_to_orders(self,
                          buyprice,
                          sellprice,
                          buyamount=100,
                          sellamount=100,
                          tags={}):
        tickLog = self.exchange.get_instrument()['tickLog']
        existing_orders = self.exchange.get_orders()
        to_create = []
        to_amend = []
        to_cancel = []

        # Perform some initial checks
        if len(self.live_orders(existing_orders)) > 4:
            logger.warning("Number of orders exceeds 4, canceling all orders")
            self.cancel_orders(existing_orders)
            return

        # Manage Buy Order
        buy_orders = self.get_all_orders_with_role(existing_orders, 'Buy')
        if buy_orders != []:
            buy_order = buy_orders[0]
            if len(buy_orders) > 1:
                # cancel all orders above 1
                to_cancel.extend(
                    self.create_cancel_orders_from_orders(buy_orders[1:]))

            # If a recently submitted order, let's not amend
            if self.is_live_order(
                    buy_order) and buy_order['price'] != buyprice:
                the_keys = ['orderID', 'side']
                amended_order = dict((key, value) for key, value in \
                                     buy_order.items() if key in the_keys)
                amended_order['price'] = buyprice
                amended_order['orderQty'] = buyamount
                amended_order.update(tags)
                if amended_order['orderQty'] > 0:
                    to_amend.append(amended_order)
        else:
            # let's create a new order
            buyorder = {
                'price': buyprice,
                'orderQty': buyamount,
                'side': "Buy",
                'orderID': random.randint(0, 100000)
            }
            buyorder.update(tags)
            if buyorder['orderQty'] > 0:
                to_create.append(buyorder)

        # Manage Sell Order
        sell_orders = self.get_all_orders_with_role(existing_orders, 'Sell')
        if sell_orders != []:
            sell_order = sell_orders[0]
            if len(sell_orders) > 1:
                # cancel all orders above 1
                to_cancel.extend(
                    self.create_cancel_orders_from_orders(sell_orders[1:]))
            # If a recently submitted order, let's not amend
            if self.is_live_order(
                    sell_order) and sell_order['price'] != sellprice:
                the_keys = ['orderID', 'side']
                amended_order = dict((key, value) for key, value in \
                                     sell_order.items() if key in the_keys)
                amended_order['price'] = sellprice
                amended_order['orderQty'] = sellamount
                amended_order.update(tags)
                if amended_order['orderQty'] > 0:
                    to_amend.append(amended_order)
        else:
            # let's create a new order
            sellorder = {
                'price': sellprice,
                'orderQty': sellamount,
                'side': "Sell",
                'orderID': random.randint(0, 100000)
            }
            sellorder.update(tags)
            if sellorder['orderQty'] > 0:
                to_create.append(sellorder)

        # Amend orders as needed
        if len(to_amend) > 0:
            self.amend_orders(to_amend, existing_orders)
        # Create any needed new orders
        if len(to_create) > 0:
            self.create_new_orders(to_create)
        # Cancel any needed orders
        if len(to_cancel) > 0:
            self.cancel_orders(to_cancel)

    def create_new_orders(self, to_create):
        tickLog = self.exchange.get_instrument()['tickLog']
        logger.info("Creating %d orders:" % (len(to_create)))
        #compare_logger.info("Creating %d orders:" % (len(to_create)))
        for order in reversed(to_create):
            logger.info(
                "%4s %d @ %.*f" %
                (order['side'], order['orderQty'], tickLog, order['price']))
            #compare_logger.info("%4s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price']))
        self.exchange.create_bulk_orders(to_create)

    def amend_orders(self, to_amend, existing_orders):
        tickLog = self.exchange.get_instrument()['tickLog']
        logger.info("Amending Orders %s" % json.dumps(to_amend))
        for amended_order in reversed(to_amend):
            reference_order = [
                o for o in existing_orders
                if o['orderID'] == amended_order['orderID']
            ][0]
            # Below is commented out because 'leavesQty is not available for CCXT
            #logger.info("Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % (
            #    amended_order['side'],
            #    reference_order['leavesQty'], tickLog, reference_order['price'],
            #    (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['price'],
            #    tickLog, (amended_order['price'] - reference_order['price'])
            #))
        # This can fail if an order has closed in the time we were processing.
        # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled)
        # made it not amendable.
        # If that happens, we need to catch it and re-tick.
        try:
            self.exchange.amend_bulk_orders(to_amend)
        except requests.exceptions.HTTPError as e:
            errorObj = e.response.json()
            if errorObj['error']['message'] == 'Invalid ordStatus':
                logger.warn(
                    "Amending failed. Waiting for order data to converge and retrying."
                )
                logger.warn("Failed on orders: %s" % json.dumps(to_amend))
                for order in to_amend:
                    self.cancelled_orders.append(order['orderID'])
                #try:
                #    self.cancel_orders(to_amend)
                #except:
                #    logger.warn("Couldn't cancel orders!: %s" % json.dumps(to_amend))
                #    raise
                # sleep(0.5)
                # return self.place_orders()
                self.amend_error_counter += 1
            else:
                logger.error("Unknown error on amend: %s. Exiting" % errorObj)
                sys.exit(1)
        except ValueError as e:
            logger.error('Failed to amend order (Ignoring amend request): ' +
                         str(e))

    def prices_to_orders(self,
                         buyprice,
                         sellprice,
                         buyamount=100,
                         sellamount=100,
                         theo=-1):
        tickLog = self.exchange.get_instrument()['tickLog']
        to_amend = []
        to_create = []
        existing_orders = self.exchange.get_orders()
        if len(existing_orders) > 4:
            logger.warning("Number of orders exceeds 4, canceling all orders")
            self.exchange.cancel_all_orders()
            return
        if self.amend_error_counter > 5:
            logger.warning(
                'Number of amend failures exceeds 5, canceling all orders')
            self.amend_error_counter = 0
            self.exchange.cancel_all_orders()
            return
        buy_present = sell_present = False
        try:
            last_price = self.exchange.recent_trades()[-1]['price']
        except:
            last_price = 0.0
        coinbase_midprice = 0.0
        if not self.settings.BACKTEST:
            try:
                #coinbase_midprice = float(self.coinbase.get_bid()+self.coinbase.get_ask())/2
                pass
            except:
                pass

        if theo < 0:
            midprice = last_price  #ticker["mid"]
        else:
            midprice = theo
        if len(existing_orders) > 1:
            for order in existing_orders:
                if 'submission_time' in order:
                    continue
                if order['side'] == "Buy":
                    if order['price'] != buyprice:
                        neworder = {
                            'orderID': order['orderID'],
                            'orderQty': buyamount,
                            'price': buyprice,
                            'side': "Buy",
                            'theo': midprice,
                            'last_price': last_price
                        }
                        if not buy_present:
                            buy_present = True
                            if neworder['orderQty'] > 0:
                                to_amend.append(neworder)
                        else:
                            #neworder['orderQty'] = 0
                            pass
                    else:
                        buy_present = True

                else:
                    if order['price'] != sellprice:
                        neworder = {
                            'orderID': order['orderID'],
                            'orderQty': sellamount,
                            'price': sellprice,
                            'side': "Sell",
                            'theo': midprice,
                            'last_price': last_price
                        }
                        if not sell_present:
                            sell_present = True
                            if neworder['orderQty'] > 0:
                                to_amend.append(neworder)
                        else:
                            #neworder['orderQty'] = 0
                            pass
                    else:
                        sell_present = True

        elif len(existing_orders) == 1:
            for order in existing_orders:
                side = "Buy" if order['side'] == "Sell" else "Sell"
                size = buyamount if order['side'] == "Sell" else sellamount
                price = buyprice if order['side'] == "Sell" else sellprice
                neworder = {
                    'price': price,
                    'orderQty': size,
                    'side': side,
                    'theo': midprice,
                    'last_price': last_price,
                    'orderID': random.randint(0, 100000)
                }
                if neworder['orderQty'] > 0:
                    to_create.append(neworder)
        else:
            #cancel existing orders and create new ones
            logger.info("Length of existing orders: %d" %
                        (len(existing_orders)))
            self.exchange.cancel_all_orders()
            buyorder = {
                'price': buyprice,
                'orderQty': buyamount,
                'side': "Buy",
                'theo': midprice,
                'last_price': last_price,
                'orderID': random.randint(0, 100000)
            }
            sellorder = {
                'price': sellprice,
                'orderQty': sellamount,
                'side': "Sell",
                'theo': midprice,
                'last_price': last_price,
                'orderID': random.randint(0, 100000)
            }
            if buyorder['orderQty'] > 0:
                to_create.append(buyorder)
            if sellorder['orderQty'] > 0:
                to_create.append(sellorder)

        # Amend orders as needed
        if len(to_amend) > 0:
            self.amend_orders(to_amend, existing_orders)
        # Create any needed new orders
        if len(to_create) > 0:
            self.create_new_orders(to_create)

    def converge_orders(self, buy_orders, sell_orders):
        """Converge the orders we currently have in the book with what we want to be in the book.
           This involves amending any open orders and creating new ones if any have filled completely.
           We start from the closest orders outward."""

        tickLog = self.exchange.get_instrument()['tickLog']
        to_amend = []
        to_create = []
        to_cancel = []
        buys_matched = 0
        sells_matched = 0
        existing_orders = self.exchange.get_orders()

        # Check all existing orders and match them up with what we want to place.
        # If there's an open one, we might be able to amend it to fit what we want.
        for order in existing_orders:
            try:
                if order['side'] == 'Buy':
                    desired_order = buy_orders[buys_matched]
                    buys_matched += 1
                else:
                    desired_order = sell_orders[sells_matched]
                    sells_matched += 1

                # Found an existing order. Do we need to amend it?
                if desired_order['orderQty'] != order['leavesQty'] or (
                        # If price has changed, and the change is more than our RELIST_INTERVAL, amend.
                        desired_order['price'] != order['price']
                        and abs((desired_order['price'] / order['price']) - 1)
                        > self.settings.RELIST_INTERVAL):

                    # The math in this next line seems wrong. Instead of the new orderQty being
                    # order['cumQty'] + desired_order['orderQty'], it seems like it should be
                    # desired_order['orderQty'] - order['cumQty']
                    to_amend.append({
                        'orderID':
                        order['orderID'],
                        'orderQty':
                        order['cumQty'] + desired_order['orderQty'],
                        'price':
                        desired_order['price'],
                        'side':
                        order['side']
                    })
            except IndexError:
                # Will throw if there isn't a desired order to match. In that case, cancel it.
                #to_cancel.append(order)
                pass

        while buys_matched < len(buy_orders):
            to_create.append(buy_orders[buys_matched])
            buys_matched += 1

        while sells_matched < len(sell_orders):
            to_create.append(sell_orders[sells_matched])
            sells_matched += 1

        if len(to_amend) > 0:
            for amended_order in reversed(to_amend):
                reference_order = [
                    o for o in existing_orders
                    if o['orderID'] == amended_order['orderID']
                ][0]
                logger.info(
                    "Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" %
                    (amended_order['side'], reference_order['leavesQty'],
                     tickLog, reference_order['price'],
                     (amended_order['orderQty'] - reference_order['cumQty']),
                     tickLog, amended_order['price'], tickLog,
                     (amended_order['price'] - reference_order['price'])))
            # This can fail if an order has closed in the time we were processing.
            # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled)
            # made it not amendable.
            # If that happens, we need to catch it and re-tick.
            try:
                self.exchange.amend_bulk_orders(to_amend)
            except requests.exceptions.HTTPError as e:
                errorObj = e.response.json()
                if errorObj['error']['message'] == 'Invalid ordStatus':
                    logger.warn(
                        "Amending failed. Waiting for order data to converge and retrying."
                    )
                    sleep(0.5)
                    return self.place_orders()
                else:
                    logger.error("Unknown error on amend: %s. Exiting" %
                                 errorObj)
                    sys.exit(1)

        if len(to_create) > 0:
            logger.info("Creating %d orders:" % (len(to_create)))
            compare_logger.info("Creating %d orders:" % (len(to_create)))
            for order in reversed(to_create):
                logger.info("%4s %d @ %.*f" %
                            (order['side'], order['orderQty'], tickLog,
                             order['price']))
                compare_logger.info("%4s %d @ %.*f" %
                                    (order['side'], order['orderQty'], tickLog,
                                     order['price']))
            self.exchange.create_bulk_orders(to_create)

        # Could happen if we exceed a delta limit
        if len(to_cancel) > 0:
            logger.info("Canceling %d orders:" % (len(to_cancel)))
            for order in reversed(to_cancel):
                logger.info("%4s %d @ %.*f" %
                            (order['side'], order['leavesQty'], tickLog,
                             order['price']))
                compare_logger.info("%4s %d @ %.*f" %
                                    (order['side'], order['leavesQty'],
                                     tickLog, order['price']))
            self.exchange.cancel_bulk_orders(to_cancel)

    ###
    # Position Limits
    ###

    def short_position_limit_exceeded(self):
        """Returns True if the short position limit is exceeded"""
        if not self.settings.CHECK_POSITION_LIMITS:
            return False
        position = self.exchange.get_delta()
        return position <= self.settings.MIN_POSITION

    def long_position_limit_exceeded(self):
        """Returns True if the long position limit is exceeded"""
        if not self.settings.CHECK_POSITION_LIMITS:
            return False
        position = self.exchange.get_delta()
        return position >= self.settings.MAX_POSITION

    ###
    # Sanity
    ##

    def sanity_check(self):
        """Perform checks before placing orders."""

        # Check if OB is empty - if so, can't quote.
        self.exchange.check_if_orderbook_empty()

        # Ensure market is still open.
        self.exchange.check_market_open()

        # Get ticker, which sets price offsets and prints some debugging info.
        ticker = self.get_ticker()

        # Sanity check:
        #if self.get_price_offset(-1) >= ticker["sell"] or self.get_price_offset(1) <= ticker["buy"]:
        #    logger.error("Buy: %s, Sell: %s" % (self.start_position_buy, self.start_position_sell))
        #    logger.error("First buy position: %s\nBitMEX Best Ask: %s\nFirst sell position: %s\nBitMEX Best Bid: %s" %
        #                 (self.get_price_offset(-1), ticker["sell"], self.get_price_offset(1), ticker["buy"]))
        #    logger.error("Sanity check failed, exchange data is inconsistent")
        #    self.exit()

        # Messaging if the position limits are reached
        if self.long_position_limit_exceeded():
            logger.info("Long delta limit exceeded")
            logger.info(
                "Current Position: %.f, Maximum Position: %.f" %
                (self.exchange.get_delta(), self.settings.MAX_POSITION))

        if self.short_position_limit_exceeded():
            logger.info("Short delta limit exceeded")
            logger.info(
                "Current Position: %.f, Minimum Position: %.f" %
                (self.exchange.get_delta(), self.settings.MIN_POSITION))

    ###
    # Running
    ###

    def check_file_change(self):
        """Restart if any files we're watching have changed."""
        for f, mtime in watched_files_mtimes:
            if getmtime(f) > mtime:
                self.restart()

    def check_connection(self):
        """Ensure the WS connections are still open."""
        return self.exchange.is_open()

    def exit(self):
        logger.info("Shutting down. All open orders will be cancelled.")
        try:
            self.exchange.cancel_all_orders()
            self.exchange.exit_exchange()
        except errors.AuthenticationError as e:
            logger.info("Was not authenticated; could not cancel orders.")
        except Exception as e:
            logger.info("Unable to cancel orders: %s" % e)

        sys.exit()

    def run_loop(self):
        print("Entered Run Loop.")

        def print_output():
            if not self.settings.BACKTEST:
                sys.stdout.write("-----\n")
                sys.stdout.flush()
            self.print_status()

        while True:
            # Remove comment to drop system into debugger, great to understand issues
            #import pdb; pdb.set_trace()
            if self.settings.BACKTEST:
                try:
                    self.exchange.wait_update()
                except EOFError:
                    self.close_log_files()
                    logger.info("Reached end of Backtest file.")
                    break
            if self.exchange.ok_to_enter_order():
                #self.check_file_change()
                #sleep(self.settings.LOOP_INTERVAL)
                if self.settings.get('SANITY_CHECK', True):
                    self.sanity_check(
                    )  # Ensures health of mm - several cut-out points here

                # This will restart on very short downtime, but if it's longer,
                # the MM will crash entirely as it is unable to connect to the WS on boot.

                self.place_orders(
                )  # Creates desired orders and converges to existing orders

                if not self.settings.BACKTEST:
                    if not self.check_connection():
                        logger.error(
                            "Realtime data connection unexpectedly closed, restarting."
                        )
                        self.restart()
                    self.print_status()  # Print skew, delta, etc
                else:
                    periodically_call(print_output, amount=200)
                #The following should now be taken care of by wait_update
                #self.exchange.loop()

    def restart(self):
        logger.info("Restarting the market maker...")
        os.execv(sys.executable, [sys.executable] + sys.argv)
Ejemplo n.º 3
0
class OrderManager:
    def __init__(self):
        self.exchange = ExchangeInterface(settings.DRY_RUN)
        # Once exchange is created, register exit handler that will always cancel orders
        # on any error.
        atexit.register(self.exit)
        signal.signal(signal.SIGTERM, self.exit)

        logger.info("Using symbol %s." % self.exchange.symbol)

        if settings.DRY_RUN:
            logger.info("Initializing dry run. Orders printed below represent what would be posted to BitMEX.")
        else:
            logger.info("Order Manager initializing, connecting to BitMEX. Live run: executing real trades.")

        self.start_time = datetime.now()
        self.instrument = self.exchange.get_instrument()
        self.starting_qty = self.exchange.get_delta()
        self.running_qty = self.starting_qty
        self.reset()

    def reset(self):
        self.exchange.cancel_all_orders()
        self.sanity_check()
        self.print_status()

        # Create orders and converge.
        self.place_orders()

    def print_status(self):
        """Print the current MM status."""

        margin = self.exchange.get_margin()
        position = self.exchange.get_position()
        self.running_qty = self.exchange.get_delta()
        tickLog = self.exchange.get_instrument()['tickLog']
        self.start_XBt = margin["marginBalance"]

        logger.info("Current XBT Balance: %.6f" % XBt_to_XBT(self.start_XBt))
        logger.info("Current Contract Position: %d" % self.running_qty)
        if settings.CHECK_POSITION_LIMITS:
            logger.info("Position limits: %d/%d" % (settings.MIN_POSITION, settings.MAX_POSITION))
        if position['currentQty'] != 0:
            logger.info("Avg Cost Price: %.*f" % (tickLog, float(position['avgCostPrice'])))
            logger.info("Avg Entry Price: %.*f" % (tickLog, float(position['avgEntryPrice'])))
        logger.info("Contracts Traded This Run: %d" % (self.running_qty - self.starting_qty))
        logger.info("Total Contract Delta: %.4f XBT" % self.exchange.calc_delta()['spot'])

    def get_ticker(self):
        ticker = self.exchange.get_ticker()
        tickLog = self.exchange.get_instrument()['tickLog']

        # Set up our buy & sell positions as the smallest possible unit above and below the current spread
        # and we'll work out from there. That way we always have the best price but we don't kill wide
        # and potentially profitable spreads.
        self.start_position_buy = ticker["buy"] + self.instrument['tickSize']
        self.start_position_sell = ticker["sell"] - self.instrument['tickSize']

        # If we're maintaining spreads and we already have orders in place,
        # make sure they're not ours. If they are, we need to adjust, otherwise we'll
        # just work the orders inward until they collide.
        if settings.MAINTAIN_SPREADS:
            if ticker['buy'] == self.exchange.get_highest_buy()['price']:
                self.start_position_buy = ticker["buy"]
            if ticker['sell'] == self.exchange.get_lowest_sell()['price']:
                self.start_position_sell = ticker["sell"]

        # Back off if our spread is too small.
        if self.start_position_buy * (1.00 + settings.MIN_SPREAD) > self.start_position_sell:
            self.start_position_buy *= (1.00 - (settings.MIN_SPREAD / 2))
            self.start_position_sell *= (1.00 + (settings.MIN_SPREAD / 2))

        # Midpoint, used for simpler order placement.
        self.start_position_mid = ticker["mid"]
        logger.info(
            "%s Ticker: Buy: %.*f, Sell: %.*f" %
            (self.instrument['symbol'], tickLog, ticker["buy"], tickLog, ticker["sell"])
        )
        logger.info('Start Positions: Buy: %.*f, Sell: %.*f, Mid: %.*f' %
                    (tickLog, self.start_position_buy, tickLog, self.start_position_sell,
                     tickLog, self.start_position_mid))
        return ticker

    def get_price_offset(self, index):
        """Given an index (1, -1, 2, -2, etc.) return the price for that side of the book.
           Negative is a buy, positive is a sell."""
        # Maintain existing spreads for max profit
        if settings.MAINTAIN_SPREADS:
            start_position = self.start_position_buy if index < 0 else self.start_position_sell
            # First positions (index 1, -1) should start right at start_position, others should branch from there
            index = index + 1 if index < 0 else index - 1
        else:
            # Offset mode: ticker comes from a reference exchange and we define an offset.
            start_position = self.start_position_buy if index < 0 else self.start_position_sell

            # If we're attempting to sell, but our sell price is actually lower than the buy,
            # move over to the sell side.
            if index > 0 and start_position < self.start_position_buy:
                start_position = self.start_position_sell
            # Same for buys.
            if index < 0 and start_position > self.start_position_sell:
                start_position = self.start_position_buy

        return math.toNearest(start_position * (1 + settings.INTERVAL) ** index, self.instrument['tickSize'])

    ###
    # Orders
    ###

    def place_orders(self):
        """Create order items for use in convergence."""

        buy_orders = []
        sell_orders = []
        # Create orders from the outside in. This is intentional - let's say the inner order gets taken;
        # then we match orders from the outside in, ensuring the fewest number of orders are amended and only
        # a new order is created in the inside. If we did it inside-out, all orders would be amended
        # down and a new order would be created at the outside.
        for i in reversed(range(1, settings.ORDER_PAIRS + 1)):
            if not self.long_position_limit_exceeded():
                buy_orders.append(self.prepare_order(-i))
            if not self.short_position_limit_exceeded():
                sell_orders.append(self.prepare_order(i))

        return self.converge_orders(buy_orders, sell_orders)

    def prepare_order(self, index):
        """Create an order object."""

        if settings.RANDOM_ORDER_SIZE is True:
            quantity = random.randint(settings.MIN_ORDER_SIZE, settings.MAX_ORDER_SIZE)
        else:
            quantity = settings.ORDER_START_SIZE + ((abs(index) - 1) * settings.ORDER_STEP_SIZE)

        price = self.get_price_offset(index)

        return {'price': price, 'orderQty': quantity, 'side': "Buy" if index < 0 else "Sell"}

    def converge_orders(self, buy_orders, sell_orders):
        """Converge the orders we currently have in the book with what we want to be in the book.
           This involves amending any open orders and creating new ones if any have filled completely.
           We start from the closest orders outward."""

        tickLog = self.exchange.get_instrument()['tickLog']
        to_amend = []
        to_create = []
        to_cancel = []
        buys_matched = 0
        sells_matched = 0
        existing_orders = self.exchange.get_orders()

        # Check all existing orders and match them up with what we want to place.
        # If there's an open one, we might be able to amend it to fit what we want.
        for order in existing_orders:
            try:
                if order['side'] == 'Buy':
                    desired_order = buy_orders[buys_matched]
                    buys_matched += 1
                else:
                    desired_order = sell_orders[sells_matched]
                    sells_matched += 1

                # Found an existing order. Do we need to amend it?
                if desired_order['orderQty'] != order['leavesQty'] or (
                        # If price has changed, and the change is more than our RELIST_INTERVAL, amend.
                        desired_order['price'] != order['price'] and
                        abs((desired_order['price'] / order['price']) - 1) > settings.RELIST_INTERVAL):
                    to_amend.append({'orderID': order['orderID'], 'orderQty': order['cumQty'] + desired_order['orderQty'],
                                     'price': desired_order['price'], 'side': order['side']})
            except IndexError:
                # Will throw if there isn't a desired order to match. In that case, cancel it.
                to_cancel.append(order)

        while buys_matched < len(buy_orders):
            to_create.append(buy_orders[buys_matched])
            buys_matched += 1

        while sells_matched < len(sell_orders):
            to_create.append(sell_orders[sells_matched])
            sells_matched += 1

        if len(to_amend) > 0:
            for amended_order in reversed(to_amend):
                reference_order = [o for o in existing_orders if o['orderID'] == amended_order['orderID']][0]
                logger.info("Amending %4s: %d @ %.*f to %d @ %.*f (%+.*f)" % (
                    amended_order['side'],
                    reference_order['leavesQty'], tickLog, reference_order['price'],
                    (amended_order['orderQty'] - reference_order['cumQty']), tickLog, amended_order['price'],
                    tickLog, (amended_order['price'] - reference_order['price'])
                ))
            # This can fail if an order has closed in the time we were processing.
            # The API will send us `invalid ordStatus`, which means that the order's status (Filled/Canceled)
            # made it not amendable.
            # If that happens, we need to catch it and re-tick.
            try:
                self.exchange.amend_bulk_orders(to_amend)
            except requests.exceptions.HTTPError as e:
                errorObj = e.response.json()
                if errorObj['error']['message'] == 'Invalid ordStatus':
                    logger.warn("Amending failed. Waiting for order data to converge and retrying.")
                    sleep(0.5)
                    return self.place_orders()
                else:
                    logger.error("Unknown error on amend: %s. Exiting" % errorObj)
                    sys.exit(1)

        if len(to_create) > 0:
            logger.info("Creating %d orders:" % (len(to_create)))
            for order in reversed(to_create):
                logger.info("%4s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price']))
            self.exchange.create_bulk_orders(to_create)

        # Could happen if we exceed a delta limit
        if len(to_cancel) > 0:
            logger.info("Canceling %d orders:" % (len(to_cancel)))
            for order in reversed(to_cancel):
                logger.info("%4s %d @ %.*f" % (order['side'], order['leavesQty'], tickLog, order['price']))
            self.exchange.cancel_bulk_orders(to_cancel)

    ###
    # Position Limits
    ###

    def short_position_limit_exceeded(self):
        """Returns True if the short position limit is exceeded"""
        if not settings.CHECK_POSITION_LIMITS:
            return False
        position = self.exchange.get_delta()
        return position <= settings.MIN_POSITION

    def long_position_limit_exceeded(self):
        """Returns True if the long position limit is exceeded"""
        if not settings.CHECK_POSITION_LIMITS:
            return False
        position = self.exchange.get_delta()
        return position >= settings.MAX_POSITION

    ###
    # Sanity
    ##

    def sanity_check(self):
        """Perform checks before placing orders."""

        # Check if OB is empty - if so, can't quote.
        self.exchange.check_if_orderbook_empty()

        # Ensure market is still open.
        self.exchange.check_market_open()

        # Get ticker, which sets price offsets and prints some debugging info.
        ticker = self.get_ticker()

        # Sanity check:
        if self.get_price_offset(-1) >= ticker["sell"] or self.get_price_offset(1) <= ticker["buy"]:
            logger.error("Buy: %s, Sell: %s" % (self.start_position_buy, self.start_position_sell))
            logger.error("First buy position: %s\nBitMEX Best Ask: %s\nFirst sell position: %s\nBitMEX Best Bid: %s" %
                         (self.get_price_offset(-1), ticker["sell"], self.get_price_offset(1), ticker["buy"]))
            logger.error("Sanity check failed, exchange data is inconsistent")
            self.exit()

        # Messaging if the position limits are reached
        if self.long_position_limit_exceeded():
            logger.info("Long delta limit exceeded")
            logger.info("Current Position: %.f, Maximum Position: %.f" %
                        (self.exchange.get_delta(), settings.MAX_POSITION))

        if self.short_position_limit_exceeded():
            logger.info("Short delta limit exceeded")
            logger.info("Current Position: %.f, Minimum Position: %.f" %
                        (self.exchange.get_delta(), settings.MIN_POSITION))

    ###
    # Running
    ###

    def check_file_change(self):
        """Restart if any files we're watching have changed."""
        for f, mtime in watched_files_mtimes:
            if getmtime(f) > mtime:
                self.restart()

    def check_connection(self):
        """Ensure the WS connections are still open."""
        return self.exchange.is_open()

    def exit(self):
        logger.info("Shutting down. All open orders will be cancelled.")
        try:
            self.exchange.cancel_all_orders()
            self.exchange.bitmex.exit()
        except errors.AuthenticationError as e:
            logger.info("Was not authenticated; could not cancel orders.")
        except Exception as e:
            logger.info("Unable to cancel orders: %s" % e)

        sys.exit()

    def run_loop(self):
        while True:
            sys.stdout.write("-----\n")
            sys.stdout.flush()

            self.check_file_change()
            sleep(settings.LOOP_INTERVAL)

            # This will restart on very short downtime, but if it's longer,
            # the MM will crash entirely as it is unable to connect to the WS on boot.
            if not self.check_connection():
                logger.error("Realtime data connection unexpectedly closed, restarting.")
                self.restart()

            self.sanity_check()  # Ensures health of mm - several cut-out points here
            self.print_status()  # Print skew, delta, etc
            self.place_orders()  # Creates desired orders and converges to existing orders

    def restart(self):
        logger.info("Restarting the market maker...")
        os.execv(sys.executable, [sys.executable] + sys.argv)