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