def __init__(self, bot_id=None, bot_token=None, chat_id=None, message_handler={}, expire_time=60, expired_handler=None): if bot_token is None: logger.debug("Telegram Bot Disabled.") self.enabled = True return if bot_id is None or bot_id == "": self.bot_id = str( random.randint(random.randint(1, 99), random.randint(100, 9999))) else: self.bot_id = bot_id logger.debug("Telegram Bot ID. %s", self.bot_id) self.bot_token = bot_token self._bot = Bot(bot_token) self.chat_id = chat_id self.message_handler = message_handler if expired_handler is not None: self.question_tmp = expiredict(expired_handler) else: self.question_tmp = expiredict( expire_time=expire_time, expired_callback=self.expired_question) self.enabled = False
def load(self): # 미리 history 크기의 df 를 만들고. # history 만큼 캔들 데이터를 채워넣는다. # 초기화. 데이터는 비어있음. # 최신것부터 limit 개를 가져온다. # new_data = await new_data = self.api.fetch_ohlcv(symbol=self.symbol, timeframe=self.period, since=self.since, limit=self.history) # 오래된순 정렬로 바꿈. # new_data = new_data[::-1] index_list = [] data_list = [] for row in new_data: logger.debug('>>> row>>> %s', row) index = row[0] index_list.append(index) data_list.append({ 'Open': row[1], 'High': row[2], 'Low': row[3], 'Close': row[4], 'Volume': row[5], }) freq = utils.period_to_freq(self.period) self.candle = BarList(self.symbol, self.history, freq) self.candle.init(index_list, data_list)
def put_order(self, order_qty, price=None, stop_price=None, post_only=True, close_position=True, type='Limit'): order_logger.info( 'NEW ORDER > symbol[%s] type[%s] order_qty[%s] price[%s] stop_price[%s]', self.symbol, type, order_qty, price, stop_price) if self.dry_run: return try: if type == 'Limit': result = self.client.Order.Order_new( symbol=self.symbol, ordType=type, orderQty=order_qty, price=price, side='Buy' if order_qty > 0 else 'Sell', execInst='ParticipateDoNotInitiate' if post_only else '').result() elif type == 'Stop': if close_position: result = self.client.Order.Order_new( symbol=self.symbol, ordType=type, stopPx=stop_price, execInst='Close,LastPrice', side='Buy' if order_qty > 0 else 'Sell').result() else: result = self.client.Order.Order_new( symbol=self.symbol, ordType=type, orderQty=order_qty, stopPx=stop_price).result() elif type == 'LimitIfTouched' or type == "StopLimit": result = self.client.Order.Order_new( symbol=self.symbol, ordType=type, orderQty=order_qty, price=price, stopPx=stop_price, execInst='ParticipateDoNotInitiate').result() elif type == 'Market': result = self.client.Order.Order_new(symbol=self.symbol, ordType=type, orderQty=order_qty, price=price).result() logger.debug('NEW ORDER > symbol[%s] Result > %s', self.symbol, result[0]) order = result[0] status = order['ordStatus'] if status != 'Rejected': return order except Exception as e: logger.error('put order error %s', e)
def init(self, index_list, data_list): logger.debug('>> index_list >> %s', index_list) # dates = pd.DatetimeIndex(index_list, dtype='datetime64[ns]', freq=self.freq) self.df = pd.DataFrame( data=data_list, index=index_list, columns=['Open', 'High', 'Low', 'Close', 'Volume'])
def update_position(self, position): logger.debug('update_position > %s', position) self.s['current_qty'] = position['currentQty'] current_qty = self.s['current_qty'] if current_qty == 0: # 포지션이 닫힌 경우. if 'isOpen' in position and not position['isOpen']: prevPnl = position['prevRealisedPnl'] logger.info('Position Closed. pnl[%s%s]', prevPnl, position['currency']) self.send_telegram('Position Closed. pnl[{} {}]'.format(prevPnl, position['currency'])) # stop모두 취소. for order in self.data['order']: if order['ordType'] == 'Stop': order_id = order['orderID'] self.api.cancel_order(id=order_id, symbol=symbol) return if 'avgEntryPrice' in position: if position['avgEntryPrice']: ## entry_price가 대부분 안들어온다. 포지션이 수정될때만 들어옴. 봇을 리스타트시 안들어옴. self.s['entry_price'] = position['avgEntryPrice'] if current_qty > 0: # 롱 포지션은 아래쪽에 Sell을 예약한다. stop_price = round(self.s['entry_price'] - self.stoploss) found = False for order in self.data['order']: # limit 주문만 수정. stop은 손절주문이므로 수정하지 않으며, update_position에서 수정함. if order['ordType'] == 'Stop' and order['side'] == 'Sell': if order['stopPx'] != stop_price: order_id = order['orderID'] self.api.edit_order(id=order_id, symbol=symbol, type='Stop', side='Buy', amount=abs(current_qty), params={'execInst': 'Close,LastPrice', 'stopPx': stop_price}) found = True break if not found: self.api.create_order(symbol, type='Stop', side='Sell', amount=abs(current_qty), params={'execInst': 'Close,LastPrice', 'stopPx': stop_price}) elif current_qty < 0: # 숏 포지션은 위쪽에 Buy를 예약한다. stop_price = round(self.s['entry_price'] + self.stoploss) found = False for order in self.data['order']: # limit 주문만 수정. stop은 손절주문이므로 수정하지 않으며, update_position에서 수정함. if order['ordType'] == 'Stop' and order['side'] == 'Buy': if order['stopPx'] != stop_price: order_id = order['orderID'] self.api.edit_order(id=order_id, symbol=symbol, type='Stop', side='Sell', amount=abs(current_qty), params={'execInst': 'Close,LastPrice', 'stopPx': stop_price}) found = True break if not found: self.api.create_order(symbol, type='Stop', side='Buy', amount=abs(current_qty), params={'execInst': 'Close,LastPrice', 'stopPx': stop_price})
def update_order(self, order): logger.debug('update_order > %s', order) # if 'side' in order: # self.send_telegram( # '{} {} {}XBT @{}'.format(order['ordType'], order['side'], order['orderQty'], order['price'])) if 'ordStatus' in order: if order['ordStatus'] == 'Filled': self.send_telegram('Order filled {} {}@{}'.format(order['symbol'], order['cumQty'], order['avgPx']))
def update_leverage(self, leverage): # update leverage self.leverage = leverage if not self.dry_run: result = self.client.Position.Position_updateLeverage( symbol=self.symbol, leverage=leverage).result() logger.debug('UPDATE LEVERAGE symbol[%s] leverage[%s] Result > %s', self.symbol, self.leverage, result[0])
def test_bitmex_data_ingest(self): exchange = utils.ccxt_exchange('bitmex', is_async=True) markets = asyncio.get_event_loop().run_until_complete( exchange.load_markets()) #exchange, symbols, start_date, end_date, periods base_dir, filepath = asyncio.get_event_loop().run_until_complete( data_ingest.ingest_data(exchange, 'XBT/USD', datetime(2019, 1, 1, 0, 0, 0), datetime(2019, 1, 1, 12, 0, 0), '5m', 12)) logger.debug('filepath > %s', filepath)
def amend_order(self, order, order_qty, price=None, stop_price=None, type='Limit'): """ 기존 주문 수정. :param key: :param order_qty: :param price: :return: """ if not self.dry_run: try: order_id = order['orderID'] if type == 'Limit': result = self.client.Order.Order_amend( orderID=order_id, orderQty=order_qty, price=price).result() elif type == 'Stop': result = self.client.Order.Order_amend( orderID=order_id, orderQty=order_qty, stopPx=stop_price).result() elif type == 'LimitIfTouched' or type == "StopLimit": result = self.client.Order.Order_amend( orderID=order_id, orderQty=order_qty, price=price, stopPx=stop_price).result() elif type == 'Market': result = self.client.Order.Order_new(symbol=self.symbol, ordType=type, orderQty=order_qty, price=price).result() logger.debug( 'AMEND ORDER > order_id[%s] symbol[%s] Result > %s', order_id, self.symbol, result[0]) order = result[0] status = order['ordStatus'] order_logger.info( 'AMEND ORDER (%s) > order_id[%s] symbol[%s] type[%s] order_qty[%s] price[%s] stop_price[%s]', status, order_id, self.symbol, type, order_qty, price, stop_price) if status != 'Rejected': return order except Exception as e: if 'Invalid ordStatus' in e.response.text: raise OrderNotFoundException() logger.error('amend order error %s', e)
def handle_topic(topic, action, data): print(topic, action, data) if topic == 'margin': if action == 'partial': self.margin = data elif action == 'update': self.margin.update(data) used_margin = self.margin['maintMargin'] avail_margin = self.margin['availableMargin'] logger.debug( f'[Margin] used_margin[{used_margin}], avail_margin[{avail_margin}]' ) elif topic == 'order': # for order in data: if action == 'partial' or action == 'insert': if data: order_id = data['orderID'] self.orders[order_id] = data else: if data and 'orderID' in data: order_id = data['orderID'] working_indicator = data['workingIndicator'] if working_indicator: self.orders[order_id].update(data) else: del self.orders[order_id] print(self.orders) print('--------------------') elif topic == 'position': if action == 'partial': self.position = data else: self.position.update(data) # 필수. currentQty = self.position['currentQty'] currency = self.position['currency'] markPrice = self.position['markPrice'] timestamp = self.position['timestamp'] liquidationPrice = self.position['liquidationPrice'] logger.info('%s %s current_qty[%s]', topic, action, currentQty)
async def run(): logger.debug('ws > %s', ws) await ws.connect() """ "affiliate", // Affiliate status, such as total referred users & payout % "execution", // Individual executions; can be multiple per order "order", // Live updates on your orders "margin", // Updates on your current account balance and margin requirements "position", // Updates on your positions "privateNotifications", // Individual notifications - currently not used "transact" // Deposit/Withdrawal updates "wallet" // Bitcoin address balance data, including total deposits & withdrawals """ # req = {'op': 'subscribe', 'args': ['tradeBin1m:XBTUSD', 'order', 'margin', 'position', 'wallet']} # req = {'op': 'subscribe', 'args': ['margin', 'position:XBTUSD', 'quote:XBTUSD', 'orderBook10:XBTUSD']} # req = {'op': 'subscribe', 'args': ['order:XBTUSD', 'position:XBTUSD', 'wallet']} req = {'op': 'subscribe', 'args': ['orderBook10:XBTUSD']} ws.update_orderbook = lambda x: print(x) await ws.listen()
def start(self): new_data = self.api.bulk_klines(symbol=self.symbol, timeframe=self.period, since=self.since) index_list = [] data_list = [] for row in new_data: logger.debug('>>> row>>> %s', row) index = pd.to_datetime(row[0], unit='ms') index_list.append(index) data_list.append({ 'open': float(row[1]), 'high': float(row[2]), 'low': float(row[3]), 'close': float(row[4]), 'volume': float(row[5]), }) freq = utils.period_to_freq(self.period) self.candle = BarList(self.symbol, self.history, freq) self.candle.init(index_list, data_list) # websocket self.api.start_websocket(self.symbol, self.period, self.__update)
def update_position(self, position): logger.info('update_position > %s', position) try: price = position["avgEntryPrice"] self.avg_entry_price = position["avgEntryPrice"] except KeyError: logger.debug("시정 평균가가 변동이 없습니다.") price = self.avg_entry_price current_qty = 0 if position["currentQty"] is None else position["currentQty"] * -1 if position['currentQty'] > 0: self.position = "BOT" price = price + self.limit_tick elif position['currentQty'] < 0: self.position = "SLD" price = price - self.limit_tick else: self.position = "NTL" logger.info(f"규모: {current_qty}, 시장평균가: {price}") if position["currentQty"] != 0: # 포지션을 가지고 있을 경우 if self.limit_order is None: # 익절 주문. self.limit_order = self.nexus.api.put_order(order_qty=current_qty, price=price, stop_price=price, type="LimitIfTouched") elif self.limit_order is not None \ and self.is_order_request \ and self.limit_order["orderQty"] != current_qty: # 기존 주문 가격, 규모 수정. self.limit_order = self.nexus.api.amend_order(order=self.limit_order, order_qty=current_qty, price=price, stop_price=price, type="LimitIfTouched") self.is_order_request = False else: # 포지션이 없을때. pass if self.count % 2 == 0: return try: last_price = position["lastPrice"] except KeyError: return order_list = self.nexus.api.get_order() cancel_orders = [] remove_orders = [] for pare_order in self.pare_orders: high_price = abs(pare_order["price"]) + (0.5 * 10) low_price = abs(pare_order["price"]) - (0.5 * 10) # 가격 범위가 넘은 경우 if last_price > high_price or low_price > last_price: cancel_orders.append(pare_order) # 대기 중이 아닌경우 is_deplay = False for order in order_list: if pare_order["limit_order"]["orderID"] == order["orderID"]: is_deplay = True break if not is_deplay: remove_orders.append(pare_order) # 취소처리 for cancel_order in cancel_orders: remove_orders.append(cancel_order) try: self.nexus.api.cancel_order(cancel_order["limit_order"]) except ExchangeError: pass try: self.nexus.api.cancel_order(cancel_order["stop_limit_order"]) except ExchangeError: pass # 저장된 데이터 제거 for remove_order in remove_orders: for pare_order in self.pare_orders: if remove_order["limit_order"]["orderID"] == pare_order["limit_order"]["orderID"]: self.pare_orders.remove(pare_order) break
def ingest_data(api, symbol, start_date, end_date, interval, history=0, reload=False): """ 데이터를 받아서 csv파일로 저장한다. 만약 파일이 존재한다면 스킵한다. :return: 해당 파일이 존재하는 디렉토리 경로. """ dir = tempfile.gettempdir() timer_start = timeit.default_timer() tz = start_date.tzinfo # 값, 단위 분리 interval_num, interval_unit = split_interval(interval) interval = interval.lower() interval_unit = interval_unit.lower() base_dir, filepath = ingest_filepath(dir, api, symbol, start_date, end_date, interval, history) logger.debug('file_path=%s', filepath) # 강제 리로드 요청이 없고, 파일이 존재하고, 사이즈도 0보다 크면, 그냥 사용. if not reload and os.path.exists( filepath) and os.path.getsize(filepath) > 0: logger.debug('# [{}] CandleFile Download Passed. {}'.format( symbol, filepath)) return base_dir, filepath prepare_date = get_prepare_date(start_date, interval, history) delta = (end_date - prepare_date) # 나누어 받을때 다음번 루프의 시작시점이 된다. next_delta = 0 if interval_unit == 'm': length = (delta.days * 24 * 60 + delta.seconds // 60) // interval_num next_delta = timedelta(minutes=interval_num).total_seconds() elif interval_unit == 'h': length = (delta.days * 24 + delta.seconds // 3600) // interval_num next_delta = timedelta(hours=interval_num).total_seconds() elif interval_unit == 'd': length = (delta.days) // interval_num next_delta = timedelta(days=interval_num).total_seconds() since = int(prepare_date.timestamp()) * 1000 logger.info('Ingest time range: %s ~ %s', prepare_date, end_date) logger.info('Ingest candle length[%s] of [%s] since[%s]', length, interval_unit, since) params = {} if api.id == 'bitmex': max_limit = 750 # params = { 'partial': 'true' } elif api.id == 'binance': max_limit = 1000 with open(filepath, 'w', encoding='utf-8', newline='') as f: wr = csv.writer(f) wr.writerow( ['Datetime', 'Index', 'Open', 'High', 'Low', 'Close', 'Volume']) seq = 0 while length > 0: if seq > 0: time.sleep(api.rateLimit / 1000) # time.sleep wants seconds limit = min(length, max_limit) logger.debug('#### fetch_ohlcv request [%s / %s]', limit, length) # candles = await candles = fetch_ohlcv(api, symbol, interval, since, limit, params) # 읽은 갯수만큼 빼준다. length -= limit logger.debug('#### fetch_ohlcv remnant [%s]', length) logger.debug('#### fetch_ohlcv response [%s] >> \n%s', len(candles), candles) if len(candles) == 0: logger.warn('candle data rows are empty!') break last_candle = None for candle in candles: wr.writerow([ datetime.fromtimestamp( int(candle[0] / 1000), tz=tz).strftime('%Y-%m-%d %H:%M:%S'), int(candle[0]), '{:.8f}'.format(candle[1]), '{:.8f}'.format(candle[2]), '{:.8f}'.format(candle[3]), '{:.8f}'.format(candle[4]), '{:.2f}'.format(candle[5]), ]) last_candle = candle # 다음번 루프는 마지막 캔들부터 이어서 내려받는다. logger.debug('>>> last_candle >>> %s', last_candle) since = last_candle[0] + next_delta seq += 1 timer_end = timeit.default_timer() logger.debug('# {} Downloaded CandleFile. elapsed: {}'.format( symbol, str(timer_end - timer_start))) return base_dir, filepath
def update_candle(self, df, candle): logger.info(f"시장가격: {df.Close[-1]}") if self.count < 5: self.count += 1 return self.count = 0 diff_sum = sum(df.Close - df.Open) add_qty = 0 logger.info(f"포지션: {self.position}, 차이가격: {diff_sum}") if diff_sum > 0: if self.position == "SLD": # SLD 포지션이면 정리. for pare_order in self.pare_orders: try: self.nexus.api.cancel_order(pare_order["limit_order"]) except ExchangeError: logger.debug("<ignore> 없어진 요청.") try: self.nexus.api.cancel_order(pare_order["stop_limit_order"]) except ExchangeError: logger.debug("<ignore> 없어진 요청.") add_qty = self.limit_order["orderQty"] self.limit_order = None self.pare_orders = [] # 매수 order = self.nexus.api.put_order(order_qty=(self.order_qty + add_qty), # side="Buy", price=df.Close[-1] - 0.5 ) if order["ordStatus"] == "New": stop_limit_order = self.nexus.api.put_order(order_qty=-(self.order_qty + add_qty), # side="Sell", price=df.Close[-1] - 0.5 - self.stop_limit_tick, stop_price=df.Close[-1] - 0.5 - self.stop_limit_tick, type="StopLimit") self.pare_orders.append({ "price": df.Close[-1], "position": "BOT", "limit_order": order, "stop_limit_order": stop_limit_order }) self.is_order_request = True elif diff_sum < 0: if self.position == "BOT": # BOT 포지션이면 정리. for pare_order in self.pare_orders: try: self.nexus.api.cancel_order(pare_order["limit_order"]) except ExchangeError: logger.debug("<ignore> 없어진 요청.") try: self.nexus.api.cancel_order(pare_order["stop_limit_order"]) except ExchangeError: logger.debug("<ignore> 없어진 요청.") add_qty = self.limit_order["orderQty"] self.limit_order = None self.pare_orders = [] # 매도 order = self.nexus.api.put_order(order_qty=-(self.order_qty + add_qty), # side="Sell", price=df.Close[-1] + 0.5) if order["ordStatus"] == "New": stop_limit_order = self.nexus.api.put_order(order_qty=(self.order_qty + add_qty), # side="Buy", price=df.Close[-1] + 0.5 + self.stop_limit_tick, stop_price=df.Close[-1] + 0.5 + self.stop_limit_tick, type="StopLimit") self.pare_orders.append({ "price": df.Close[-1], "position": "SLD", "limit_order": order, "stop_limit_order": stop_limit_order }) self.is_order_request = True
def distribute(self, update_type, message_id, text, choice=None): try: logger.debug("==============================================") logger.debug("새로운 메시지가 도착했습니다.") logger.debug(f"[{update_type}] message_id: {message_id}, choice: {choice}") logger.debug(f"{text}") logger.debug("==============================================") if update_type == "message": # TODO 메시지로 도착했을때 명령어 기능 추가하면 좋을듯.. logger.debug("[미개발..] %s", text) elif update_type == "callback_query": # 요청한 봇이 맞는지 확인한다. bot_id = text.split("\n")[0].split(":")[1].strip() if self.bot_id != bot_id: logger.debug("다른 봇 메시지는 무시합니다.") return # 임시저장된 내용 가져온수 삭제처리. self.question_tmp.lock() question = self.question_tmp.get(message_id) del self.question_tmp[message_id] self.question_tmp.unlock() # 버튼을 지운다. self._bot.edit_message_text(chat_id=question["message"]["chat_id"], message_id=question["message"]["message_id"], text=f"{question['message']['text']}", parse_mode="HTML") # 선택에 따라서 함수 호출한다. result = None if choice == "YES": logger.debug("selected: YES") if question['yes_func'] is not None: result = question['yes_func'](question['yes_param']) else: logger.debug("selected: No") if question['no_func'] is not None: result = question['no_func'](question['no_param']) # 결과를 전송한다. if result is None: result = "No Data" self._bot.edit_message_text(chat_id=question["message"]["chat_id"], message_id=question["message"]["message_id"], text=f"{question['message']['text']}\n결과 >> {result}", parse_mode="HTML") except KeyError: # ignore 요청의 두번 답을 말할경우. pass
def distribute(self, update_type, message_id, text, choice=None): try: logger.debug("==============================================") logger.debug("새로운 메시지가 도착했습니다.") logger.debug(f"[{update_type}] message_id: {message_id}, choice: {choice}") logger.debug(f"{text}") logger.debug("==============================================") if update_type == "message": # TODO 메시지로 도착했을때 명령어 기능 추가하면 좋을듯.. logger.debug("[미개발..] %s", text) elif update_type == "callback_query": bot_id = text.split("\n")[0].split(":")[1].strip() if self.bot_id != bot_id: logger.debug("다른 봇 메시지는 무시합니다.") return question = self.question_tmp.get(message_id) del self.question_tmp[message_id] if choice == "YES": logger.debug("selected: YES") if question['yes_func'] is not None: result = question['yes_func'](question['yes_param']) else: logger.debug("selected: No") if question['no_func'] is not None: result = question['no_func'](question['no_param']) self.bot.edit_message_text(chat_id=question["message"]["chat_id"], message_id=question["message"]["message_id"], text=f"{question['message']['text']}\n결과 >> {result}", parse_mode="HTML") except KeyError: logger.error(KeyError)
def cancel_order(self, order): order_id = order['orderID'] if not self.dry_run: result = self.client.Order.Order_cancel(orderID=order_id).result() logger.debug('CANCEL ORDER > order_id[%s] symbol[%s] Result > %s', order_id, self.symbol, result[0])
def __on_message(self, message): '''Handler for parsing WS messages.''' message = json.loads(message) logger.debug(json.dumps(message)) table = message['table'] if 'table' in message else None action = message['action'] if 'action' in message else None try: if 'subscribe' in message: logger.debug("Subscribed to %s." % message['subscribe']) elif action: if table not in self.data: self.data[table] = [] # There are four possible actions from the WS: # 'partial' - full table image # 'insert' - new row # 'update' - update row # 'delete' - delete row value = message['data'] if action == 'partial': logger.debug("%s: partial" % table) self.data[table] += value # Keys are communicated on partials to let you know how to uniquely identify # an item. We use it for updates. self.keys[table] = message['keys'] elif action == 'insert': logger.debug('%s: inserting %s' % (table, value)) self.data[table] += value # Limit the max length of the table to avoid excessive memory usage. # Don't trim orders because we'll lose valuable state if we do. if table not in [ 'order', 'orderBookL2' ] and len(self.data[table]) > BitmexWS.MAX_TABLE_LEN: self.data[table] = self.data[table][int(BitmexWS. MAX_TABLE_LEN / 2):] elif action == 'update': logger.debug('%s: updating %s' % (table, value)) # Locate the item in the collection and update it. for updateData in value: item = findItemByKeys(self.keys[table], self.data[table], updateData) if not item: return # No item found to update. Could happen before push item.update(updateData) # Remove cancelled / filled orders if table == 'order' and item['leavesQty'] <= 0: self.data[table].remove(item) elif action == 'delete': logger.debug('%s: deleting %s' % (table, value)) # Locate the item in the collection and remove it. for deleteData in value: item = findItemByKeys(self.keys[table], self.data[table], deleteData) self.data[table].remove(item) else: raise Exception("Unknown action: %s" % action) # 업데이트 콜백을 호출한다. for item in value: if self.is_ready: if table.startswith('tradeBin'): if self.update_candle and item: self.update_candle(item) elif table == 'orderBook10': if self.update_orderbook and item: self.update_orderbook(item) elif table == 'order': if self.update_order and item: self.update_order(item) if table == 'position': if self.update_position and item: self.update_position(item) except: logger.error(traceback.format_exc())