from __future__ import absolute_import
from time import sleep
import sys
from datetime import date,datetime,timedelta
from os.path import getmtime
import random
import requests
import atexit
import signal
import pandas as pd
import numpy as np

from market_maker import bitmex
from market_maker.settings import settings
from market_maker.utils import log, constants, errors, math, common_util

# Used for reloading the bot - saves modified times of key files
import os

from market_maker.utils.singleton import singleton_data

watched_files_mtimes = [(f, getmtime(f)) for f in settings.WATCHED_FILES]


#
# Helpers
#
logger = log.setup_custom_logger('root')

class ExchangeInterface:
    def __init__(self, dry_run=False):
        logger.info("[ExchangeInterface][__init__]")
        self.dry_run = dry_run
        if len(sys.argv) > 1:
            self.symbol = sys.argv[1]
        else:
            self.symbol = settings.SYMBOL
        self.bitmex = bitmex.BitMEX(base_url=settings.BASE_URL, symbol=self.symbol,
                                    apiKey=settings.API_KEY, apiSecret=settings.API_SECRET,
                                    orderIDPrefix=settings.ORDERID_PREFIX, postOnly=settings.POST_ONLY,
                                    timeout=settings.TIMEOUT)

    def cancel_order(self, order):
        logger.info("[ExchangeInterface][cancel_order]")

        tickLog = self.get_instrument()['tickLog']
        logger.info("Canceling: %s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price']))
        while True:
            try:
                self.bitmex.cancel(order['orderID'])
                sleep(settings.API_REST_INTERVAL)
            except ValueError as e:
                logger.info(e)
                sleep(settings.API_ERROR_INTERVAL)
            else:
                break

    def cancel_all_orders(self, filter='All'):
        # filter : All, Sell, Buy

        logger.info("[ExchangeInterface][cancel_all_orders] filter : " + str(filter))

        if self.dry_run:
            return

        #logger.info("Resetting current position. Canceling all existing orders, filter : " + str(filter))
        tickLog = self.get_instrument()['tickLog']

        # In certain cases, a WS update might not make it through before we call this.
        # For that reason, we grab via HTTP to ensure we grab them all.
        orders = self.bitmex.http_open_orders()
        filteredOrders = []

        for order in orders:
            if filter == 'All' or order['side'] == filter:
                filteredOrders.append(order)
                logger.info("Canceling: %s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price']))

        if len(filteredOrders):
            self.bitmex.cancel([order['orderID'] for order in filteredOrders])

        sleep(settings.API_REST_INTERVAL)

    def get_portfolio(self):
        logger.info("[ExchangeInterface][get_portfolio]")

        contracts = settings.CONTRACTS
        portfolio = {}
        for symbol in contracts:
            position = self.bitmex.position(symbol=symbol)
            instrument = self.bitmex.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):
        logger.info("[ExchangeInterface][calc_delta]")
        """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):
        logger.info("[ExchangeInterface][get_delta]")

        if symbol is None:
            symbol = self.symbol
        return self.get_position(symbol)['currentQty']

    def get_instrument(self, symbol=None):
        #logger.info("[ExchangeInterface][get_instrument]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.instrument(symbol)

    def get_tradeBin(self, bidSize = '1m'):
        update_required = False

        #logger.info("[ExchangeInterface][get_tradeBin1m]")

        if bidSize == '1m' or bidSize == '5m':
            bin_data = self.bitmex.get_trade_bin(bidSize)

        update_required = singleton_data.instance().appendOHLC_data(bin_data, bidSize)

        return update_required

    def get_margin(self):
        logger.info("[ExchangeInterface][get_margin]")

        if self.dry_run:
            return {'marginBalance': float(settings.DRY_BTC), 'availableFunds': float(settings.DRY_BTC)}
        return self.bitmex.funds()

    def get_orders(self, filter='All'):
        # filter : All, Sell, Buy

        #logger.info("[ExchangeInterface][get_orders]")

        if self.dry_run:
            return []

        orders = self.bitmex.open_orders()
        filteredOrders = []

        for order in orders:
            if filter == 'All' or order['side'] == filter:
                filteredOrders.append(order)

        return filteredOrders

    def get_highest_buy(self):
        logger.info("[ExchangeInterface][get_highest_buy]")

        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):
        logger.info("[ExchangeInterface][get_lowest_sell]")

        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):
        #logger.info("[ExchangeInterface][get_position]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.position(symbol)

    def get_avgCostPrice(self, symbol=None):
        #logger.info("[ExchangeInterface][get_avgCostPrice]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.position(symbol)['avgCostPrice']

    def get_currentQty(self, symbol=None):
        #logger.info("[ExchangeInterface][get_currentQty]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.position(symbol)['currentQty']

    def get_ticker(self, symbol=None):
        logger.info("[ExchangeInterface][get_ticker]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.ticker_data(symbol)

    def is_open(self):
        #logger.info("[ExchangeInterface][is_open]")

        """Check that websockets are still open."""
        return not self.bitmex.ws.exited

    def check_market_open(self):
        logger.info("[ExchangeInterface][check_market_open]")

        instrument = self.get_instrument()
        if instrument["state"] != "Open" and instrument["state"] != "Closed":
            raise errors.MarketClosedError("The instrument %s is not open. State: %s" %
                                           (self.symbol, instrument["state"]))

    def check_if_orderbook_empty(self):
        logger.info("[ExchangeInterface][check_if_orderbook_empty]")

        """This function checks whether the order book is empty"""
        instrument = self.get_instrument()
        if instrument['midPrice'] is None:
            raise errors.MarketEmptyError("Orderbook is empty, cannot quote")

    def amend_bulk_orders(self, orders):
        logger.info("[ExchangeInterface][amend_bulk_orders]")

        if self.dry_run:
            return orders
        return self.bitmex.amend_bulk_orders(orders)

    def create_bulk_orders(self, orders):
        logger.info("[ExchangeInterface][create_bulk_orders]")

        if self.dry_run:
            return orders
        return self.bitmex.create_bulk_orders(orders)

    def create_order(self, order_type, quantity, price):
        logger.info("[ExchangeInterface][create_order]")

        if self.dry_run:
            return None

        if order_type == 'Buy':
            return self.bitmex.buy(quantity, price)
        elif order_type == 'Sell':
            return self.bitmex.sell(quantity, price)

    def cancel_bulk_orders(self, orders):
        logger.info("[ExchangeInterface][cancel_bulk_orders]")

        if self.dry_run:
            return orders
        return self.bitmex.cancel([order['orderID'] for order in orders])

    def minutes_of_new_data(self, min_span, binSize = '1m'):
        logger.info("[ExchangeInterface][minutes_of_new_data] : " + binSize)

        if self.dry_run:
            return min_span
        symbol = settings.SYMBOL

        return self.bitmex.minutes_of_new_data(symbol, min_span, binSize)

    def get_user_margin(self):
        logger.info("[ExchangeInterface][get_user_margin]")

        return self.bitmex.get_user_margin()

class OrderManager():
    def __init__(self):
        logger.info("[OrderManager][__init__]")

        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()
        self.setInitOHLC()

    def setInitOHLC(self):
        logger.info("[OrderManager][setInitOHLC]")

        # 1 min
        bin1m_df = self.getBinData('1m')
        singleton_data.instance().setOHLC_1m_data(bin1m_df)

        # 5 mins
        bin5m_df = self.getBinData('5m')
        singleton_data.instance().setOHLC_5m_data(bin5m_df)

    def getBinData(self, binSize = '1m'):
        cnt = 120

        bin_data = self.exchange.minutes_of_new_data(cnt, binSize)
        df = None

        if binSize == '1m' or binSize == '5m':
            df = pd.DataFrame({
                'timestamp' : [bin_data[i]['timestamp'] for i in range(0, len(bin_data))],
                'symbol' : [bin_data[i]['symbol'] for i in range(0, len(bin_data))],
                'open' : [bin_data[i]['open'] for i in range(0, len(bin_data))],
                'high' : [bin_data[i]['high'] for i in range(0, len(bin_data))],
                'low' : [bin_data[i]['low'] for i in range(0, len(bin_data))],
                'close' : [bin_data[i]['close'] for i in range(0, len(bin_data))],
                'trades' : [bin_data[i]['trades'] for i in range(0, len(bin_data))],
                'volume' : [bin_data[i]['volume'] for i in range(0, len(bin_data))],

                #'vwap' : [bin_data[i]['vwap'] for i in range(0, len(bin_data))],
                #'lastSize' : [bin_data[i]['lastSize'] for i in range(0, len(bin_data))],
                #'turnover' : [bin_data[i]['turnover'] for i in range(0, len(bin_data))],
                #'homeNotional' : [bin_data[i]['homeNotional'] for i in range(0, len(bin_data))],
                #'foreignNotional' : [bin_data[i]['foreignNotional'] for i in range(0, len(bin_data))]

                },
                index=pd.to_datetime([bin_data[i]['timestamp'] for i in range(0, len(bin_data))]))

        df["open"] = df["open"].astype(float)
        df["high"] = df["high"].astype(float)
        df["close"] = df["close"].astype(float)
        df["trades"] = df["trades"].astype(float)
        df["volume"] = df["volume"].astype(float)

        #df["vwap"] = df["vwap"].astype(float)
        #df["lastSize"] = df["lastSize"].astype(float)
        #df["turnover"] = df["turnover"].astype(float)
        #df["homeNotional"] = df["homeNotional"].astype(float)
        #df["foreignNotional"] = df["foreignNotional"].astype(float)

        return df

    def reset(self):
        logger.info("[OrderManager][reset]")

        self.exchange.cancel_all_orders()
        self.sanity_check()
        self.print_status()

        # Create orders and converge.
        #self.place_orders()

    def print_status(self):
        logger.info("[OrderManager][print_status]")

        """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):
        logger.info("[OrderManager][get_ticker]")

        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):
        logger.info("[OrderManager][get_price_offset]")

        """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):
        logger.info("[OrderManager][place_orders]")

        """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):
        logger.info("[OrderManager][prepare_order]")

        """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):
        logger.info("[OrderManager][converge_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()
        ret = []

        # 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']))
            ret = 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']))
            ret = self.exchange.cancel_bulk_orders(to_cancel)
        return ret

    ###
    # Position Limits
    ###

    def short_position_limit_exceeded(self):
        logger.info("[OrderManager][short_position_limit_exceeded]")

        """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):
        logger.info("[OrderManager][long_position_limit_exceeded]")

        """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):
        logger.info("[OrderManager][sanity_check]")

        """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):
        #logger.info("[OrderManager][check_file_change]")

        """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):
        #logger.info("[OrderManager][check_connection]")

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

    def exit(self):
        logger.info("[OrderManager][exit]")
        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")
        singleton_data.instance().sendTelegram("Restarting the market maker")
        os.execv(sys.executable, [sys.executable] + sys.argv)
#
# Helpers
#


def XBt_to_XBT(XBt):
    return float(XBt) / constants.XBt_TO_XBT


def cost(instrument, quantity, price):
    mult = instrument["multiplier"]
    P = mult * price if mult >= 0 else mult / price
    return abs(quantity * P)


def margin(instrument, quantity, price):
    return cost(instrument, quantity, price) * instrument["initMargin"]


def run():
    logger.info('BitMEX Market Maker Version: %s\n' % constants.VERSION)

    om = OrderManager()
    # Try/except just keeps ctrl-c from printing an ugly stacktrace
    try:
        om.run_loop()
    except (KeyboardInterrupt, SystemExit):
        sys.exit()

from __future__ import absolute_import
from time import sleep
import sys
from datetime import date,datetime,timedelta
from os.path import getmtime
import random
import requests
import atexit
import signal
import pandas as pd
import numpy as np

from market_maker import bitmex
from market_maker.settings import settings
from market_maker.utils import log, constants, errors, math, common_util

# Used for reloading the bot - saves modified times of key files
import os

from market_maker.utils.singleton import singleton_data

watched_files_mtimes = [(f, getmtime(f)) for f in settings.WATCHED_FILES]


#
# Helpers
#
logger = log.setup_custom_logger('root')

class ExchangeInterface:
    def __init__(self, dry_run=False):
        logger.info("[ExchangeInterface][__init__]")
        self.dry_run = dry_run
        if len(sys.argv) > 1:
            self.symbol = sys.argv[1]
        else:
            self.symbol = settings.SYMBOL
        self.bitmex = bitmex.BitMEX(base_url=settings.BASE_URL, symbol=self.symbol,
                                    apiKey=settings.API_KEY, apiSecret=settings.API_SECRET,
                                    orderIDPrefix=settings.ORDERID_PREFIX, postOnly=settings.POST_ONLY,
                                    timeout=settings.TIMEOUT)

    def cancel_order(self, order):
        logger.info("[ExchangeInterface][cancel_order]")

        tickLog = self.get_instrument()['tickLog']
        logger.info("Canceling: %s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price']))
        while True:
            try:
                self.bitmex.cancel(order['orderID'])
                sleep(settings.API_REST_INTERVAL)
            except ValueError as e:
                logger.info(e)
                sleep(settings.API_ERROR_INTERVAL)
            else:
                break

    def cancel_all_orders(self, filter='All'):
        # filter : All, Sell, Buy

        logger.info("[ExchangeInterface][cancel_all_orders] filter : " + str(filter))

        if self.dry_run:
            return

        #logger.info("Resetting current position. Canceling all existing orders, filter : " + str(filter))
        tickLog = self.get_instrument()['tickLog']

        # In certain cases, a WS update might not make it through before we call this.
        # For that reason, we grab via HTTP to ensure we grab them all.
        orders = self.bitmex.http_open_orders()
        filteredOrders = []

        for order in orders:
            if filter == 'All' or order['side'] == filter:
                filteredOrders.append(order)
                logger.info("Canceling: %s %d @ %.*f" % (order['side'], order['orderQty'], tickLog, order['price']))

        if len(filteredOrders):
            self.bitmex.cancel([order['orderID'] for order in filteredOrders])

        sleep(settings.API_REST_INTERVAL)

    def get_portfolio(self):
        logger.info("[ExchangeInterface][get_portfolio]")

        contracts = settings.CONTRACTS
        portfolio = {}
        for symbol in contracts:
            position = self.bitmex.position(symbol=symbol)
            instrument = self.bitmex.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):
        logger.info("[ExchangeInterface][calc_delta]")
        """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):
        logger.info("[ExchangeInterface][get_delta]")

        if symbol is None:
            symbol = self.symbol
        return self.get_position(symbol)['currentQty']

    def get_instrument(self, symbol=None):
        #logger.info("[ExchangeInterface][get_instrument]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.instrument(symbol)

    def get_tradeBin(self, bidSize = '1m'):
        update_required = False

        #logger.info("[ExchangeInterface][get_tradeBin1m]")

        if bidSize == '1m' or bidSize == '5m':
            bin_data = self.bitmex.get_trade_bin(bidSize)

        update_required = singleton_data.instance().appendOHLC_data(bin_data, bidSize)

        return update_required

    def get_margin(self):
        logger.info("[ExchangeInterface][get_margin]")

        if self.dry_run:
            return {'marginBalance': float(settings.DRY_BTC), 'availableFunds': float(settings.DRY_BTC)}
        return self.bitmex.funds()

    def get_orders(self, filter='All'):
        # filter : All, Sell, Buy

        #logger.info("[ExchangeInterface][get_orders]")

        if self.dry_run:
            return []

        orders = self.bitmex.open_orders()
        filteredOrders = []

        for order in orders:
            if filter == 'All' or order['side'] == filter:
                filteredOrders.append(order)

        return filteredOrders

    def get_highest_buy(self):
        logger.info("[ExchangeInterface][get_highest_buy]")

        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):
        logger.info("[ExchangeInterface][get_lowest_sell]")

        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):
        #logger.info("[ExchangeInterface][get_position]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.position(symbol)

    def get_avgCostPrice(self, symbol=None):
        #logger.info("[ExchangeInterface][get_avgCostPrice]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.position(symbol)['avgCostPrice']

    def get_currentQty(self, symbol=None):
        #logger.info("[ExchangeInterface][get_currentQty]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.position(symbol)['currentQty']

    def get_ticker(self, symbol=None):
        logger.info("[ExchangeInterface][get_ticker]")

        if symbol is None:
            symbol = self.symbol
        return self.bitmex.ticker_data(symbol)

    def is_open(self):
        #logger.info("[ExchangeInterface][is_open]")

        """Check that websockets are still open."""
        return not self.bitmex.ws.exited

    def check_market_open(self):
        logger.info("[ExchangeInterface][check_market_open]")

        instrument = self.get_instrument()
        if instrument["state"] != "Open" and instrument["state"] != "Closed":
            raise errors.MarketClosedError("The instrument %s is not open. State: %s" %
                                           (self.symbol, instrument["state"]))

    def check_if_orderbook_empty(self):
        logger.info("[ExchangeInterface][check_if_orderbook_empty]")

        """This function checks whether the order book is empty"""
        instrument = self.get_instrument()
        if instrument['midPrice'] is None:
            raise errors.MarketEmptyError("Orderbook is empty, cannot quote")

    def amend_bulk_orders(self, orders):
        logger.info("[ExchangeInterface][amend_bulk_orders]")

        if self.dry_run:
            return orders
        return self.bitmex.amend_bulk_orders(orders)

    def create_bulk_orders(self, orders):
        logger.info("[ExchangeInterface][create_bulk_orders]")

        if self.dry_run:
            return orders
        return self.bitmex.create_bulk_orders(orders)

    def create_order(self, order_type, quantity, price):
        logger.info("[ExchangeInterface][create_order]")

        if self.dry_run:
            return None

        if order_type == 'Buy':
            return self.bitmex.buy(quantity, price)
        elif order_type == 'Sell':
            return self.bitmex.sell(quantity, price)

    def cancel_bulk_orders(self, orders):
        logger.info("[ExchangeInterface][cancel_bulk_orders]")

        if self.dry_run:
            return orders
        return self.bitmex.cancel([order['orderID'] for order in orders])

    def minutes_of_new_data(self, min_span, binSize = '1m'):
        logger.info("[ExchangeInterface][minutes_of_new_data] : " + binSize)

        if self.dry_run:
            return min_span
        symbol = settings.SYMBOL

        return self.bitmex.minutes_of_new_data(symbol, min_span, binSize)

    def get_user_margin(self):
        logger.info("[ExchangeInterface][get_user_margin]")

        return self.bitmex.get_user_margin()

class OrderManager():
    def __init__(self):
        logger.info("[OrderManager][__init__]")

        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()
        self.setInitOHLC()

    def setInitOHLC(self):
        logger.info("[OrderManager][setInitOHLC]")

        # 1 min
        bin1m_df = self.getBinData('1m')
        singleton_data.instance().setOHLC_1m_data(bin1m_df)

        # 5 mins
        bin5m_df = self.getBinData('5m')
        singleton_data.instance().setOHLC_5m_data(bin5m_df)

    def getBinData(self, binSize = '1m'):
        cnt = 120

        bin_data = self.exchange.minutes_of_new_data(cnt, binSize)
        df = None

        if binSize == '1m' or binSize == '5m':
            df = pd.DataFrame({
                'timestamp' : [bin_data[i]['timestamp'] for i in range(0, len(bin_data))],
                'symbol' : [bin_data[i]['symbol'] for i in range(0, len(bin_data))],
                'open' : [bin_data[i]['open'] for i in range(0, len(bin_data))],
                'high' : [bin_data[i]['high'] for i in range(0, len(bin_data))],
                'low' : [bin_data[i]['low'] for i in range(0, len(bin_data))],
                'close' : [bin_data[i]['close'] for i in range(0, len(bin_data))],
                'trades' : [bin_data[i]['trades'] for i in range(0, len(bin_data))],
                'volume' : [bin_data[i]['volume'] for i in range(0, len(bin_data))],

                #'vwap' : [bin_data[i]['vwap'] for i in range(0, len(bin_data))],
                #'lastSize' : [bin_data[i]['lastSize'] for i in range(0, len(bin_data))],
                #'turnover' : [bin_data[i]['turnover'] for i in range(0, len(bin_data))],
                #'homeNotional' : [bin_data[i]['homeNotional'] for i in range(0, len(bin_data))],
                #'foreignNotional' : [bin_data[i]['foreignNotional'] for i in range(0, len(bin_data))]

                },
                index=pd.to_datetime([bin_data[i]['timestamp'] for i in range(0, len(bin_data))]))

        df["open"] = df["open"].astype(float)
        df["high"] = df["high"].astype(float)
        df["close"] = df["close"].astype(float)
        df["trades"] = df["trades"].astype(float)
        df["volume"] = df["volume"].astype(float)

        #df["vwap"] = df["vwap"].astype(float)
        #df["lastSize"] = df["lastSize"].astype(float)
        #df["turnover"] = df["turnover"].astype(float)
        #df["homeNotional"] = df["homeNotional"].astype(float)
        #df["foreignNotional"] = df["foreignNotional"].astype(float)

        return df

    def reset(self):
        logger.info("[OrderManager][reset]")

        self.exchange.cancel_all_orders()
        self.sanity_check()
        self.print_status()

        # Create orders and converge.
        #self.place_orders()

    def print_status(self):
        logger.info("[OrderManager][print_status]")

        """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):
        logger.info("[OrderManager][get_ticker]")

        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):
        logger.info("[OrderManager][get_price_offset]")

        """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):
        logger.info("[OrderManager][place_orders]")

        """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):
        logger.info("[OrderManager][prepare_order]")

        """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):
        logger.info("[OrderManager][converge_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()
        ret = []

        # 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']))
            ret = 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']))
            ret = self.exchange.cancel_bulk_orders(to_cancel)
        return ret

    ###
    # Position Limits
    ###

    def short_position_limit_exceeded(self):
        logger.info("[OrderManager][short_position_limit_exceeded]")

        """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):
        logger.info("[OrderManager][long_position_limit_exceeded]")

        """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):
        logger.info("[OrderManager][sanity_check]")

        """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):
        #logger.info("[OrderManager][check_file_change]")

        """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):
        #logger.info("[OrderManager][check_connection]")

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

    def exit(self):
        logger.info("[OrderManager][exit]")
        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")
        singleton_data.instance().sendTelegram("Restarting the market maker")
        os.execv(sys.executable, [sys.executable] + sys.argv)
#
# Helpers
#


def XBt_to_XBT(XBt):
    return float(XBt) / constants.XBt_TO_XBT


def cost(instrument, quantity, price):
    mult = instrument["multiplier"]
    P = mult * price if mult >= 0 else mult / price
    return abs(quantity * P)


def margin(instrument, quantity, price):
    return cost(instrument, quantity, price) * instrument["initMargin"]


def run():
    logger.info('BitMEX Market Maker Version: %s\n' % constants.VERSION)

    om = OrderManager()
    # Try/except just keeps ctrl-c from printing an ugly stacktrace
    try:
        om.run_loop()
    except (KeyboardInterrupt, SystemExit):
        sys.exit()

# Used for reloading the bot - saves modified times of key files # # Helpers # # filter : All, Sell, Buy #logger.info("Resetting current position. Canceling all existing orders, filter : " + str(filter)) # In certain cases, a WS update might not make it through before we call this. # For that reason, we grab via HTTP to ensure we grab them all. Calculate currency delta for portfolio #logger.info("[ExchangeInterface][get_instrument]") #logger.info("[ExchangeInterface][get_tradeBin1m]") # filter : All, Sell, Buy #logger.info("[ExchangeInterface][get_orders]") # ought to be enough for anyone #logger.info("[ExchangeInterface][get_position]") #logger.info("[ExchangeInterface][get_avgCostPrice]") #logger.info("[ExchangeInterface][get_currentQty]") #logger.info("[ExchangeInterface][is_open]") Check that websockets are still open. This function checks whether the order book is empty # Once exchange is created, register exit handler that will always cancel orders # on any error. # 1 min # 5 mins #'vwap' : [bin_data[i]['vwap'] for i in range(0, len(bin_data))], #'lastSize' : [bin_data[i]['lastSize'] for i in range(0, len(bin_data))], #'turnover' : [bin_data[i]['turnover'] for i in range(0, len(bin_data))], #'homeNotional' : [bin_data[i]['homeNotional'] for i in range(0, len(bin_data))], #'foreignNotional' : [bin_data[i]['foreignNotional'] for i in range(0, len(bin_data))] #df["vwap"] = df["vwap"].astype(float) #df["lastSize"] = df["lastSize"].astype(float) #df["turnover"] = df["turnover"].astype(float) #df["homeNotional"] = df["homeNotional"].astype(float) #df["foreignNotional"] = df["foreignNotional"].astype(float) # Create orders and converge. #self.place_orders() Print the current MM status. # 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. # 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. # Back off if our spread is too small. # Midpoint, used for simpler order placement. 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 # First positions (index 1, -1) should start right at start_position, others should branch from there # Offset mode: ticker comes from a reference exchange and we define an offset. # If we're attempting to sell, but our sell price is actually lower than the buy, # move over to the sell side. # Same for buys. ### # Orders ### Create order items for use in convergence. # 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. Create an order object. 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. # 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. # Found an existing order. Do we need to amend it? # If price has changed, and the change is more than our RELIST_INTERVAL, amend. # Will throw if there isn't a desired order to match. In that case, cancel it. # 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. # Could happen if we exceed a delta limit ### # Position Limits ### Returns True if the short position limit is exceeded Returns True if the long position limit is exceeded ### # Sanity ## Perform checks before placing orders. # Check if OB is empty - if so, can't quote. # Ensure market is still open. # Get ticker, which sets price offsets and prints some debugging info. # Sanity check: # Messaging if the position limits are reached ### # Running ### #logger.info("[OrderManager][check_file_change]") Restart if any files we're watching have changed. #logger.info("[OrderManager][check_connection]") Ensure the WS connections are still open. # 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.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 # # Helpers # # Try/except just keeps ctrl-c from printing an ugly stacktrace