class Bitmex(Trader): def __init__(self, myname): self.whatsmyname = myname self.logger = logging.getLogger(self.whatsmyname) self.logger.setLevel(logging.DEBUG) logging.basicConfig( format= '%(asctime)s.%(msecs)03d|%(levelname)-8s|%(name)-10s| %(message)s', level=logging.INFO, datefmt='%M:%S') # class vars self.data = {} self.keys = {} self.partials = [] self.ord_keep = None self.book = None self.max_len = None self.agent = None self.init_margin_balance = None self.deadman_renew = None self.client = None self.rem_limit = None # ws specific self.subs = None self.symbol_list = None self.endpoint = None self.api_key = None self.secret = None def boot(self, conf=0, boot_client=True, deadman_renew=6, subs=None, symbol_list=None): """ boot sequence and any specific params :param conf: config value :param boot_client: bool :param deadman_renew: int :param subs: list :param symbol_list: list """ # using sentinels for defaults instead of mutables (these create memory errors that spawn deep copy issues) if subs is None: subs = ['orderBookL2_25', 'order', 'trade', 'margin'] if symbol_list is None: symbol_list = ['BCHZ18'] # testnet if conf == "test": # max self.endpoint = connect['test']['endpoint'] self.api_key = connect['test']['apiKey'] self.secret = connect['test']['apiSecret'] test = True elif conf == 'prod': # trade@test self.endpoint = connect['prod']['endpoint'] self.api_key = connect['prod']['apiKey'] self.secret = connect['prod']['apiSecret'] test = False self.warn("!!!!!!! YOU ARE RUNNING IN PROD !!!!!!!") else: raise Exception("conf must be of test or prod (str)") self.deadman_renew = deadman_renew # every {} * 5 (poll intervalls) seconds # subs self.subs = subs self.symbol_list = symbol_list # data repo if boot_client: self.client = BitmexInterface(apiKey=self.api_key, apiSecret=self.secret, endpoint=self.endpoint, min_remain=30, test=test) else: self.client = None # TODO MED mount these externally, either via passing to boot or via object assignment self.ord_keep = OrderKeep(bulk_put=self.client.put_orders, bulk_amend=self.client.amend_orders, bulk_cancel=self.client.cancel_orders, bulk_query=self.client.query_orders) self.book = OrderBook( self.symbol_list[0], update_callback=self.ord_keep.size_update ) # TODO MED: this only gets one symbol. For actual trading, we need more # I need to patch the method after instantiation :-/ self.ord_keep.get_id_lvl = self.book.get_id self.max_len = 5000 # trading stuff self.agent = MM(symbol_list=self.symbol_list, bbo=self.book.bbo, get_inventory=self.get_inventory, get_inv_avg_price=self.inv_avg_price, execute=self.ord_keep.execute_target, margin_report=self.margin_report, request_trades=self.recent_trades, order_report=self.ord_keep.exec_report) ########### # Utilities# ########### def debug(self, msg): """ simple wrapper to debug :param msg: :return: """ self.logger.debug(str(msg)) def warn(self, msg): """ simple wrapper to warning :param msg: :return: """ self.logger.warning(str(msg)) def info(self, msg): """ simple wrapper to warning :param msg: :return: """ self.logger.info(str(msg)) def critical(self, msg): """ simple wrapper to warning :param msg: :return: """ self.logger.critical(str(msg)) def _prep_connection(self): ws = WebSocket(self.endpoint, compress=True) apikey = self.api_key secret = self.secret nonce = generate_nonce() ws.add_header(str.encode("api-nonce"), str.encode(str(nonce))) ws.add_header( str.encode("api-signature"), str.encode( str( generate_signature_bitmex(secret, 'GET', '/realtime', nonce, '')))) ws.add_header(str.encode("api-key"), str.encode(str(apikey))) return ws def message_data_updater(self, message, received_time): '''Handler for parsing WS messages. message is json parsed data and ks are dicts (to be onboarded in the class I need a different dict to make sure I don ''' # message = json.loads(message) # self.logger.debug(json.dumps(message)) table = message['table'] if 'table' in message.keys() else None action = message['action'] if 'action' in message.keys() else None try: # if 'subscribe' in message: # self.logger.debug("Subscribed to %s." % message['subscribe']) if 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 if action == 'partial': # print(message) # self.logger.debug('\n %s: partial %s \n' % (table, message['data'])) self.data[table] += message['data'] self.keys[table] = message['keys'] self.partials.append(table) if table == 'margin' and 'marginBalance' in message[ 'data'][0].keys(): if not self.init_margin_balance: self.init_margin_balance = message['data'][0][ 'marginBalance'] elif action == 'insert' and table in self.partials: # print(message) # self.logger.debug('\n %s: inserting %s \n' % (table, message['data'])) self.data[table] += message['data'] # 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]) > self.max_len: self.data[table] = self.data[table][int(self.max_len / 2):] elif action == 'update' and table in self.partials: # print(message) # self.logger.debug('\n %s: updating %s \n' % (table, message['data'])) # Locate the item in the collection and update it. for updateData in message['data']: item = findItemByKeys(self.keys[table], self.data[table], updateData) item.update(updateData) elif action == 'delete' and table in self.partials: # TODO this should no necessarily be a problem as we would remain if everybody else cancels # self.logger.debug('%s: deleting %s' % (table, message['data'])) # Locate the item in the collection and remove it. for deleteData in message['data']: item = findItemByKeys(self.keys[table], self.data[table], deleteData) self.data[table].remove(item) elif table not in self.partials: self.debug("discarding incomplete data") elif action not in ['delete', 'update', 'insert', 'partial']: raise Exception("Unknown action: %s" % action) except: self.logger.warning(traceback.format_exc()) def recent_trades(self, n=10): """ retrieves the last N recent trades :param n: int :return: list """ if 'margin' in self.partials: print(self.data['trade']) return self.data['trade'][-n:] else: return None def get_inventory(self): """ basic inventory calculation :return: """ if self.ord_keep.transactions != []: return sum([x['orderQty'] for x in self.ord_keep.transactions]) else: return 0 def inv_avg_price(self): """ get the average price (utility function) :return: """ if self.ord_keep.transactions != []: return avg_price_inv(self.symbol_list[0], self.ord_keep.transactions) else: return None def margin_report(self): """ retrieves initial margin balance and current margin balance :return: float, float """ if 'margin' in self.partials: return self.init_margin_balance, self.data['margin'][0][ 'marginBalance'] #print("m bal: {}\n{}".format(self.init_margin_balance,self.data['margin'][0]['marginBalance'])) else: return None, None def _subscribe(self, ws): subs_args = build_requests(self.subs, self.symbol_list) if len(subs_args) > 10: for i in range(0, len(subs_args), 10): # Create an index range for l of n items: ws.send_json({"op": "subscribe", "args": subs_args[i:i + 10]}) else: ws.send_json({"op": "subscribe", "args": subs_args}) # margin ws.send_json({"op": "subscribe", "args": "margin"}) def run(self): cnt_deadman = 0 # recommended is 15 seconds, but 30 should do for dev cnt2 = 0 # ghetto reducing spamm subs_args = build_requests(self.subs, self.symbol_list) ws = self._prep_connection() for event in persist(ws): if isinstance(event, events.Rejected): self.logger.warning(str(event.response)) self.warn(event.response) if isinstance(event, events.BackOff): self.warn("backing off for {} s".format(str(event.delay))) sleep(event.delay) ws.close() self.run() # regen nonce and stuff if event.name == "ready": # send deadman switch ws.send_json({"op": "cancelAllAfter", "args": 60000}) self._subscribe(ws=ws) if isinstance(event, events.Text): msg = (json.loads(event.text)) # print(msg) # now we get the limit value from welcome message (or rate limit, later)#TODO MED test it rough!!! if 'info' in msg.keys() and 'limit' in msg.keys(): self.rem_limit = msg['limit'] # we check and ack subscriptions if 'success' in msg.keys() and 'subscribe' in msg.keys(): try: subs_args.remove(msg['subscribe']) except: self.warn( "that's weird : {} was not to be sub'd".format( msg['subscribe'])) if len(subs_args) == 0: self.debug("all subs good to go") # # now we deal with L2 if 'table' in msg.keys() and msg['table'] == 'orderBookL2_25': self.book.getmsg(msg) if self.book.partial: # waiting for the order book # self.toy_mm() self.agent.on_l2() # # orders if 'table' in msg.keys() and msg['table'] == 'order': # print(msg) self.ord_keep.ws_update( msg) # TODO this needs to be threadsafe if 'table' in msg.keys() and msg['table'] in [ 'trade', 'margin' ]: self.message_data_updater(msg, event.received_time) if msg['table'] == 'trade': # we calculate state (trade ratio might change as a result) self.agent.on_trade() if isinstance(event, events.Poll): cnt_deadman += 1 print(self.ord_keep.exec_report()) if cnt_deadman == self.deadman_renew: # send deadman switch self.debug("deadman re-activate") ws.send_json({"op": "cancelAllAfter", "args": 60000}) cnt_deadman = 0 try: self.debug(self.book.bbo(lvl=5)) except: pass self.debug(self.ord_keep.ord_report()) print("current inventory: {}".format( sum([x['orderQty'] for x in self.ord_keep.transactions]))) avg_price = avg_price_inv(self.symbol_list[0], self.ord_keep.transactions) if avg_price: print("holding at avg price {}".format(str(avg_price)))