Esempio n. 1
0
 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)
Esempio n. 2
0
 def on_update(self, bin_size, strategy):
     """
     Register the strategy function
     bind functions with webosocket data streams        
     :param strategy: strategy
     """       
     self.bin_size = bin_size
     self.strategy = strategy
     if self.is_running:
         self.ws = BitMexWs(account=self.account, pair=self.pair, 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)
Esempio n. 3
0
    def test_subscribe_margin(self):
        ws = BitMexWs()

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

        ws.bind('margin', subscribe)

        self.wait_complete()
        ws.close()
Esempio n. 4
0
    def test_subscribe_instrument(self):
        ws = BitMexWs()

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

        ws.bind('instrument', subscribe)

        self.wait_complete()
        ws.close()
Esempio n. 5
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()
Esempio n. 6
0
class BitMex:
    # Account
    account = ''
    # Pair
    pair = 'XBTUSD'
    # Wallet
    wallet = None
    # Price
    market_price = 0
    # Position
    position = None
    # Margin
    margin = None
    # Time Frame
    bin_size = '1h'
    # Client for private API
    private_client = None
    # Client for public API
    public_client = None
    # Is bot running
    is_running = True
    # Bar crawler
    crawler = None
    # Strategy
    strategy = None
    # Enable log output
    enable_trade_log = True
    # OHLCV length
    ohlcv_len = 100
    # OHLCV data
    data = None
    # Profit target long and short for a simple limit exit strategy
    sltp_values = {
        'profit_long': 0,
        'profit_short': 0,
        'stop_long': 0,
        'stop_short': 0
    }
    # Round decimals
    round_decimals = 0
    # Profit, Loss and Trail Offset
    exit_order = {'profit': 0, 'loss': 0, 'trail_offset': 0}
    # Trailing Stop
    trail_price = 0
    # Last strategy execution time
    last_action_time = None

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

    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['bitmext_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")

    def close_all(self):
        """
        Close all positions for this pair
        """
        self.__init_client()
        order = retry(lambda: self.private_client.Order.Order_closePosition(
            symbol=self.pair).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):
        """
        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']})")
        return True

    def __new_order(self,
                    ord_id,
                    side,
                    ord_qty,
                    limit=0,
                    stop=0,
                    post_only=False,
                    reduce_only=False):
        """
        create an order
        """
        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,
              when=True):
        """
        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

        if not long and pos_size < 0:
            return

        ord_qty = qty + abs(pos_size)

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

    def order(self,
              id,
              long,
              qty,
              limit=0,
              stop=0,
              post_only=False,
              reduce_only=False,
              allow_amend=True,
              when=True):
        """
        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)

    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,
             round_decimals=2):
        """
        simple profit target triggered 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
        }
        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()
            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()

        # 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()

        # 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()

    # simple TP implementation

    def eval_sltp(self):
        """
        evaluate simple profit target and stop loss
        """

        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 == 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)
        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)
        #sl
        sl_order = self.get_open_order('SL')
        if sl_order is not None:
            origQty = sl_order['orderQty']
            is_sl_full_size = origQty == pos_size if True else False
            #pos_size =  pos_size - origQty

        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)
                    #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)
        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)
                    #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)

    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=True).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):
        """
        Recalculate and obtain different time frame data
        """
        return resample(self.data, bin_size)[:-1]

    def __update_ohlcv(self, action, new_data):
        """
        get OHLCV data and execute the strategy
        """
        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])

        # exclude current candle 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:
            # 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())

            notify(f"An error occurred. {e}")
            notify(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_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
        """
        self.bin_size = bin_size
        self.strategy = strategy
        if self.is_running:
            self.ws = BitMexWs(account=self.account,
                               pair=self.pair,
                               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):
        """
        Stop the crawler
        """
        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
Esempio n. 7
0
class BitMex:
    # wallet
    wallet = None
    # 価格
    market_price = 0
    # ポジション
    position = None
    # マージン
    margin = None
    # 利用する時間足
    bin_size = '1h'
    # プライベートAPI用クライアント
    private_client = None
    # パブリックAPI用クライアント
    public_client = None
    # 稼働中
    is_running = True
    # 時間足を取得するクローラ
    crawler = None
    # 戦略を実施するリスナー
    strategy = None
    # ログの出力
    enable_trade_log = True
    # OHLCの長さ
    ohlcv_len = 100
    # OHLCのキャッシュ
    data = None
    # 利確損切戦略
    exit_order = {'profit': 0, 'loss': 0, 'trail_offset': 0}
    # Trailing Stopのためのピン留価格
    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:
                    notify(f"Order retry count exceed")
                    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"======================================")

            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):
        """
        注文を更新する
        """
        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,
              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())

            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())

            notify(f"An error occurred. {e}")
            notify(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")
            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

        # 利確損切の評価
        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
Esempio n. 8
0
            if action == "partial" or \
                    action == "insert":
                orders[ordId] = v
            elif action == "update" and ordId in orders:
                orders[ordId]["size"] = v['size']
            elif action == "delete" and ordId in orders:
                del orders[ordId]

        bid_prices = sorted([v['price'] for v in self.asks.values()])
        ask_prices = sorted([v['price'] for v in self.bids.values()])

        if len(ask_prices) > 0:
            self.ask_max_price = ask_prices[-1]
        if len(bid_prices) > 0:
            self.bid_min_price = bid_prices[0]
        if len(ask_prices) > 0:
            self.best_bid_price = bid_prices[-1]
        if len(ask_prices) > 0:
            self.best_ask_price = ask_prices[0]

    def get_prices(self):
        return self.best_bid_price, self.best_ask_price


if __name__ == '__main__':
    ws = BitMexWs(account=BitMexWs.account, pair=BitMexWs.pair)
    ob = OrderBook(ws)
    while True:
        sys.stdout.write(f"\r{ob.get_prices()}")
        sys.stdout.flush()
Esempio n. 9
0
 def test_setup(self):
     ws = BitMexWs()
     ws.close()
Esempio n. 10
0
class BitMex:
    # wallet
    wallet = None
    # 가격
    market_price = 0
    # 포지션
    position = None
    # 마진
    margin = None
    # 이용하는 봉시간 단위
    bin_size = '1h'
    # 개인API사용자
    private_client = None
    # 공개API사용자
    public_client = None
    # 기동중
    is_running = True
    # 봉, 거래량을 취득하는 크롤러
    crawler = None
    # 전략실시 리스너
    strategy = None
    # 로그출력
    enable_trade_log = True
    # OHLC길이
    # ohlcv_len = 100
    ohlcv_len = 1000
    # OHLC캐쉬
    data = None
    # 이익 확인 손절 구분자
    exit_order = {'profit': 0, 'loss': 0, 'trail_offset': 0}
    # Trailing Stop 가격 봉의 축가격
    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")
        # tobby2002
        # api_key = 'KQW_2f_brKDMjonpBTkBC8nK'
        # api_secret = 'NQ2mXkIWNVClJddk0t3ZdO1jV9Ihq39ISV5DLT1pwcU1ZGpt'
        # redlee80
        api_key = 'NPo11uetPveJeDUMcMW19B_x'
        api_secret = 'pcbKMRlLxH_fS3oCyEeDkFNhp1UGmyu8CpxLbwEokOvpd2Ud'

        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()
        if margin and position:
            return math.floor(
                (1 - self.get_retain_rate()) * self.get_market_price() *
                margin['excessMargin'] /
                (position['initMarginReq'] * 100000000))
        else:
            # logger.info("Error---> There is no margin or no position.")  # 거래내역이 없으면 생기는것 같다.
            return 0

    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()
        try:
            currentqty = self.get_position()['currentQty']
        except Exception as e:  # 에러 종류
            logger.error(e)
            logger.error(traceback.format_exc())
        return currentqty

    def get_whichpositon(self):
        """
        현재 포지션 유무체크
        :return:
        """
        self.__init_client()
        if self.get_position()['currentQty'] > 1:
            return 'LONG'
        elif self.get_position()['currentQty'] < -1:
            return 'SHORT'
        elif self.get_position()['currentQty'] == 0:
            return None

    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):
        """
        주문 작성
        """

        try:
            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:
                        notify(f"Order retry count exceed")
                        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())

        except Exception as e:
            logger.error('Exception: __new_order : %s' % e)

        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):
        """
        주문 갱신
        """
        try:
            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())
        except Exception as e:
            logger.error('Exception: __amend_order : %s' % e)

        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,
              when=True):
        """
        주문 엔트리 함수. pine언어와 동등
        https://kr.tradingview.com/study-script-reference/#fun_strategy{dot}entry
        :param id: 주문번호
        :param long: 롱 or 숏
        :param qty: 주문수량
        :param limit: 제시가
        :param stop: 스탑제시가
        :param post_only: 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://kr.tradingview.com/study-script-reference/#fun_strategy{dot}order
        :param id: 주문번호
        :param long: 롱 or 숏
        :param qty: 주문수량
        :param limit: 제시가
        :param stop: 스탑제시가
        :param post_only: 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())
        # print('open_orders:%s' % open_orders)

        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: 이익 (Tick설정)
        :param loss: 손익 (Tick설정)
        :param trail_offset: Trail Stop 가격 (Tick설정)
        """
        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], sort=True)

            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], sort=True)
            else:
                self.data = d1
        else:
            self.data = pd.concat([self.data, new_data], sort=True)

        # 마지막행은 불학정정보이기에 베제한다. (original)
        # re_sample_data = resample(self.data, self.bin_size)[:-1]

        # 마지막행도 반영한다. (by neo)
        re_sample_data = resample(self.data, self.bin_size)[:]

        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())

            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())

            notify(f"An error occurred. {e}")
            notify(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']

        # 포지션 사이즈가 변경된 경우, Trail 개시가격을 현재의 가격에 리셋한다.
        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")
            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

        # 익손절의 평가
        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
Esempio n. 11
0
        for v in values:
            ordId = v['id']
            side = v['side']
            orders = self.asks if side == "Buy" else self.bids
            if action == "partial" or \
                    action == "insert":
                orders[ordId] = v
            elif action == "update" and ordId in orders:
                orders[ordId]["size"] = v['size']
            elif action == "delete" and ordId in orders:
                del orders[ordId]

        ask_prices = sorted([v['price'] for v in self.asks.values()])
        bid_prices = sorted([v['price'] for v in self.bids.values()])

        if len(ask_prices) > 0:
            self.ask_max_price = ask_prices[-1]
        if len(bid_prices) > 0:
            self.bid_min_price = bid_prices[0]

    def get_prices(self):
        return self.bid_min_price, self.ask_max_price

if __name__ == '__main__':
    ws = BitMexWs()
    ob = OrderBook(ws)
    while True:
        sys.stdout.write(f"\r{ob.get_prices()}")
        sys.stdout.flush()
Esempio n. 12
0
 def test_setup(self):
     ws = BitMexWs(account=self.account, pair=self.pair)
     ws.close()