Esempio n. 1
0
class Bot:
    FRONTRUN_USD_SIZE = float(config['FRONTRUN_USD_SIZE'])
    CATCH_MISS_PRICE_USD_SIZE = float(config['CATCH_MISS_PRICE_USD_SIZE'])

    def __init__(self, api_key, api_secret):
        self.ftx = FTX(
            market=MARKET,
            api_key=api_key,
            api_secret=api_secret,
            subaccount=SUBACCOUNT)
        self.order_ids = []
        print(f"ENV:{PYTHON_ENV}\nBOT:{BOT_NAME}\nSUBACCOUNT:{SUBACCOUNT}")
        # タスクの設定およびイベントループの開始
        loop = asyncio.get_event_loop()
        tasks = [self.run()]
        loop.run_until_complete(asyncio.wait(tasks))

    # ---------------------------------------- #
    # bot main
    # ---------------------------------------- #
    async def run(self):
        while True:
            try:
                await self.main(15)
                await asyncio.sleep(0)
            except Exception as e:
                print('An exception occurred', e)
                push_message(
                    f"ERROR\nBOT:{BOT_NAME}\nSUBACCOUNT:{SUBACCOUNT}")
                exit(1)

    async def main(self, interval):
        """
            - positionを取ってくる
            - futureからデータとる
        if hour ==0 and minが01-03であるとき:
            if 01 and 十分な価格変化:
                成り行き
                catch_miss_price
            if 03 あたり:
                01での指値を全キャンセル
        else
            if positionがあるなら,決済
            if 閾値に達したなら....
        """
        position = {}
        self.ftx.positions()
        response = await self.ftx.send()
        for pos in response[0]["result"]:
            if pos["future"] == MARKET:
                position = pos
                break
        print("\nPOSITION :>>")
        pprint(position)

        self.ftx.future()
        response = await self.ftx.send()
        print("\nfuture :>>")
        pprint(response[0]["result"])

        utc_date = datetime.now(timezone.utc)
        hour = utc_date.hour
        min = utc_date.minute
        if hour == 0 and min >= 1 and min <= 3:
            if abs(position["size"]):
                return
            perps = self.extract_change_bod(
                [response[0]["result"]], floor_bod=FLOOR_CHANGE_BOD)
            pprint(perps)
            perp = perps[0]
            if min == 1:
                # positionを持っているならば,
                # positionを持っていなくて,bodが基準値以上ならば
                # bod>0ならば,perpを買い増しリバランスなのでtakerで順方向エントリー
                side = 'buy' if perp["changeBod"] > 0 else 'sell'
                size = self.FRONTRUN_USD_SIZE / float(perp["bid"])
                await self.taker_frontrun(MARKET, side, size)

                await asyncio.sleep(10)
                # miss priceを狙って逆張りの指値
                change = DISTANCE_CHANGE * sign(perp["changeBod"])
                price = float(perp["bid"]) * (1.0 + change)
                inverse_side = 'buy' if perp["changeBod"] < 0 else 'sell'
                size = self.CATCH_MISS_PRICE_USD_SIZE / float(perp["bid"])
                response, success = await self.maker_frontrun(MARKET, price, inverse_side, size)
                if success:
                    self.order_ids.append(response[0]["result"]["id"])
                await asyncio.sleep(15)
            if min == 3:
                # 指値注文全キャンセル
                for id in self.order_ids:
                    self.ftx.cancel_order(id)
                    response = await self.ftx.send()
                    await asyncio.sleep(1)
                    if response[0]["status"] == 200:
                        self.order_ids.remove(id)
        else:
            # if hour minが01-03ではないとき
            # positionがあるなら決済
            if position != {} and abs(position["size"]) > 0:
                await self.taker_settle(MARKET, position["size"])
            else:
                # if リバランスの閾値に達しているなら...
                perps = self.extract_change_bod(
                    [response[0]["result"]], grater_than=0.06, smaller_than=-0.11)
                if len(perps) > 0:
                    perp = perps[0]
                    pass
            await asyncio.sleep(interval)

    def extract_markets(self, markets, market_type=["spot", "future", "move", "perpetual"], exclude_keywords=[
            "/HEDGE", "/BULL", "/BEAR", "/HALF", "BVOL"]) -> List[Dict[str, Union[str, float]]]:
        """引数で指定した条件を満たしたマーケットを返す関数

        FTX REST APIのfutures/marketsレスポンスを満たす型を受け取り,引数で指定した条件を満たしたマーケットを返す

        Args:
            markets ( List[Dict[str, Union[str, float]]] ): FTX REST APIのfuturesとmarketsのレスポンスの型
            market_type (List[str], optional) : 結果に含めるマーケットのタイプ
            exclude_keywords (List[str], optional) : マーケット名に含まれるとき結果から除外するキーワード

        Returns:
            [List[Dict[str, Union[str,float]]]]: [market_typeを満たし,exclude_keywordsが銘柄名に部分文字列として含まれるものを除外したmarkets]
        """
        satsfied = []
        has_spot = "spot" in market_type
        has_future = "future" in market_type
        has_move = "move" in market_type
        has_perpetual = "perpetual" in market_type
        for market in markets:
            if market["enabled"]:
                for keyword in exclude_keywords:
                    if keyword in market["name"]:
                        continue
                if has_spot and market['type'] == "spot" and market["quoteCurrency"] == 'USD':
                    satsfied.append(market)
                if has_future and market["type"] == 'future':
                    satsfied.append(market)
                if has_move and market['type'] == "move":
                    satsfied.append(market)
                if has_perpetual and market["type"] == 'perpetual':
                    satsfied.append(market)
        return satsfied

    def extract_change_bod(
            self,
            markets,
            floor_bod=0.0,
            grater_than=0.0,
            smaller_than=0.0):
        satsfied = []
        if floor_bod > 0:
            for market in markets:
                if floor_bod <= abs(market["changeBod"]):
                    satsfied.append(market)
            return satsfied
        if grater_than > 0 and smaller_than < 0:
            for market in markets:
                if grater_than <= market["changeBod"]:
                    satsfied.append(market)
                if smaller_than >= market["changeBod"]:
                    satsfied.append(market)
        return satsfied

    async def _entry(self, market, side, price, size, ord_type='limit', postOnly=False):
        if PYTHON_ENV == 'production':
            self.ftx.place_order(
                type=ord_type,
                market=market,
                side=side,
                price=price,
                size=size,
                postOnly=postOnly)
        else:
            self.ftx.place_order(
                type=ord_type,
                market=market,
                side=side,
                price=price,
                size=size,
                postOnly=True)
        response = await self.ftx.send()
        print(response[0])
        msg = f"ENV: {PYTHON_ENV}\nBOT:{BOT_NAME}\nOrdered\nMARKET: {market}\nSIZE: {size}\nSIDE: {side}"
        push_message(msg)
        return response, True

    async def taker_frontrun(self, market, side, size):
        return await self._entry(market, side, '', size, 'market', False)

    async def maker_frontrun(self, market, price, side, size):
        return await self._entry(market, side, price, size, 'limit', True)

    async def taker_settle(self, market, size):
        side = 'buy' if size < 0 else 'sell'
        size = abs(size)
        self.ftx.place_order(market, side, 'market', size)
        response = await self.ftx.send()
        print(response[0]["result"])
        return True
Esempio n. 2
0
class BotBase:
    def __init__(self, _market, market_type, api_key, api_secret, subaccount):
        self.ftx = FTX(
            market=_market,
            api_key=api_key,
            api_secret=api_secret,
            subaccount=subaccount)
        self.logger = setup_logger(f'log/{BOT_NAME.lower()}.log')
        self.BOT_NAME: str = BOT_NAME
        self.MAX_ORDER_NUMBER: int = MAX_ORDER_NUMBER
        self.SUBACCOUNT = subaccount
        self.MARKET: str = _market
        self.MARKET_TYPE: str = market_type
        self.MAX_POSITION_SIZE = MAX_POSITION_SIZE
        self.position: Dict[str, Any] = {}
        self.open_orders: List[Dict[str, Any]] = []
        self.error_tracking = {'count': 0, 'error_message': '', 'timestamp': 0}
        self.next_update_time = time.time()

        self.logger.info(f'ENV:{PYTHON_ENV} {self.SUBACCOUNT} {self.BOT_NAME} {self.MARKET}')
        # タスクの設定およびイベントループの開始
        # loop = asyncio.get_event_loop()
        # tasks = [self.run()]
        # loop.run_until_complete(asyncio.wait(tasks))

    # ---------------------------------------- #
    # bot main
    # ---------------------------------------- #
    async def run(self, interval):
        while True:
            try:
                await self.main(interval)
                await asyncio.sleep(0)
            except Exception as e:
                self.logger.error(f'RUN_CYCLE_ERROR: {str(e)}')
                self.push_message(str(e))

    async def get_single_market(self):
        try:
            self.ftx.single_market()
            res = await self.ftx.send()
            if res[0]['success']:
                return res[0]['result'], True
            else:
                raise APIRequestError(res[0]['error'])
        except Exception as e:
            self.logger.error(str(e))
            return {}, False

    async def get_markets(self):
        try:
            self.ftx.market()
            res = await self.ftx.send()
            if res[0]['success']:
                return res[0]['result'], True
            else:
                raise APIRequestError(res[0]['error'])
        except Exception as e:
            self.logger.error(str(e))
            return {}, False

    async def get_open_orders(self):
        try:
            self.ftx.open_orders()
            res = await self.ftx.send()
            if res[0]['success']:
                return res[0]['result'], True
            else:
                raise APIRequestError(res[0]['error'])
        except Exception as e:
            self.push_message(str(e))
            self.logger.error(str(e))
            return {}, False

    async def require_num_open_orders_within(self, max_order_number):
        """
            オープンオーダーが与えられたオーダー数なら,`CycleError`エラーを投げる
        """
        open_orders, success = await self.get_open_orders()
        if success:
            if len(open_orders) >= max_order_number:
                msg = f'TOO_MANY_OPEN_ORDERS: {len(open_orders)}'
                self.logger.warn(msg)
                self.push_message(msg)
                raise CycleError(msg)

    def isvalid_reduce_only(self, size):
        reduce_only_size = 0.0
        pos = self.position
        if not self.has_position():
            return False
        for op_ord in self.open_orders:
            if op_ord['reduceOnly']:
                reduce_only_size += op_ord['size']
        if reduce_only_size + size >= pos['size']:
            self.logger.warn('Invalid ResuceOnly order')
            self.push_message('Invalid ResuceOnly order')
            return False
        else:
            return True

    def isvalid_size(self, size):
        return ('size' in self.position) and (self.MAX_POSITION_SIZE >= self.position['size'])

    async def place_order(self,
                          side,
                          ord_type,
                          size,
                          price='',
                          ioc=False,
                          reduceOnly=False,
                          postOnly=False,
                          sec_to_expire=SEC_TO_EXPIRE,
                          delay=5):
        """ place_order
        新規オーダーを置く.オーダーの成功・失敗を通知する
        レスポンスのオーダー情報とリクエストの可否のタプルを返す.
        """
        try:
            # if self.isvalid_size(size):
            #     return {}, False
            if reduceOnly:
                if not self.isvalid_reduce_only(size):
                    return {}, False
            if not sec_to_expire:
                sec_to_expire = SEC_TO_EXPIRE
            self.ftx.place_order(
                market=self.MARKET,
                side=side,
                ord_type=ord_type,
                size=size,
                price=price,
                ioc=ioc,
                reduceOnly=reduceOnly,
                postOnly=postOnly)
            res = await self.ftx.send()
            if res[0]['success']:
                data = res[0]['result']
                new_order = {}
                if data['status'] != 'cancelled':
                    new_order = {
                        'orderId': data['id'],
                        'side': data['side'],
                        'type': data['type'],
                        'size': data['size'],
                        'price': data['price'],
                        'status': data['status'],
                        'orderTime': time.time(),
                        'expireTime': time.time() + float(sec_to_expire),
                        'cancelTime': None,
                        'excutedSize': data['filledSize'],
                    }
                    self.open_orders.append(new_order)
                self.logger.info(self._message(new_order, 'new'))
                if PUSH_NOTIF:
                    self.push_message(data)
                await asyncio.sleep(delay)
                return data, True
            else:
                raise APIRequestError(res[0]['error'])
        except Exception as e:
            self.logger.error(str(e))
            self.push_message(str(e))
            return {}, False

    async def cancel_expired_orders(self, delay=1):
        """ 期限切れの全てのオーダーをキャンセルする.
            ステータスがopenまたはnewではない時に限る.
        """
        self.logger.debug('[Cycle] Cancel expired orders...')
        for order in self.open_orders:
            if (order['status'] in ['new', 'open']) and float(
                    order['expireTime']) < time.time() and order['cancelTime'] is None:
                _, success = await self.cancel_order(order)
                try:
                    if not success:
                        raise OrderCycleError(order, 'cancel')
                        # self.logger.error('CANCEL_EXPIRED_ORDERS: cancel_order failed')
                except Exception as e:
                    self.logger.error(str(e))
                await asyncio.sleep(delay)

    async def update_orders_status(self, delay=1):
        """
            オープンオーダーリストの`status`がopenまたはnewのオーダーのステータスをリクエストして更新する.
            約定済み,キャンセルになったオーダーはリストから削除し,ポジションを自炊更新する.
        """
        self.logger.debug('[Cycle] Updating orders status...')
        for order in self.open_orders:
            try:
                if order['status'] in ['open', 'new']:
                    self.ftx.order_status(order['orderId'])
                    res = await self.ftx.send()
                    if res[0]['success']:
                        if 'result' in res[0]:
                            data = res[0]['result']
                            self._update_per_order(data, order)
                        else:
                            self.logger.warn(f'key `result` not in {res[0]}')
                    else:
                        self.logger.error(res[0])
                        raise APIRequestError(res[0]['error'])
                    await asyncio.sleep(delay)
            except Exception as e:
                self.logger.error(f'[Cycle] UPDATE_ORDERS_STATUS_ERROR {str(e)}')

    def _update_per_order(self, data, order):
        """
            `data`で`order`の情報を更新する,および
            `order`の情報で現在ポジションとオープンオーダーのリストを更新する.
        """
        try:
            if isinstance(data, Dict):
                if 'status' in data and 'filledSize' in data:
                    order['status'] = data['status']
                    order['excutedSize'] = data['filledSize']
                    if order['status'] == 'closed':
                        self.logger.debug(self._message(order, 'update'))
                else:
                    self.logger.warn(f'`No valid key in data`:{data}')
            else:
                self.logger.warn(f'Expected type [Dict] `data`:{data}')
            # new
            if order['status'] == 'new':  # FTXではcancelledかfilledはclosedとして表わされる.
                pass
            # open
            if order['status'] == 'open':
                pass
            # cancelled
            if order['cancelTime'] is not None:  # orderがキューに入ってstatusが更新されていないときcancelledとみなす
                order['status'] = 'cancelled'
            if order['status'] == 'cancelled':
                pass
            # filled or cancelled
            if order['status'] == 'closed' and order['cancelTime'] is not None:  # cancelされた注文はcancelTimeが数値になる
                pass
            if order['status'] == 'closed' and order['cancelTime'] is None:
                self._update_position_by(order)
            if order['status'] == 'filled' and order['cancelTime'] is None:
                self._update_position_by(order)
            return self._update_open_order_by(order)
        except Exception as e:
            self.logger.error(f'_update_open_order_status {str(e)}')
            raise

    async def cancel_order(self, order):
        """ オーダーをキャンセルをリクエストする
            キャンセルリクエストのタイムスタンプをオーダーに追加する.および,オープンオーダーのリストから削除する.
            リクエストが失敗した時のみ,通知する
        """
        try:
            self.ftx.cancel_order(order['orderId'])
            res = await self.ftx.send()
            if res[0]['success']:
                data = res[0]['result']
                self.logger.info(self._message(data, 'cancel'))
                order['cancelTime'] = time.time()
                self._update_per_order(data, order)
                return data, True
            else:
                raise APIRequestError(f'ERROR:{res[0]["error"]}:orderId {order["orderId"]}')
        except Exception as e:
            self.logger.error(str(e))
            self.push_message(str(e))
            return {}, False

    def _update_open_order_by(self, order):
        """ `order`でオープンオーダーリストを更新する
            ステータスがキャンセルまたは,約定済みならばリストから削除する
        """
        try:
            # cancelled
            if order['status'] == 'cancelled':
                self.open_orders.remove(order)
            # cancelled
            elif order['status'] == 'closed' and order['cancelTime'] is not None:
                self.open_orders.remove(order)
            # filled
            elif order['status'] == 'closed' and order['cancelTime'] is None:
                self.open_orders.remove(order)
            elif order['status'] == 'filled' and order['cancelTime'] is None:
                self.open_orders.remove(order)
            elif order['status'] == 'open' or order['status'] == 'new':
                pass
            else:
                self.logger.warn(f'Unexpected Order status{order["status"]}')
        except Exception as e:
            self.logger.error(str(e))
            raise OrderCycleError(order, 'update')
            # raise Exception(f'UPDATE_OPEN_ORDER_BY_STATUS {str(e)}')

    def remove_not_open_orders(self):
        """ オープンオーダーリストのオーダーでステータスがキャンセルのものを全て削除する
        """
        self.logger.debug('[Cycle] Removec canceled orders...')
        for order in self.open_orders:
            self._update_open_order_by(order)

    def _update_position_by(self, order):
        """ `order`でポジションを自炊更新する
        """
        try:
            net_excuted = order['excutedSize'] if order['side'] == 'buy' else - order['excutedSize']
            self.position['size'] += abs(net_excuted)  # sizeは絶対値
            self.position['netSize'] += net_excuted
            self.position['side'] = 'buy' if float(self.position['size']) > 0 else 'sell'
        except KeyError as e:
            raise KeyError('KeyError', order)
        except Exception as e:
            self.logger.error(str(e))
            raise PositionCycleError(order, 'update')

    async def get_position(self, market=None):
        if market is None:
            market = self.MARKET

        self.ftx.positions()
        res = await self.ftx.send()
        try:
            if res[0]['success']:
                data = res[0]['result']
                for pos in data:
                    key = 'future' if 'future' in pos else 'name'
                    if market in pos[key]:
                        return pos, True
                else:
                    raise Exception('GET_POSITION', data)
            else:
                raise APIRequestError(res[0]['error'])
        except Exception as e:
            self.logger.error(str(e))
            return {}, False

    async def sync_position(self, delay=0):
        """
            ポジションをリクエストして最新の状態に同期させる.
        """
        try:
            self.logger.debug('[Cycle] Sync position...')
            pos, success = await self.get_position()
            await asyncio.sleep(delay)
            if success:
                self.position = pos
            else:
                raise PositionCycleError('SYNC_POSITION_ERROR', '')
        except PositionCycleError as e:
            self.logger.error(str(e))

    def log_status(self):
        if VERBOSE:
            self.logger.debug(f'self.position :>> {self.position["netSize"]}')
            self.logger.debug(f'self.open_orders lenth :>> {len(self.open_orders)}')

    def has_position(self):
        return self.position != {} and self.position['size'] > 0

    def _message(self, data='', msg_type=''):
        return _message(data, msg_type)

    def push_message(self, data, msg_type=''):
        """ ボットの基本情報+引数のデータ型に応じたテキストを追加して送信する.
             - `data`がstrなら,そのまま送信
             - `position`なら,sizeとsideを送信
             - `order`ならpriceとtype,sideを送信
        """
        bot_info = f'{self.SUBACCOUNT}:{self.BOT_NAME}\n{self.MARKET}'
        text = self._message(data, msg_type)
        push_message(f'{bot_info}\n{text}')

    def _update(self, interval):
        if time.time() > self.next_update_time:
            self.next_update_time += interval
            return True
        else:
            return False

    async def main(self, interval):
        try:
            if self._update(60):
                await self.require_num_open_orders_within(self.MAX_ORDER_NUMBER)

            await self.update_orders_status(delay=2)

            await asyncio.sleep(5)

            await self.cancel_expired_orders(delay=2)
            self.remove_not_open_orders()
            if self.MARKET_TYPE.lower() == 'future':
                await self.sync_position(delay=5)
            elif self.MARKET_TYPE.lower() == 'spot':
                pass
            self.log_status()
            await asyncio.sleep(interval)
        except CycleError as e:
            self.ftx.cancel_all_orders()
            res = await self.ftx.send()
            print("res[0] :>>", res[0])
            if res[0]['success'] and 'result' in res[0]:
                msg = '[Cycle] CANCEL_ALL_ORDERS'
                self.logger.info(msg)
                self.push_message(msg)
            else:
                raise APIRequestError(res[0]['error'])
        except Exception as e:
            self.logger.error(str(e))
            self.push_message(str(e))
Esempio n. 3
0
class Bot:
    # ---------------------------------------- #
    # init
    # ---------------------------------------- #
    def __init__(self, api_key, api_secret):
        self.ftx = FTX(MARKET,
                       api_key=api_key,
                       api_secret=api_secret,
                       subaccount=SUBACCOUNT)

        print("BOT_NAME: %s \nENV:%s \nMARKET %s \nSUBACCOUNT: %s" %
              (BOT_NAME, PYTHON_ENV, MARKET, SUBACCOUNT))
        # タスクの設定およびイベントループの開始
        loop = asyncio.get_event_loop()
        tasks = [self.run()]

        loop.run_until_complete(asyncio.wait(tasks))

    # ---------------------------------------- #
    # bot main
    # ---------------------------------------- #
    async def run(self):
        while True:
            await self.main(5)
            await asyncio.sleep(0)

    def create_time_fields(self, sec=10):
        utc_date = datetime.now(timezone.utc)
        utc_date = utc_date.replace(second=(utc_date.second - sec) % 60)
        if PYTHON_ENV != 'production':
            utc_date = utc_date.replace(day=(utc_date.day - 3) % 60)
        start_time_fields = "start_time=" + \
            utc_date.strftime("%Y-%m-%dT%H:%M:%SZ")
        return start_time_fields

    async def main(self, interval):
        # main処理
        """
        # account情報を取得
        self.ftx.account()
        response = await self.ftx.send()
        print(response[0])
        """

        self.ftx.positions()
        response = await self.ftx.send()
        # print(json.dumps(response[0], indent=2, sort_keys=False))
        position = {}
        for pos in response[0]["result"]:
            if pos["future"] == MARKET:
                position = pos
        print("position :>>", position)

        await asyncio.sleep(5)

        if position["size"] > float(MAX_SIZE):
            return

        query = "query=from:elonmusk -is:retweet"
        tweet_fields = "tweet.fields=author_id"
        start_time_fields = self.create_time_fields(sec=10)
        queries = [query, tweet_fields, start_time_fields]
        keywords = ['doge', 'Doge', 'DOGE']
        result = recent_research(keywords, queries)

        if len(result) > 0:
            push_message(f"Detect events:\nkeywords:{keywords}\n{result}")
            if PYTHON_ENV == 'production':
                self.ftx.place_order(type='market',
                                     side='buy',
                                     price='',
                                     size=180,
                                     postOnly=False)
            else:
                self.ftx.place_order(type='limit',
                                     side='buy',
                                     price=1111,
                                     size=0.001,
                                     postOnly=True)
            response = await self.ftx.send()
            print(response[0])
            orderId = response[0]['result']['id']
            push_message(f"Ordered :\norderId:{orderId}")
        await asyncio.sleep(interval)