예제 #1
0
    def test_subscribe_margin(self):
        ws = BitMexWs(account=self.account, pair=self.pair)

        def subscribe(x):
            print(x)
            self.complete()

        ws.bind('margin', subscribe)

        self.wait_complete()
        ws.close()
예제 #2
0
파일: bitmex.py 프로젝트: jerryqhyu/thebot
class BitMex:
    wallet = None
    market_price = 0
    position = None
    margin = None
    bin_size = '1h'
    private_client = None
    public_client = None
    is_running = True
    crawler = None
    strategy = None
    enable_trade_log = True
    ohlcv_len = 100
    data = None
    exit_order = {'profit': 0, 'loss': 0, 'trail_offset': 0}
    trail_price = 0
    last_action_time = None

    def __init__(self, demo=False, threading=True):
        """
        :param demo:
        :param run:
        """
        self.demo = demo
        self.is_running = threading

    def __init_client(self):
        """
        初期化関数
        """
        if self.private_client is not None and self.public_client is not None:
            return
        api_key = os.environ.get(
            "BITMEX_TEST_APIKEY") if self.demo else os.environ.get(
                "BITMEX_APIKEY")
        api_secret = os.environ.get(
            "BITMEX_TEST_SECRET") if self.demo else os.environ.get(
                "BITMEX_SECRET")
        self.private_client = bitmex_api(test=self.demo,
                                         api_key=api_key,
                                         api_secret=api_secret)
        self.public_client = bitmex_api(test=self.demo)

    def now_time(self):
        """
        現在の時間
        """
        return datetime.now().astimezone(UTC)

    def get_retain_rate(self):
        """
        証拠金維持率。
        :return:
        """
        return 0.8

    def get_lot(self):
        """
        ロットの計算を行う。
        :return:
        """
        margin = self.get_margin()
        position = self.get_position()
        return math.floor(
            (1 - self.get_retain_rate()) * self.get_market_price() *
            margin['excessMargin'] / (position['initMarginReq'] * 100000000))

    def get_balance(self):
        """
        残高の取得を行う。
        :return:
        """
        self.__init_client()
        return self.get_margin()["walletBalance"]

    def get_margin(self):
        """
        マージンの取得
        :return:
        """
        self.__init_client()
        if self.margin is not None:
            return self.margin
        else:  # WebSocketで取得できていない場合
            self.margin = retry(lambda: self.private_client.User.
                                User_getMargin(currency="XBt").result())
            return self.margin

    def get_leverage(self):
        """
        レバレッジの取得する。
        :return:
        """
        self.__init_client()
        return self.get_position()["leverage"]

    def get_position(self):
        """
        現在のポジションを取得する。
        :return:
        """
        self.__init_client()
        if self.position is not None:
            return self.position
        else:  # WebSocketで取得できていない場合
            ret = retry(lambda: self.private_client.Position.Position_get(
                filter=json.dumps({"symbol": "XBTUSD"})).result())
            if len(ret) > 0:
                self.position = ret[0]
            return self.position

    def get_position_size(self):
        """
        現在のポジションサイズを取得する。
        :return:
        """
        self.__init_client()
        return self.get_position()['currentQty']

    def get_position_avg_price(self):
        """
        現在のポジションの平均価格を取得する。
        :return:
        """
        self.__init_client()
        return self.get_position()['avgEntryPrice']

    def get_market_price(self):
        """
        現在の取引額を取得する。
        :return:
        """
        self.__init_client()
        if self.market_price != 0:
            return self.market_price
        else:  # WebSocketで取得できていない場合
            self.market_price = retry(
                lambda: self.public_client.Instrument.Instrument_get(
                    symbol="XBTUSD").result())[0]["lastPrice"]
            return self.market_price

    def get_trail_price(self):
        """
        Trail Priceを取得する。
        :return:
        """
        return self.trail_price

    def set_trail_price(self, value):
        """
        Trail Priceを設定する。
        :return:
        """
        self.trail_price = value

    def get_commission(self):
        """
        手数料を取得する。
        :return:
        """
        return 0.075 / 100

    def cancel_all(self):
        """
        すべての注文をキャンセルする。
        """
        self.__init_client()
        orders = retry(
            lambda: self.private_client.Order.Order_cancelAll().result())
        for order in orders:
            logger.info(
                f"Cancel Order : (orderID, orderType, side, orderQty, limit, stop) = "
                f"({order['orderID']}, {order['ordType']}, {order['side']}, {order['orderQty']}, "
                f"{order['price']}, {order['stopPx']})")
        logger.info(f"Cancel All Order")

    def close_all(self):
        """
        すべてのポジションを解消する。
        """
        self.__init_client()
        order = retry(lambda: self.private_client.Order.Order_closePosition(
            symbol="XBTUSD").result())
        logger.info(
            f"Close Position : (orderID, orderType, side, orderQty, limit, stop) = "
            f"({order['orderID']}, {order['ordType']}, {order['side']}, {order['orderQty']}, "
            f"{order['price']}, {order['stopPx']})")
        logger.info(f"Close All Position")

    def cancel(self, id):
        """
        注文をキャンセルする。
        :param id: 注文番号
        :return 成功したか:
        """
        self.__init_client()
        order = self.get_open_order(id)
        if order is None:
            return False

        try:
            retry(lambda: self.private_client.Order.Order_cancel(orderID=order[
                'orderID']).result())[0]
        except HTTPNotFound:
            return False
        logger.info(
            f"Cancel Order : (orderID, orderType, side, orderQty, limit, stop) = "
            f"({order['orderID']}, {order['ordType']}, {order['side']}, {order['orderQty']}, "
            f"{order['price']}, {order['stopPx']})")
        return True

    def __new_order(self,
                    ord_id,
                    side,
                    ord_qty,
                    limit=0,
                    stop=0,
                    post_only=False):
        """
        注文を作成する
        """
        if limit > 0 and post_only:
            ord_type = "Limit"
            retry(lambda: self.private_client.Order.Order_new(
                symbol="XBTUSD",
                ordType=ord_type,
                clOrdID=ord_id,
                side=side,
                orderQty=ord_qty,
                price=limit,
                execInst='ParticipateDoNotInitiate').result())
        elif limit > 0 and stop > 0:
            ord_type = "StopLimit"
            retry(lambda: self.private_client.Order.Order_new(symbol="XBTUSD",
                                                              ordType=ord_type,
                                                              clOrdID=ord_id,
                                                              side=side,
                                                              orderQty=ord_qty,
                                                              price=limit,
                                                              stopPx=stop).
                  result())
        elif limit > 0:
            ord_type = "Limit"
            retry(lambda: self.private_client.Order.Order_new(symbol="XBTUSD",
                                                              ordType=ord_type,
                                                              clOrdID=ord_id,
                                                              side=side,
                                                              orderQty=ord_qty,
                                                              price=limit).
                  result())
        elif stop > 0:
            ord_type = "Stop"
            retry(lambda: self.private_client.Order.Order_new(symbol="XBTUSD",
                                                              ordType=ord_type,
                                                              clOrdID=ord_id,
                                                              side=side,
                                                              orderQty=ord_qty,
                                                              stopPx=stop).
                  result())
        elif post_only:  # market order with post only
            ord_type = "Limit"
            i = 0
            while True:
                prices = self.ob.get_prices()
                limit = prices[1] if side == "Buy" else prices[0]
                retry(lambda: self.private_client.Order.Order_new(
                    symbol="XBTUSD",
                    ordType=ord_type,
                    clOrdID=ord_id,
                    side=side,
                    orderQty=ord_qty,
                    price=limit,
                    execInst='ParticipateDoNotInitiate').result())
                time.sleep(1)

                if not self.cancel(ord_id):
                    break
                time.sleep(2)
                i += 1
                if i > 10:
                    break
            self.cancel_all()
        else:
            ord_type = "Market"
            retry(lambda: self.private_client.Order.Order_new(symbol="XBTUSD",
                                                              ordType=ord_type,
                                                              clOrdID=ord_id,
                                                              side=side,
                                                              orderQty=ord_qty)
                  .result())

        if self.enable_trade_log:
            logger.info(f"========= New Order ==============")
            logger.info(f"ID     : {ord_id}")
            logger.info(f"Type   : {ord_type}")
            logger.info(f"Side   : {side}")
            logger.info(f"Qty    : {ord_qty}")
            logger.info(f"Limit  : {limit}")
            logger.info(f"Stop   : {stop}")
            logger.info(f"======================================")

    def __amend_order(self,
                      ord_id,
                      side,
                      ord_qty,
                      limit=0,
                      stop=0,
                      post_only=False):
        """
        注文を更新する
        """
        if limit > 0 and stop > 0:
            ord_type = "StopLimit"
            retry(lambda: self.private_client.Order.Order_amend(
                origClOrdID=ord_id, orderQty=ord_qty, price=limit, stopPx=stop)
                  .result())
        elif limit > 0:
            ord_type = "Limit"
            retry(lambda: self.private_client.Order.Order_amend(
                origClOrdID=ord_id, orderQty=ord_qty, price=limit).result())
        elif stop > 0:
            ord_type = "Stop"
            retry(lambda: self.private_client.Order.Order_amend(
                origClOrdID=ord_id, orderQty=ord_qty, stopPx=stop).result())
        elif post_only:  # market order with post only
            ord_type = "Limit"
            prices = self.ob.get_prices()
            limit = prices[1] if side == "Buy" else prices[0]
            retry(lambda: self.private_client.Order.Order_amend(
                origClOrdID=ord_id, orderQty=ord_qty, price=limit).result())
        else:
            ord_type = "Market"
            retry(lambda: self.private_client.Order.Order_amend(
                origClOrdID=ord_id, orderQty=ord_qty).result())

        if self.enable_trade_log:
            logger.info(f"========= Amend Order ==============")
            logger.info(f"ID     : {ord_id}")
            logger.info(f"Type   : {ord_type}")
            logger.info(f"Side   : {side}")
            logger.info(f"Qty    : {ord_qty}")
            logger.info(f"Limit  : {limit}")
            logger.info(f"Stop   : {stop}")
            logger.info(f"======================================")

    def entry(self,
              id,
              long,
              qty,
              limit=0,
              stop=0,
              post_only=False,
              when=True):
        """
        注文をする。pineの関数と同等の機能。
        https://jp.tradingview.com/study-script-reference/#fun_strategy{dot}entry
        :param id: 注文の番号
        :param long: ロング or ショート
        :param qty: 注文量
        :param limit: 指値
        :param stop: ストップ指値
        :param post_only: ポストオンリー
        :param when: 注文するか
        :return:
        """
        self.__init_client()

        if self.get_margin()['excessMargin'] <= 0 or qty <= 0:
            return

        if not when:
            return

        pos_size = self.get_position_size()

        if long and pos_size > 0:
            return

        if not long and pos_size < 0:
            return

        ord_qty = qty + abs(pos_size)

        self.order(id, long, ord_qty, limit, stop, post_only, when)

    def order(self,
              id,
              long,
              qty,
              limit=0,
              stop=0,
              post_only=False,
              when=True):
        """
        注文をする。pineの関数と同等の機能。
        https://jp.tradingview.com/study-script-reference/#fun_strategy{dot}order
        :param id: 注文の番号
        :param long: ロング or ショート
        :param qty: 注文量
        :param limit: 指値
        :param stop: ストップ指値
        :param post_only: ポストオンリー
        :param when: 注文するか
        :return:
        """
        self.__init_client()

        if self.get_margin()['excessMargin'] <= 0 or qty <= 0:
            return

        if not when:
            return

        side = "Buy" if long else "Sell"
        ord_qty = qty

        order = self.get_open_order(id)
        ord_id = id + ord_suffix() if order is None else order["clOrdID"]

        if order is None:
            self.__new_order(ord_id, side, ord_qty, limit, stop, post_only)
        else:
            self.__amend_order(ord_id, side, ord_qty, limit, stop, post_only)

    def get_open_order(self, id):
        """
        注文を取得する。
        :param id: 注文番号
        :return:
        """
        self.__init_client()
        open_orders = retry(lambda: self.private_client.Order.Order_getOrders(
            filter=json.dumps({
                "symbol": "XBTUSD",
                "open": True
            })).result())
        open_orders = [o for o in open_orders if o["clOrdID"].startswith(id)]
        if len(open_orders) > 0:
            return open_orders[0]
        else:
            return None

    def exit(self, profit=0, loss=0, trail_offset=0):
        """
        利確、損切戦略の登録 lossとtrail_offsetが両方設定されたら、trail_offsetが優先される
        :param profit: 利益(ティックで指定する)
        :param loss: 損切(ティックで指定する)
        :param trail_offset: トレーリングストップの価格(ティックで指定)
        """
        self.exit_order = {
            'profit': profit,
            'loss': loss,
            'trail_offset': trail_offset
        }

    def get_exit_order(self):
        """
        利確、損切戦略を取得する
        """
        return self.exit_order

    def eval_exit(self):
        """
        利確、損切戦略の評価
        """
        if self.get_position_size() == 0:
            return

        unrealised_pnl = self.get_position()['unrealisedPnl']

        # trail assetが設定されていたら
        if self.get_exit_order()['trail_offset'] > 0 and self.get_trail_price(
        ) > 0:
            if self.get_position_size() > 0 and \
                    self.get_market_price() - self.get_exit_order()['trail_offset'] < self.get_trail_price():
                logger.info(
                    f"Loss cut by trailing stop: {self.get_exit_order()['trail_offset']}"
                )
                self.close_all()
            elif self.get_position_size() < 0 and \
                    self.get_market_price() + self.get_exit_order()['trail_offset'] > self.get_trail_price():
                logger.info(
                    f"Loss cut by trailing stop: {self.get_exit_order()['trail_offset']}"
                )
                self.close_all()

        # lossが設定されていたら
        if unrealised_pnl < 0 and \
                0 < self.get_exit_order()['loss'] < abs(unrealised_pnl / 100000000):
            logger.info(
                f"Loss cut by stop loss: {self.get_exit_order()['loss']}")
            self.close_all()

        # profitが設定されていたら
        if unrealised_pnl > 0 and \
                0 < self.get_exit_order()['profit'] < abs(unrealised_pnl / 100000000):
            logger.info(
                f"Take profit by stop profit: {self.get_exit_order()['profit']}"
            )
            self.close_all()

    def fetch_ohlcv(self, bin_size, start_time, end_time):
        """
        足データを取得する
        :param start_time: 開始時間
        :param end_time: 終了時間
        :return:
        """
        self.__init_client()

        fetch_bin_size = allowed_range[bin_size][0]
        left_time = start_time
        right_time = end_time
        data = to_data_frame([])

        while True:
            source = retry(lambda: self.public_client.Trade.Trade_getBucketed(
                symbol="XBTUSD",
                binSize=fetch_bin_size,
                startTime=left_time,
                endTime=right_time,
                count=500,
                partial=False).result())
            if len(source) == 0:
                break

            source = to_data_frame(source)
            data = pd.concat([data, source])

            if right_time > source.iloc[-1].name + delta(fetch_bin_size):
                left_time = source.iloc[-1].name + delta(fetch_bin_size)
                time.sleep(2)
            else:
                break

        return resample(data, bin_size)

    def security(self, bin_size):
        """
        別時間軸データを再計算して、取得する
        """
        return resample(self.data, bin_size)[:-1]

    def __update_ohlcv(self, action, new_data):
        """
        データを取得して、戦略を実行する。
        """

        if self.data is None:
            end_time = datetime.now(timezone.utc)
            start_time = end_time - self.ohlcv_len * delta(self.bin_size)
            d1 = self.fetch_ohlcv(self.bin_size, start_time, end_time)
            if len(d1) > 0:
                d2 = self.fetch_ohlcv(
                    allowed_range[self.bin_size][0],
                    d1.iloc[-1].name + delta(allowed_range[self.bin_size][0]),
                    end_time)

                self.data = pd.concat([d1, d2])
            else:
                self.data = d1
        else:
            self.data = pd.concat([self.data, new_data])

        # 最後の行は不確定情報のため、排除する
        re_sample_data = resample(self.data, self.bin_size)[:-1]

        if self.data.iloc[-1].name == re_sample_data.iloc[-1].name:
            self.data = re_sample_data.iloc[-1 * self.ohlcv_len:, :]

        if self.last_action_time is not None and \
                self.last_action_time == re_sample_data.iloc[-1].name:
            return

        open = re_sample_data['open'].values
        close = re_sample_data['close'].values
        high = re_sample_data['high'].values
        low = re_sample_data['low'].values
        volume = re_sample_data['volume'].values

        try:
            if self.strategy is not None:
                self.strategy(open, close, high, low, volume)
            self.last_action_time = re_sample_data.iloc[-1].name
        except FatalError as e:
            # 致命的エラー
            logger.error(f"Fatal error. {e}")
            logger.error(traceback.format_exc())
            self.stop()
        except Exception as e:
            logger.error(f"An error occurred. {e}")
            logger.error(traceback.format_exc())

    def __on_update_instrument(self, action, instrument):
        """
         取引価格を更新する
        """
        if 'lastPrice' in instrument:
            self.market_price = instrument['lastPrice']

            # trail priceの更新
            if self.get_position_size() > 0 and \
                    self.market_price > self.get_trail_price():
                self.set_trail_price(self.market_price)
            if self.get_position_size() < 0 and \
                    self.market_price < self.get_trail_price():
                self.set_trail_price(self.market_price)

    def __on_update_wallet(self, action, wallet):
        """
         walletを更新する
        """
        self.wallet = {
            **self.wallet,
            **wallet
        } if self.wallet is not None else self.wallet

    def __on_update_position(self, action, position):
        """
         ポジションを更新する
        """
        # ポジションサイズの変更がされたか
        is_update_pos_size = self.get_position(
        )['currentQty'] != position['currentQty']

        # ポジションサイズが変更された場合、トレイル開始価格を現在の価格にリセットする
        if is_update_pos_size and position['currentQty'] != 0:
            self.set_trail_price(self.market_price)

        if is_update_pos_size:
            logger.info(
                f"Updated Position\n"
                f"Price: {self.get_position()['avgEntryPrice']} => {position['avgEntryPrice']}\n"
                f"Qty: {self.get_position()['currentQty']} => {position['currentQty']}\n"
                f"Balance: {self.get_balance()/100000000} XBT")

        self.position = {
            **self.position,
            **position
        } if self.position is not None else self.position

        # 利確損切の評価
        self.eval_exit()

    def __on_update_margin(self, action, margin):
        """
         マージンを更新する
        """
        self.margin = {
            **self.margin,
            **margin
        } if self.margin is not None else self.margin

    def on_update(self, bin_size, strategy):
        """
        戦略の関数を登録する。
        :param strategy:
        """
        self.bin_size = bin_size
        self.strategy = strategy
        if self.is_running:
            self.ws = BitMexWs(test=self.demo)
            self.ws.bind(allowed_range[bin_size][0], self.__update_ohlcv)
            self.ws.bind('instrument', self.__on_update_instrument)
            self.ws.bind('wallet', self.__on_update_wallet)
            self.ws.bind('position', self.__on_update_position)
            self.ws.bind('margin', self.__on_update_margin)
            self.ob = OrderBook(self.ws)

    def stop(self):
        """
        クローラーを止める。
        """
        self.is_running = False
        self.ws.close()

    def show_result(self):
        """
        取引結果を表示する。
        """
        pass

    def plot(self, name, value, color, overlay=True):
        """
        グラフに描画する。
        """
        pass
예제 #3
0
 def test_setup(self):
     ws = BitMexWs(account=self.account, pair=self.pair)
     ws.close()
예제 #4
0
class BitMex:      
    # Use minute granularity?
    minute_granularity = False
    # Sort timeframe execution when multiple timeframes
    timeframes_sorted = True # True for higher first, False for lower first and None when off      
    # Enable log output
    enable_trade_log = True
    # OHLCV length
    ohlcv_len = 100    
    # Round decimals
    round_decimals = 0

    def __init__(self, account, pair, demo=False, threading=True):
        """
        constructor
        :account:
        :pair:
        :param demo:
        :param run:
        """
        # Account
        self.account = account
        # Pair
        self.pair = pair
        # Use testnet?
        self.demo = demo
        # Is bot running
        self.is_running = threading
        # Wallet
        self.wallet = None
         # Price
        self.market_price = 0
        # Order update
        self.order_update = None
        # Position
        self.position = None
        # Margin
        self.margin = None
        # Time Frame
        self.bin_size = ['1h']
        # Client for private API
        self.private_client = None
        # Client for public API
        self.public_client = None
        # Bar crawler
        self.crawler = None
        # Strategy
        self.strategy = None
        # OHLCV data
        self.timeframe_data = None
        # Timeframe data info like partial candle data values, last candle values, last action etc.
        self.timeframe_info = {}
        # Profit target long and short for a simple limit exit strategy
        self.sltp_values = {
                        'profit_long': 0,
                        'profit_short': 0,
                        'stop_long': 0,
                        'stop_short': 0,
                        'eval_tp_next_candle': False,
                        'profit_long_callback': None,
                        'profit_short_callback': None,
                        'stop_long_callback': None,
                        'stop_short_callback': None
                        }         
        # Profit, Loss and Trail Offset
        self.exit_order = {
                        'profit': 0, 
                        'loss': 0, 
                        'trail_offset': 0, 
                        'profit_callback': None,
                        'loss_callback': None,
                        'trail_callbak': None
                        }
        # Trailing Stop
        self.trail_price = 0
        # Order callbacks
        self.callbacks = {}
        # Last strategy execution time
        self.last_action_time = None
        
    def __init_client(self):
        """
        initialization of client
        """
        if self.private_client is not None and self.public_client is not None:
            return
       
        api_key =  conf['bitmex_test_keys'][self.account]['API_KEY'] if self.demo else conf['bitmex_keys'][self.account]['API_KEY']        
        api_secret = conf['bitmex_test_keys'][self.account]['SECRET_KEY'] if self.demo else conf['bitmex_keys'][self.account]['SECRET_KEY']

        self.private_client = bitmex_api(test=self.demo, api_key=api_key, api_secret=api_secret)
        self.public_client = bitmex_api(test=self.demo)
        
    def now_time(self):
        """
        current time
        """
        return datetime.now().astimezone(UTC)
        
    def get_retain_rate(self):
        """
        maintenance margin
        :return:
        """
        return 0.8

    def get_lot(self):
        """
        lot calculation
        :return:
        """
        margin = self.get_margin()
        position = self.get_position()
        return math.floor((1 - self.get_retain_rate()) * self.get_market_price()
                          * margin['excessMargin'] / (position['initMarginReq'] * 100000000))        

    def get_balance(self):
        """
        get balance
        :return:
        """
        self.__init_client()
        return self.get_margin()["walletBalance"]

    def get_margin(self):
        """
        get margin
        :return:
        """
        self.__init_client()
        if self.margin is not None:
            return self.margin
        else:  # when the WebSocket cant get it
            self.margin = retry(lambda: self.private_client
                                .User.User_getMargin(currency="XBt").result())
            return self.margin        

    def get_leverage(self):
        """
        get leverage
        :return:
        """
        self.__init_client()
        return self.get_position()["leverage"]

    def get_position(self):
        """
        get the current position
        :return:
        """
        self.__init_client()
        if self.position is not None:
            return self.position
        else:  # when the WebSocket cant get it
            ret = retry(lambda: self.private_client
                                  .Position.Position_get(filter=json.dumps({"symbol": self.pair})).result())
            if len(ret) > 0:
                self.position = ret[0]
            return self.position

    def get_position_size(self):
        """
        get position size
        :return:
        """
        self.__init_client() 
        position_size = self.get_position()
        if position_size is not None:
            return position_size['currentQty']
        else:
            return 0

    def get_position_avg_price(self):
        """
        get average price of the current position
        :return:
        """
        self.__init_client()
        return self.get_position()['avgEntryPrice']

    def get_market_price(self):
        """
        get current price
        :return:
        """
        self.__init_client()
        if self.market_price != 0:
            return self.market_price
        else:  # when the WebSocket cant get it
            self.market_price = retry(lambda: self.public_client
                                      .Instrument.Instrument_get(symbol=self.pair).result())[0]["lastPrice"]
            return self.market_price
        
    def get_trail_price(self):
        """
        get Trail Price。
        :return:
        """
        return self.trail_price

    def set_trail_price(self, value):
        """
        set Trail Price
        :return:
        """
        self.trail_price = value

    def get_commission(self):
        """
        get commission
        :return:
        """
        return 0.075 / 100

    def cancel_all(self):
        """
        market close opened position for this pair
        """
        self.__init_client()
        orders = retry(lambda: self.private_client.Order.Order_cancelAll(symbol=self.pair).result())
        for order in orders:
            logger.info(f"Cancel Order : (orderID, orderType, side, orderQty, limit, stop) = "
                        f"({order['orderID']}, {order['ordType']}, {order['side']}, {order['orderQty']}, "
                        f"{order['price']}, {order['stopPx']})")
        logger.info(f"Cancel All Order")
        self.callbacks = {}

    def close_all(self, callback=None):
        """
        Close all positions for this pair
        """
        self.__init_client()
        order = retry(lambda: self.private_client.Order.Order_closePosition(symbol=self.pair).result())
        self.callbacks[order['orderID']] = callback
        logger.info(f"Close Position : (orderID, orderType, side, orderQty, limit, stop) = "
                    f"({order['orderID']}, {order['ordType']}, {order['side']}, {order['orderQty']}, "
                    f"{order['price']}, {order['stopPx']})")
        logger.info(f"Close All Position")

    def cancel(self, id):
        """
        Cancel a specific order by id
        :param id: id of the order
        :return: result
        """
        self.__init_client()
        order = self.get_open_order(id)
        if order is None:
            return False

        try:
            retry(lambda: self.private_client.Order.Order_cancel(orderID=order['orderID']).result())[0]
        except HTTPNotFound:
            return False
        logger.info(f"Cancel Order : (orderID, orderType, side, orderQty, limit, stop) = "
                    f"({order['orderID']}, {order['ordType']}, {order['side']}, {order['orderQty']}, "
                    f"{order['price']}, {order['stopPx']})")
        self.callbacks.pop(order['orderID'])
        return True

    def __new_order(self, ord_id, side, ord_qty, limit=0, stop=0, post_only=False, reduce_only=False):
        """
        create an order
        """
        logger.info(f"{ord_id} {side} {ord_qty} {stop}")
        if limit > 0 and post_only:
            ord_type = "Limit"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty, price=limit,
                                                              execInst='ParticipateDoNotInitiate').result())
        elif limit > 0 and stop > 0 and reduce_only:
            ord_type = "StopLimit"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty, price=limit,
                                                              stopPx=stop, execInst='LastPrice,Close').result())
        elif limit > 0 and reduce_only:
            ord_type = "Limit"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty, price=limit,
                                                              execInst='ReduceOnly').result())        
        elif limit > 0 and stop > 0:
            ord_type = "StopLimit"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty, price=limit,
                                                              stopPx=stop).result())
        elif limit > 0:
            ord_type = "Limit"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty, price=limit).result())
        elif stop > 0 and reduce_only:
            ord_type = "Stop"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty, stopPx=stop,
                                                              execInst='LastPrice,Close').result())
        elif stop > 0:
            ord_type = "Stop"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty, stopPx=stop).result())
        elif post_only: # limit order with post only loop
            ord_type = "Limit"
            i = 0
            while True:
                prices = self.ob.get_prices()
                limit = prices[0] if side == "Buy" else prices[1]                
                retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                                  side=side, orderQty=ord_qty, price=limit,
                                                                  execInst='ParticipateDoNotInitiate').result())
                time.sleep(1)

                if not self.cancel(ord_id):
                    break
                time.sleep(2)
                i += 1
                if i > 10:
                    notify(f"Order retry count exceed")
                    break
            self.cancel_all()
        else:
            ord_type = "Market"
            retry(lambda: self.private_client.Order.Order_new(symbol=self.pair, ordType=ord_type, clOrdID=ord_id,
                                                              side=side, orderQty=ord_qty).result())

        if self.enable_trade_log:
            logger.info(f"========= New Order ==============")
            logger.info(f"ID     : {ord_id}")
            logger.info(f"Type   : {ord_type}")
            logger.info(f"Side   : {side}")
            logger.info(f"Qty    : {ord_qty}")
            logger.info(f"Limit  : {limit}")
            logger.info(f"Stop   : {stop}")
            logger.info(f"======================================")

            notify(f"New Order\nType: {ord_type}\nSide: {side}\nQty: {ord_qty}\nLimit: {limit}\nStop: {stop}")

    def __amend_order(self, ord_id, side, ord_qty, limit=0, stop=0, post_only=False):
        """
        Amend order
        """
        if limit > 0 and stop > 0:
            ord_type = "StopLimit"
            retry(lambda: self.private_client.Order.Order_amend(origClOrdID=ord_id,
                                                                orderQty=ord_qty, price=limit, stopPx=stop).result())
        elif limit > 0:
            ord_type = "Limit"
            retry(lambda: self.private_client.Order.Order_amend(origClOrdID=ord_id,
                                                                orderQty=ord_qty, price=limit).result())
        elif stop > 0:
            ord_type = "Stop"
            retry(lambda: self.private_client.Order.Order_amend(origClOrdID=ord_id,
                                                                orderQty=ord_qty, stopPx=stop).result())
        elif post_only: # market order with post only
            ord_type = "Limit"
            prices = self.ob.get_prices()
            limit = prices[1] if side == "Buy" else prices[0]
            retry(lambda: self.private_client.Order.Order_amend(origClOrdID=ord_id,
                                                                orderQty=ord_qty, price=limit).result())
        else:
            ord_type = "Market"
            retry(lambda: self.private_client.Order.Order_amend(origClOrdID=ord_id,
                                                                orderQty=ord_qty).result())

        if self.enable_trade_log:
            logger.info(f"========= Amend Order ==============")
            logger.info(f"ID     : {ord_id}")
            logger.info(f"Type   : {ord_type}")
            logger.info(f"Side   : {side}")
            logger.info(f"Qty    : {ord_qty}")
            logger.info(f"Limit  : {limit}")
            logger.info(f"Stop   : {stop}")
            logger.info(f"======================================")

            notify(f"Amend Order\nType: {ord_type}\nSide: {side}\nQty: {ord_qty}\nLimit: {limit}\nStop: {stop}")

    def entry(self, id, long, qty, limit=0, stop=0, post_only=False, reduce_only=False, allow_amend=False, when=True, round_decimals=0, callback=None):
        """
        places an entry order, works as equivalent to tradingview pine script implementation
        https://tradingview.com/study-script-reference/#fun_strategy{dot}entry
        :param id: Order id
        :param long: Long or Short
        :param qty: Quantity
        :param limit: Limit price
        :param stop: Stop limit
        :param post_only: Post only
        :param reduce_only: Reduce Only means that your existing position cannot be increased only reduced by this order
        :param when: Do you want to execute the order or not - True for live trading
        :return:
        """        
        self.__init_client()

        if self.get_margin()['excessMargin'] <= 0 or qty <= 0:
            return
       
        if not when:
            return
       
        pos_size = self.get_position_size()

        if long and pos_size > 0:
            return
            logger.info(f"11")
        if not long and pos_size < 0:
            logger.info(f"11")
            return
        
        ord_qty = qty + abs(pos_size)
        ord_qty = round(ord_qty, round_decimals)

        self.order(id, long, ord_qty, limit, stop, post_only, reduce_only, allow_amend, when, callback)

    def entry_pyramiding(self, id, long, qty, limit=0, stop=0, post_only=False, reduce_only=False, cancel_all=False, pyramiding=2, allow_amend=False, when=True, round_decimals=0, callback=None):
        """
        places an entry order, works as equivalent to tradingview pine script implementation with pyramiding
        https://tradingview.com/study-script-reference/#fun_strategy{dot}entry
        :param id: Order id
        :param long: Long or Short
        :param qty: Quantity
        :param limit: Limit price
        :param stop: Stop limit
        :param post_only: Post only
        :param reduce_only: Reduce Only means that your existing position cannot be increased only reduced by this order
        :param cancell_all: cancell all open order before sending the entry order?
        :param pyramiding: number of entries you want in pyramiding
        :param when: Do you want to execute the order or not - True for live trading
        :return:
        """       

        # if self.get_margin()['excessMargin'] <= 0 or qty <= 0:
        #     return
        if qty <= 0:
            return

        if not when:
            return

        pos_size = self.get_position_size()

        if long and pos_size >= pyramiding*qty:
            return

        if not long and pos_size <= -(pyramiding*qty):
            return
        
        if cancel_all:
            self.cancel_all()   

        if long and pos_size < 0:
            ord_qty = qty + abs(pos_size)
        elif not long and pos_size > 0:
            ord_qty = qty + abs(pos_size)
        else:
            ord_qty = qty  
        
        if long and (pos_size + qty > pyramiding*qty):
            ord_qty = pyramiding*qty - abs(pos_size)

        if not long and (pos_size - qty < -(pyramiding*qty)):
            ord_qty = pyramiding*qty - abs(pos_size)
        # make sure it doesnt spam small entries, which in most cases would trigger risk management orders evaluation, you can make this less than 2% if needed  
        if ord_qty < ((pyramiding*qty) / 100) * 2:
            return       

        ord_qty = round(ord_qty, round_decimals)

        self.order(id, long, ord_qty, limit, stop, post_only, reduce_only, allow_amend, when, callback)

    def order(self, id, long, qty, limit=0, stop=0, post_only=False, reduce_only=False, allow_amend=False, when=True, callback=None):
        """
        places an order, works as equivalent to tradingview pine script implementation
        https://www.tradingview.com/pine-script-reference/#fun_strategy{dot}order
        :param id: Order id
        :param long: Long or Short
        :param qty: Quantity
        :param limit: Limit price
        :param stop: Stop limit
        :param post_only: Post only
        :param reduce only: Reduce Only means that your existing position cannot be increased only reduced by this order by this order
        :param allow_amend: Allow amening existing orders
        :param when: Do you want to execute the order or not - True for live trading
        :return:
        """
        self.__init_client()        
        
        if self.get_margin()['excessMargin'] <= 0 or qty <= 0:            
            return
        
        if not when:
            return
        
        side = "Buy" if long else "Sell"
        ord_qty = qty             

        if allow_amend:
            order = self.get_open_order(id)
            ord_id = id + ord_suffix() if order is None else order["clOrdID"]

            if order is None:
                self.__new_order(ord_id, side, ord_qty, limit, stop, post_only, reduce_only)
            else:
                self.__amend_order(ord_id, side, ord_qty, limit, stop, post_only)
        else:
            ord_id = id + ord_suffix()
            self.__new_order(ord_id, side, ord_qty, limit, stop, post_only, reduce_only)
        
        self.callbacks[ord_id] = callback  

    def get_open_order(self, id):
        """
        Get order
        :param id: order id
        :return:
        """
        self.__init_client()
        open_orders = retry(lambda: self.private_client
                            .Order.Order_getOrders(filter=json.dumps({"symbol": self.pair, "open": True}))
                            .result())
        open_orders = [o for o in open_orders if o["clOrdID"].startswith(id)]
        if len(open_orders) > 0:
            return open_orders[0]
        else:
            return None

    def exit(self, profit=0, loss=0, trail_offset=0):
        """
        profit taking and stop loss and trailing, if both stop loss and trailing offset are set trailing_offset takes precedence
        :param profit: Profit (specified in ticks)
        :param loss: Stop loss (specified in ticks)
        :param trail_offset: Trailing stop price (specified in ticks)
        """
        self.exit_order = {'profit': profit, 'loss': loss, 'trail_offset': trail_offset}

    def sltp(self, profit_long=0, profit_short=0, stop_long=0, stop_short=0, eval_tp_next_candle=False, round_decimals=2,
                 profit_long_callback=None, profit_short_callback=None, stop_long_callback=None, stop_short_callback=None):
        """
        Simple take profit and stop loss implementation, which sends a reduce only stop loss order upon entering a position.
        :param profit_long: profit target value in % for longs
        :param profit_short: profit target value in % for shorts
        :param stop_long: stop loss value for long position in %
        :param stop_short: stop loss value for short position in %
        :param round_decimals: round decimals 
        """
        self.sltp_values = {
                            'profit_long': profit_long/100,
                            'profit_short': profit_short/100,
                            'stop_long': stop_long/100,
                            'stop_short': stop_short/100,
                            'eval_tp_next_candle': eval_tp_next_candle,
                            'profit_long_callback': profit_long_callback,
                            'profit_short_callback': profit_short_callback,
                            'stop_long_callback': stop_long_callback,
                            'stop_short_callback': stop_short_callback
                            }        
        self.round_decimals = round_decimals

    def get_exit_order(self):
        """
        get profit take and stop loss and trailing settings
        """
        return self.exit_order

    def get_sltp_values(self):
        """
        get values for the simple profit target/stop loss in %
        """
        return self.sltp_values 

    def eval_exit(self):
        """
        evalution of profit target and stop loss and trailing
        """
        if self.get_position_size() == 0:
            return

        unrealised_pnl = self.get_position()['unrealisedPnl']

        # trail asset
        if self.get_exit_order()['trail_offset'] > 0 and self.get_trail_price() > 0:
            if self.get_position_size() > 0 and \
                    self.get_market_price() - self.get_exit_order()['trail_offset'] < self.get_trail_price():
                logger.info(f"Loss cut by trailing stop: {self.get_exit_order()['trail_offset']}")
                self.close_all(self.get_exit_order()['trail_callback'])
            elif self.get_position_size() < 0 and \
                    self.get_market_price() + self.get_exit_order()['trail_offset'] > self.get_trail_price():
                logger.info(f"Loss cut by trailing stop: {self.get_exit_order()['trail_offset']}")
                self.close_all(self.get_exit_order()['trail_callback'])

        # stop loss
        if unrealised_pnl < 0 and \
                0 < self.get_exit_order()['loss'] < abs(unrealised_pnl / 100000000):
            logger.info(f"Loss cut by stop loss: {self.get_exit_order()['loss']}")
            self.close_all(self.get_exit_order()['loss_callback'])

        # profit take
        if unrealised_pnl > 0 and \
                0 < self.get_exit_order()['profit'] < abs(unrealised_pnl / 100000000):
            logger.info(f"Take profit by stop profit: {self.get_exit_order()['profit']}")
            self.close_all(self.get_exit_order()['profit_callback'])
     
    def eval_sltp(self):
        """
        Simple take profit and stop loss implementation, which sends a reduce only stop loss order upon entering a position.
        - requires setting values with sltp() prior
        """
        pos_size = self.get_position_size()
        # sl_order = self.get_open_order('SL')
        # if pos_size == 0 and sl_order is not None:
        #     self.cancel(id=sl_order['clOrdID'])
        #     return
        if pos_size == 0:
            return
            
        # tp
        tp_order = self.get_open_order('TP')   
        
        is_tp_full_size = False 
        is_sl_full_size = False        

        if tp_order is not None:
            origQty = tp_order['orderQty']
            is_tp_full_size = origQty == abs(pos_size) if True else False
            #pos_size =  pos_size - origQty                 
        
        tp_percent_long = self.get_sltp_values()['profit_long']
        tp_percent_short = self.get_sltp_values()['profit_short']   

        avg_entry = self.get_position_avg_price()

        # tp execution logic                
        if tp_percent_long > 0 and is_tp_full_size == False:
            if pos_size > 0:                
                tp_price_long = round(avg_entry +(avg_entry*tp_percent_long), self.round_decimals) 
                if tp_order is not None:
                    #time.sleep(2)                    
                    self.__amend_order(tp_order['clOrdID'], False, abs(pos_size), limit=tp_price_long)
                else:               
                    self.order("TP", False, abs(pos_size), limit=tp_price_long, reduce_only=True, allow_amend=False, callback=self.get_sltp_values()['profit_long_callback'])
        if tp_percent_short > 0 and is_tp_full_size == False:
            if pos_size < 0:                
                tp_price_short = round(avg_entry -(avg_entry*tp_percent_short), self.round_decimals)
                if tp_order is not None: 
                     #time.sleep(2)                   
                    self.__amend_order(tp_order['clOrdID'], True, abs(pos_size), limit=tp_price_short)
                else:
                    self.order("TP", True, abs(pos_size), limit=tp_price_short, reduce_only=True, allow_amend=False, callback=self.get_sltp_values()['profit_short_callback'])
        #sl
        sl_order = self.get_open_order('SL')
        if sl_order is not None:
            origQty = sl_order['orderQty']
            orig_side = sl_order['side'] == "Buy" if True else False
            if orig_side == False:
                origQty = -origQty            
            is_sl_full_size = origQty == -pos_size if True else False           

        sl_percent_long = self.get_sltp_values()['stop_long']
        sl_percent_short = self.get_sltp_values()['stop_short']

        # sl execution logic
        if sl_percent_long > 0 and is_sl_full_size == False:
            if pos_size > 0:
                sl_price_long = round(avg_entry - (avg_entry*sl_percent_long), self.round_decimals)
                if sl_order is not None:                             
                    self.cancel(id=sl_order['clOrdID'])
                    time.sleep(2)
                    self.order("SL", False, abs(pos_size), stop=sl_price_long, reduce_only=True, allow_amend=False, callback=self.get_sltp_values()['stop_long_callback'])
                    #self.__amend_order(sl_order['clOrdID'], False, abs(pos_size), stop=sl_price_long)
                else:  
                    self.order("SL", False, abs(pos_size), stop=sl_price_long, reduce_only=True, allow_amend=False, callback=self.get_sltp_values()['stop_long_callback'])
        if sl_percent_short > 0 and is_sl_full_size == False:
            if pos_size < 0:
                sl_price_short = round(avg_entry + (avg_entry*sl_percent_short), self.round_decimals)
                if sl_order is not None:                                  
                    self.cancel(id=sl_order['clOrdID'])
                    time.sleep(2)
                    self.order("SL", True, abs(pos_size), stop=sl_price_short, reduce_only=True, allow_amend=False, callback=self.get_sltp_values()['stop_short_callback'])
                    #self.__amend_order(sl_order['clOrdID'], True, abs(pos_size), stop=sl_price_short)
                else:  
                    self.order("SL", True, abs(pos_size), stop=sl_price_short, reduce_only=True, allow_amend=False, callback=self.get_sltp_values()['stop_short_callback'])

    def fetch_ohlcv(self, bin_size, start_time, end_time):
        """
        fetch OHLCV data
        :param start_time: start time
        :param end_time: end time
        :return:
        """        
        self.__init_client()

        fetch_bin_size = allowed_range[bin_size][0]
        left_time = start_time
        right_time = end_time
        data = to_data_frame([])

        while True:
            source = retry(lambda: self.public_client.Trade.Trade_getBucketed(symbol=self.pair, binSize=fetch_bin_size,
                                                                              startTime=left_time, endTime=right_time,
                                                                              count=500, partial=False).result())
            if len(source) == 0:
                break
            logger.info(f"fetching OHLCV data - {left_time}") 
            source = to_data_frame(source)
            data = pd.concat([data, source])

            if right_time > source.iloc[-1].name + delta(fetch_bin_size):
                left_time = source.iloc[-1].name + delta(fetch_bin_size)
                time.sleep(2)
            else:
                break        
        return resample(data, bin_size)        

    def security(self, bin_size):
        """
        Recalculate and obtain different time frame data
        """        
        return resample(self.data, bin_size)[:-1]   
    
    def __update_ohlcv(self, action, new_data):
        """
        get and update OHLCV data and execute the strategy
        """           
        if self.timeframe_data is None:
            self.timeframe_data = {}
            for t in self.bin_size:
                bin_size = t
                end_time = datetime.now(timezone.utc)
                start_time = end_time - self.ohlcv_len * delta(bin_size)
                self.timeframe_data[bin_size] = self.fetch_ohlcv(bin_size, start_time, end_time)
                self.timeframe_info[bin_size] = {
                                                    "allowed_range": allowed_range_minute_granularity[t][0] if self.minute_granularity else allowed_range[t][0], 
                                                    "ohlcv": self.timeframe_data[t][:-1], # Dataframe with closed candles                                                   
                                                    "last_action_time": None,#self.timeframe_data[bin_size].iloc[-1].name, # Last strategy execution time
                                                    "last_candle": self.timeframe_data[bin_size].iloc[-2].values,  # Store last complete candle
                                                    "partial_candle": self.timeframe_data[bin_size].iloc[-1].values  # Store incomplete candle
                                                }
                # The last candle is an incomplete candle with timestamp in future                
                if self.timeframe_data[bin_size].iloc[-1].name > end_time:
                    last_candle = self.timeframe_data[t].iloc[-1].values # Store last candle
                    self.timeframe_data[bin_size] = self.timeframe_data[t][:-1] # Exclude last candle
                    self.timeframe_data[bin_size].loc[end_time.replace(microsecond=0)] = last_candle #set last candle to end_time
                #d1 = self.timeframe_data[bin_size]
                # if len(d1) > 0:
                #     d2 = self.fetch_ohlcv(allowed_range[bin_size][0],
                #                         d1.iloc[-1].name + delta(allowed_range[bin_size][0]), end_time)

                #     self.timeframe_data[bin_size] = pd.concat([d1, d2])               
                # else:
                #     self.timeframe_data[bin_size] = d1                

                logger.info(f"Initial Buffer Fill - Last Candle: {self.timeframe_data[bin_size].iloc[-1].name}")   
        #logger.info(f"{self.timeframe_data}") 

        timeframes_to_update = []

        for t in self.timeframe_info:            
            if self.timeframe_info[t]["allowed_range"] == action:
                # append minute count of a timeframe when sorting when sorting is need otherwise just add a string timeframe
                timeframes_to_update.append(allowed_range_minute_granularity[t][3]) if self.timeframes_sorted != None else timeframes_to_update.append(t)  

        # Sorting timeframes that will be updated
        if self.timeframes_sorted == True:
            timeframes_to_update.sort(reverse=True)
        if self.timeframes_sorted == False:
            timeframes_to_update.sort(reverse=False)

        logger.info(f"timefeames to update: {timeframes_to_update}")        

        for t in timeframes_to_update:
            # Find timeframe string based on its minute count value
            if self.timeframes_sorted != None:             
                t = find_timeframe_string(t)               
                    
            # replace latest candle if timestamp is same or append
            if self.timeframe_data[t].iloc[-1].name == new_data.iloc[0].name:
                self.timeframe_data[t] = pd.concat([self.timeframe_data[t][:-1], new_data])
            else:
                self.timeframe_data[t] = pd.concat([self.timeframe_data[t], new_data])      

            # exclude current candle data and store partial candle data
            re_sample_data = resample(self.timeframe_data[t], t, minute_granularity=True if self.minute_granularity else False)
            self.timeframe_info[t]['partial_candle'] = re_sample_data.iloc[-1].values # store partial candle data
            re_sample_data =re_sample_data[:-1] # exclude current candle data

            logger.info(f"{self.timeframe_info[t]['last_action_time']} : {self.timeframe_data[t].iloc[-1].name} : {re_sample_data.iloc[-1].name}")  

            if self.timeframe_info[t]["last_action_time"] is not None and \
                self.timeframe_info[t]["last_action_time"] == re_sample_data.iloc[-1].name:
                continue

            # The last candle in the buffer needs to be preserved 
            # while resetting the buffer as it may be incomlete
            # or contains latest data from WS
            self.timeframe_data[t] = pd.concat([re_sample_data.iloc[-1 * self.ohlcv_len:, :], self.timeframe_data[t].iloc[[-1]]]) 
            #store ohlcv dataframe to timeframe_info dictionary
            self.timeframe_info[t]["ohlcv"] = re_sample_data
            #logger.info(f"Buffer Right Edge: {self.data.iloc[-1]}")
            
            open = re_sample_data['open'].values
            close = re_sample_data['close'].values
            high = re_sample_data['high'].values
            low = re_sample_data['low'].values
            volume = re_sample_data['volume'].values 
                                        
            try:
                if self.strategy is not None:   
                    self.timestamp = re_sample_data.iloc[-1].name.isoformat()           
                    self.strategy(t, open, close, high, low, volume)              
                self.timeframe_info[t]['last_action_time'] = re_sample_data.iloc[-1].name
            except FatalError as e:
                # Fatal error
                logger.error(f"Fatal error. {e}")
                logger.error(traceback.format_exc())

                notify(f"Fatal error occurred. Stopping Bot. {e}")
                notify(traceback.format_exc())
                self.stop()
            except Exception as e:
                logger.error(f"An error occurred. {e}")
                logger.error(traceback.format_exc())    
        
    def __on_update_instrument(self, action, instrument):
        """
        Update instrument
        """
        if 'lastPrice' in instrument:
            self.market_price = instrument['lastPrice']

            # trail price update
            if self.get_position_size() > 0 and \
                    self.market_price > self.get_trail_price():
                self.set_trail_price(self.market_price)
            if self.get_position_size() < 0 and \
                    self.market_price < self.get_trail_price():
                self.set_trail_price(self.market_price)

    def __on_update_wallet(self, action, wallet):
        """
        update wallet
        """
        self.wallet = {**self.wallet, **wallet} if self.wallet is not None else self.wallet

    def __on_update_order(self, action, order):
        """
        Update order status        
        """
        self.order_update = order
        #logger.info(f"order: {order}")
        #logger.info(f"action:{ac tion}")

        #only after order if completely filled
        if order['leavesQty'] == 0: 
            logger.info(f"========= Order Update ==============")
            logger.info(f"ID     : {order['clOrdID']}") # Clinet Order ID
            logger.info(f"Pair   : {order['symbol']}") 
            logger.info(f"Type   : {order['ordType']}")
            #logger.info(f"Uses   : {order['wt']}")
            logger.info(f"Side   : {order['side']}")
            logger.info(f"Status : {order['ordStatus']}")
            logger.info(f"Qty    : {order['orderQty']}")
            logger.info(f"Leaves qty: {order['leavesQty']}")
            logger.info(f"Limit  : {order['price']}")
            logger.info(f"Stop   : {order['stopPx']}")
            logger.info(f"APrice : {order['avgPx']}")
            logger.info(f"======================================")

            # Call the respective order callback
            callback = self.callbacks.pop(order['clOrdID'], None)  # Removes the respective order callback and returns it
            if callback != None:
                callback()

        # Evaluation of profit and loss
        self.eval_exit()
        #self.eval_sltp()
        
    def __on_update_position(self, action, position):
        """
        Update position
        """
        # Was the position size changed?
        is_update_pos_size = self.get_position()['currentQty'] != position['currentQty']

        # Reset trail to current price if position size changes
        if is_update_pos_size and position['currentQty'] != 0:
            self.set_trail_price(self.market_price)

        if is_update_pos_size:            
            if 'avgEntryPrice' not in position:
                position.update( {'avgEntryPrice' : self.get_position()['avgEntryPrice']})
            logger.info(f"Updated Position\n"
                        f"Price: {self.get_position()['avgEntryPrice']} => {position['avgEntryPrice']}\n"
                        f"Qty: {self.get_position()['currentQty']} => {position['currentQty']}\n"
                        f"Balance: {self.get_balance()/100000000} XBT")
            notify(f"Updated Position\n"
                   f"Price: {self.get_position()['avgEntryPrice']} => {position['avgEntryPrice']}\n"
                   f"Qty: {self.get_position()['currentQty']} => {position['currentQty']}\n"
                   f"Balance: {self.get_balance()/100000000} XBT")

        self.position = {**self.position, **position} if self.position is not None else self.position

        # Evaluation of profit and loss
        self.eval_exit()
        self.eval_sltp()

    def __on_update_margin(self, action, margin):
        """
        Update margin
        """
        self.margin = {**self.margin, **margin} if self.margin is not None else self.margin

    def on_update(self, bin_size, strategy):
        """
        Register the strategy function
        bind functions with webosocket data streams        
        :param strategy: strategy
        """       
        logger.info(f"pair: {self.pair}")  
        logger.info(f"timeframes: {bin_size}")  
        self.bin_size = bin_size
        self.strategy = strategy       

        if self.is_running:
            self.ws = BitMexWs(account=self.account, pair=self.pair, test=self.demo)
            
            #if len(self.bin_size) > 1:   
                #self.minute_granularity=True  

            #if self.minute_granularity==True and '1m' not in self.bin_size:
                #self.bin_size.append('1m')      

            #self.ws.bind('1m' if self.minute_granularity else allowed_range[bin_size[0]][0] \
                        #, self.__update_ohlcv)     

            if len(self.bin_size) > 0: 
                for t in self.bin_size:                                        
                    self.ws.bind(allowed_range_minute_granularity[t][0] if self.minute_granularity else allowed_range[t][0] \
                        , self.__update_ohlcv)                              
            self.ws.bind('instrument', self.__on_update_instrument)
            self.ws.bind('wallet', self.__on_update_wallet)
            self.ws.bind('position', self.__on_update_position)
            self.ws.bind('order', self.__on_update_order)
            self.ws.bind('margin', self.__on_update_margin)
            self.ob = OrderBook(self.ws)
                        
    def stop(self):
        """
        Stop the crawler
        """
        if self.is_running:
            self.is_running = False
            self.ws.close()

    def show_result(self):
        """
        Show results
        """
        pass

    def plot(self, name, value, color, overlay=True):
        """
        Draw the graph
        """
        pass