Example #1
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))
class Bot:
    DEFAULT_USD_SIZE = config.getfloat('DEFAULT_USD_SIZE')
    SPECIFIC_NAMES = config['SPECIFIC_NAMES']
    SPECIFIC_USD_SIZE = config.getfloat('SPECIFIC_USD_SIZE')

    def __init__(self, api_key, api_secret):
        self.ftx = FTX("",
                       api_key=api_key,
                       api_secret=api_secret,
                       subaccount=SUBACCOUNT)
        self.cg = CoinGeckoAPI()
        self.logger = setup_logger("log/listed_and_long.log")
        self.prev_markets: List[Dict[str, Union[str, float]]] = []
        self.positions = []
        self.HODL_TIME = config.getfloat('HODL_TIME')

        self.logger.info(
            f"BOT:{BOT_NAME} ENV:{PYTHON_ENV} SUBACCOUNT:{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(5)
                await asyncio.sleep(0)
            except KeyError as e:
                push_message("KeyError: {}".format(e))
                self.logger.error('An exception occurred', str(e))
            except Exception as e:
                push_message(str(e))
                self.logger.error('An unhandled exception occurred' + str(e))
                exit(1)

    async def main(self, interval):
        # main処理
        listed = []
        new_listed = []

        self.ftx.market()
        response = await self.ftx.send()
        # print(json.dumps(response[0]['result'], indent=2, sort_keys=False))
        # 引数に与えた条件に当てはまる上場銘柄をリストに抽出する
        listed = self.extract_markets(markets=response[0]['result'],
                                      market_type=["spot", "future"],
                                      future_type='-PERP',
                                      exclude=[
                                          'HEDGE', 'BULL', 'BEAR', 'HALF',
                                          'BVOL', '-0326', "MOVE-"
                                      ])
        VERBOSE and self.logger.debug(listed)

        # 前回の上場銘柄リストがあるならば,現在の上場リストと比較して新規上場銘柄があるか調べる
        if len(self.prev_markets) > 0:
            # 条件に合格した新規上場銘柄を抽出する
            new_listed, _ = self.extract_new_listed(self.prev_markets, listed,
                                                    RANK)
            if len(new_listed) > 0:
                self.logger.info(f'上場銘柄差分:{_}')
                self.logger.info(f'合格した新規上場銘柄:{new_listed}')
            try:
                for new in new_listed:
                    # SNS通知
                    msg = f"New Listing: {new['name']}"
                    self.logger.info(msg)
                    push_message(msg)
                    # トレードを許可しているならば,エントリー
                    if TRADABLE:
                        ord_type = 'market'
                        market = new['name']
                        price = float(new['bid'])
                        key = 'underlying' if new[
                            'type'] == 'future' else 'baseCurrency'
                        usd = self.SPECIFIC_USD_SIZE if str(new[key]).upper(
                        ) in self.SPECIFIC_NAMES else self.DEFAULT_USD_SIZE
                        size = usd / (float(new['bid']) +
                                      float(new['ask'])) / 2
                        if PYTHON_ENV != 'production':
                            price = 1000
                            market = 'ETH-PERP'
                            size = 0.001
                            ord_type = 'limit'
                        responce, _ = await self.entry(market, size, ord_type,
                                                       'buy', price)
                        self.positions.append({
                            'orderTime':
                            time.time(),
                            'market':
                            market,
                            'size':
                            size,
                            'side':
                            'buy',
                            'price':
                            responce[0]['result']['price']
                        })
                        await self.entry(market, size, 'limit', 'sell',
                                         price * 1.08)
            except Exception as e:
                self.logger.error(str(e))
        # ---------共通の処理----------
        # 最新の上場のリストを更新
        self.prev_markets = listed
        listed = []
        self.logger.debug("Snapshot markets...")
        if len(self.positions) > 0:
            self.logger.info("Current positions :>>")
            self.logger.info(self.positions)
            await self.settle(market_type=["future"])
        await asyncio.sleep(interval)

    def extract_markets(
            self,
            markets,
            market_type=["spot", "future"],
            future_type='-PERP',
            exclude=["HEDGE", "BULL", "BEAR", "HALF", "BVOL", "MOVE",
                     "-0326"]):
        satsfied = []
        has_spot = "spot" in market_type
        has_future = "future" in market_type
        for market in markets:
            if market["enabled"]:
                if has_spot and market['type'] == "spot" and market[
                        "quoteCurrency"] == 'USD':
                    is_excluded = True
                    for keyword in exclude:
                        is_excluded = is_excluded and keyword not in market[
                            "name"]
                    if is_excluded:
                        satsfied.append(market)
                if has_future and market[
                        "type"] == 'future' and future_type in market["name"]:
                    satsfied.append(market)
        return satsfied

    def extract_new_listed(self, prev_markets, current_markets, rank):
        """
        リスティングの差分をとり,coingeckoに上場していてかつ,coingeckoでの時価総額ランキングが`rank`以上のマーケットのリストを返す
        ただし,`SPECIFIC_NAMES`に一致する名前を持つ場合はcoingeckoに上場していなくても含まれる
        """
        specifics = []
        diff = self.extract_listing_diff(prev_markets, current_markets)
        for new in diff:
            key = 'underlying' if new['type'] == 'future' else 'baseCurrency'
            if str(new[key]).upper() in self.SPECIFIC_NAMES:
                specifics.append(new.copy())
        markets = self.fileter_by_market_cap(diff, rank)
        return markets + specifics, diff

    def extract_listing_diff(
        self, prev_markets: List[Dict[str, Union[str, float]]],
        current_markets: List[Dict[str, Union[str, float]]]
    ) -> List[Dict[str, Union[str, float]]]:
        """
        引数に与えられた二つのリスティング情報の差分をとる.
        """
        new_listed = []
        if len(current_markets) == 0:
            return new_listed
        prev_market_names = [
            prev_market["name"] for prev_market in prev_markets
        ]
        for current_market in current_markets:
            if current_market["name"] not in prev_market_names:
                new_listed.append(current_market)
        return new_listed

    def fileter_by_market_cap(self,
                              markets: List[Dict[str, Union[str, float]]],
                              rank=500):
        """
        marketにcg_idが存在し,時価総額が`rank`以上のマーケットをフィルターし返す.
        """
        if (markets is None) or len(markets) == 0:
            return markets
        coins = self.cg.get_coins_list(
        )  # List all supported coins id, name and symbol
        markets = self.append_cg_id(markets, coins)
        for market in markets:
            try:
                # `cg_id`が空文字でないならcoingeckoに上場している.そうでないなら,リストから落とす
                if 'cg_id' in market and len(str(market['cg_id'])) > 0:
                    # coingeckoでmarket情報を取得し,時価総額が条件を満たさないならリストから落とす
                    print("market['cg_id'] :>>", market['cg_id'])
                    result = self.cg.get_coins_markets(
                        ids=market['cg_id'],
                        vs_currency='usd',
                        category='coin category')
                    if len(result) > 0:
                        market_cap_rank = result[0]['market_cap_rank']
                        if isinstance(market_cap_rank, int):
                            if market_cap_rank == 0 or market_cap_rank > rank:
                                markets.remove(market)
                    elif 'error' in result:
                        raise Exception('COIN_GECKO_API ERROR', market, result)
                else:
                    self.logger.info(
                        f'DOES NOT FOUND {market["name"]} coin gecko')
                    markets.remove(market)
            except Exception as e:
                self.logger.error(f'COIN_GECKO_API ERROR {str(e)}')
        return markets

    def append_cg_id(self, markets: List[Dict[str, Union[str, float]]],
                     cg_coins):
        """
        引数に与えられたmarketsにcoingeckoのidの要素`cg_id`を追加する.
        coingeckoに対応するidが見つからないときは空文字とする
        """
        try:
            for market in markets:
                symbol = ''
                if market['type'] == "spot":
                    symbol = str(market['baseCurrency']).lower()
                elif market['type'] == 'future':
                    symbol = str(market['underlying']).lower()
                for coin in cg_coins:
                    if symbol == coin['symbol']:
                        market['cg_id'] = coin['id']
                        break
                    else:
                        market['cg_id'] = ''
            return markets
        except Exception as e:
            self.logger.error(str(e))

    async def entry(self,
                    market,
                    size,
                    ord_type,
                    side,
                    price="",
                    postOnly=False,
                    reduceOnly=False):
        try:
            self.ftx.place_order(ord_type=ord_type,
                                 market=market,
                                 side=side,
                                 price=price,
                                 size=size,
                                 postOnly=postOnly,
                                 reduceOnly=reduceOnly)
            response = await self.ftx.send()
            if response[0]['success']:
                msg = f"BOT:{BOT_NAME}\nOrdered\n{market}\nSIDE:{side}\nSIZE:{size}"
                self.logger.info(msg)
                push_message(msg)
                return response, True
            else:
                raise Exception(response[0])
        except Exception as e:
            msg = f"BOT:{BOT_NAME}\nERROR: {str(e)}"
            self.logger.error(msg)
            push_message(msg)
            return {}, False

    async def settle(self, market_type=["future"]):
        has_future = "future" in market_type
        for pos in self.positions:
            if has_future and ("/USD" not in pos["market"]):
                try:
                    if time.time() - pos["orderTime"] >= self.HODL_TIME:
                        price = ""
                        ord_type = 'market'
                        _, success = await self.entry(market=pos["market"],
                                                      size=pos["size"],
                                                      ord_type=ord_type,
                                                      price=price,
                                                      side='sell',
                                                      postOnly=False,
                                                      reduceOnly=True)
                        if success:
                            self.positions.remove(pos)
                except KeyError as e:
                    self.logger.error("KeyError" + str(e))
                except Exception as e:
                    self.logger.error("Exception: " + str(e))
Example #3
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
Example #4
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)