class BiboxMarketMakerKeeper: """Keeper acting as a market maker on Bibox.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bibox-market-maker-keeper') parser.add_argument( "--bibox-api-server", type=str, default="https://api.bibox.com", help= "Address of the Bibox API server (default: 'https://api.bibox.com')" ) parser.add_argument("--bibox-api-key", type=str, required=True, help="API key for the Bibox API") parser.add_argument("--bibox-secret", type=str, required=True, help="Secret for the Bibox API") parser.add_argument( "--bibox-timeout", type=float, default=9.5, help= "Timeout for accessing the Bibox API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--control-feed", type=str, help="Source of control feed") parser.add_argument( "--control-feed-expiry", type=int, default=86400, help="Maximum age of the control feed (in seconds, default: 86400)" ) parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.bibox_api = BiboxApi(api_server=self.arguments.bibox_api_server, api_key=self.arguments.bibox_api_key, secret=self.arguments.bibox_secret, timeout=self.arguments.bibox_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.control_feed = create_control_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.bibox_api.get_orders(pair=self.pair(), retry=True)) self.order_book_manager.get_balances_with( lambda: self.bibox_api.coin_list(retry=True)) self.order_book_manager.cancel_orders_with( lambda order: self.bibox_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): self.order_book_manager.cancel_all_orders(final_wait_time=30) def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number( next(filter(lambda coin: coin['symbol'] == token, our_balances))['balance']) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.control_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # Place new orders self.place_orders( bands.new_orders( our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance( order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance( order_book.balances, self.token_sell()), target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount amount_symbol = self.token_sell() money = new_order_to_be_placed.buy_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.pay_amount money_symbol = self.token_buy() new_order_id = self.bibox_api.place_order( is_sell=new_order_to_be_placed.is_sell, amount=amount, amount_symbol=amount_symbol, money=money, money_symbol=money_symbol) return Order(new_order_id, 0, new_order_to_be_placed.is_sell, Wad(0), amount, amount_symbol, money, money_symbol) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order))
class BiboxMarketSurfer: """Keeper acting as a market maker on Bibox.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bibox-market-maker-keeper') parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the surfer will operate") parser.add_argument( "--output-path", type=str, required=True, help="output file path of the completed order result") # reserved for old program # parser.add_argument("--price-feed", type=str, required=True, # help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.orderlog = Logger(self.arguments.output_path + "SurferResult_" + self.arguments.pair, level='info') try: f = open(self.arguments.config, 'r') config = json.loads(f.read()) f.close() self.bibox_api = BiboxApi(api_server=config["bibox_api_server"], api_key=config["bibox_api_key"], secret=config["bibox_secret"], timeout=config["bibox_timeout"]) for pair in config['pairs']: if pair['pair'] == self.arguments.pair: self.total_amount = pair["total_amount"] # percent of total amount of each transaction or each order self.each_order_percent = pair["each_order_percent"] self.arbitrage_percent = pair["arbitrage_percent"] # the order count of sell or buy bands must less than limit self.band_order_limit = pair["band_order_limit"] except Exception as e: logging.getLogger().warning( f"Config file is invalid ({e}). Treating the config file as it has no bands." ) self.history = History() self.bands_config = ReloadableConfig(self.arguments.config) # self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.local_orders = [] self.each_order_amount = self.total_amount * self.each_order_percent # To implement abstract function with different exchanges API self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.bibox_api.get_orders(pair=self.pair(), retry=True)) self.order_book_manager.get_balances_with( lambda: self.bibox_api.coin_list(retry=True)) self.order_book_manager.cancel_orders_with( lambda order: self.bibox_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): # Place new orders while initialize the whole surfer system self.initialize_orders(self.each_order_amount, self.arbitrage_percent, self.band_order_limit) time.sleep( 5 ) # wait for order book manager to get placed orders 足够时间保证系统稳定返回,这个时间还是不一定够 self.local_orders = self.order_book_manager.get_order_book().orders with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(10, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def initialize_orders(self, each_order_amount, arbitrage_percent, band_order_limit): orders = [] i = 1 base_price = self.get_last_price(self.pair()) while band_order_limit + 1 > i: # place sell order price = float(base_price) * (1 + arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) pay_amount = each_order_amount * self.amount_disguise( ) #bix amount pay_amount = pay_amount + self.suffix_amount_identify() buy_amount = pay_amount * price #eth money # print(pay_amount) orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) # place buy order, pay attention to rotate bix - eth price = float(base_price) * (1 - arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) tmp = each_order_amount * self.amount_disguise() pay_amount = tmp * price #eth money 25 buy_amount = tmp #bix amount 0.05 buy_amount = buy_amount + self.suffix_amount_identify() # print(buy_amount) orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) i = i + 1 self.place_orders(orders) # 偶尔有 bug,提交的完成慢,导致 local orders 比 下一次 获取回来少, initial_delay 加长时间到15秒,时间太长也麻烦, # 会导致一开始提交就成交的那部分订单不会存到 local orders # 需要换地方,order_book_manager更新不及时的情况下,会导致返回的订单数据不全,或者订单里的参数默认为0的情况 # self.local_orders = self.order_book_manager.get_order_book().orders @staticmethod def amount_disguise(): rand = [ 0.8, 0.84, 0.88, 0.92, 0.95, 0.99, 1.03, 1.06, 1.09, 1.12, 1.16, 1.2 ] return rand[random.randint(0, 11)] # suffix unique amount number to identify different buy/sell order pairs for result performance statics @staticmethod def suffix_amount_identify(): return round(random.random() / 10.0, 10) def get_last_price(self, pair): return self.bibox_api.ticker(pair)['last'] def shutdown(self): self.order_book_manager.cancel_all_orders(final_wait_time=30) def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number( next(filter(lambda coin: coin['symbol'] == token, our_balances))['balance']) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def count_sell_orders(self, our_orders: list) -> int: return len(list(filter(lambda order: order.is_sell, our_orders))) def count_buy_orders(self, our_orders: list) -> int: return len(list(filter(lambda order: not order.is_sell, our_orders))) def synchronize_orders(self): # bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() self.logger.info( "---**----synchronize_orders: The length of local_orders " + str(self.local_orders.__len__())) self.logger.info( "---**----synchronize_orders: The length of order_book.orders " + str(len(order_book.orders))) local_order_ids = set(order.order_id for order in self.local_orders) order_book_ids = set(order.order_id for order in order_book.orders) completed_order_ids = list(local_order_ids - order_book_ids) # 如果没有后续的更新 local orders,只有这里的更新模块,肯定有问题的,因为一旦有成交, # completed_order_ids 不为0,则永远更新不了local orders 了 # return if there none order be completed # 下面这种情况,只有在order_book订单完全"包含"local_order订单时,但是两者并不相等时,才会让本地订单等于远程订单; # 这种一般是远程订单比本地订单多,往往比如人工在系统提交了新的订单 # 这段是必须的,比如程序起步的时候,同时提交一批订单,导致速度慢,导致 local_orders 只有真正订单的一部分 if completed_order_ids.__len__() == 0: if local_order_ids.__len__() != order_book_ids.__len__(): self.logger.info( "---**---update local order with remote order while no completed order in past cycle" ) self.local_orders = order_book.orders return # completed_orders = list(filter(lambda order: order.order_id in completed_order_ids, self.local_orders)) completed_orders = [ order for order in self.local_orders if order.order_id in completed_order_ids ] # completed_orders = list(filter(lambda order: order.order_id in local_order_ids, order_book.orders)) self.logger.info("---**---The number of completed orders is " + str(len(completed_orders))) self.logger.info("---**---Below is/are completed orders : ") self.logger.info(completed_orders) # completed_orders_new = list(set(self.local_orders) - set(order_book.orders)) # print("---**---The lenght of completed new orders " + str(len(completed_orders_new))) # print(completed_orders_new) # completed_orders = [{'amount': Wad(2220000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': True, # 'money': Wad(52779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2294750000000000)}, {'amount': Wad(2990000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': False, # 'money': Wad(55779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2394750000000000)}] # our_buy_orders = self.our_buy_orders(order_book.orders) # our_sell_orders = self.our_sell_orders(order_book.orders) # print(our_buy_orders) # print(our_sell_orders) # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # order_book.orders 按照从低到高价格排序,获取价格最低(也就是最远)的1个订单作为要取消的订单; sorted_buy_orders = sorted(self.our_buy_orders(order_book.orders), key=lambda order: order.price) # order_book.orders 按照从高到低价格排序,获取价格最高(也就是最远)的 1 个订单作为要取消的订单; sorted_sell_orders = sorted(self.our_sell_orders(order_book.orders), key=lambda order: order.price, reverse=True) # if (self.local_orders.__len__() - len(order_book.orders) > 0): if len(completed_orders) > 0: self.logger.info( "---**---some orders have been completed, and new should be submitted" ) new_orders = [] cancellable_orders = [] step = 1 buy_index = 0 sell_index = 0 count_sell_order = self.count_sell_orders(order_book.orders) count_buy_order = self.count_buy_orders(order_book.orders) for cod in completed_orders: # print(type(cod)) # print(cod.is_sell) # the completed order is sell order, buy order should be placed cancel_order_count = 0 self.orderlog.logger.info(" - " + str(cod.is_sell) + " - " + str(cod.price) + " - " + str(cod.amount) + " - " + str(cod.price * cod.amount)) if cod.is_sell: #(1-1 buy) place buy order, pay attention to rotate bix - eth price = float(cod.price) * (1 - self.arbitrage_percent) self.logger.info("To submit a new buy order with price " + str(price)) pay_amount = float(cod.amount) * price # eth money 25 buy_amount = float(cod.amount) # bix amount 0.05 new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) #(1-2 buy) 取消超出订单数量上限的buy订单,价格最远的订单--就是最低价格的订单 buyo = count_buy_order + 1 - self.band_order_limit self.logger.info( "Number of exceed the upper limit of buy order: " + str(buyo)) # 加上后面这个条件,防止buy订单也成交,当前的订单列表里实际已经没有那么多数量的 buy 订单供取消 # 比如同一周期,buy 订单被吃了1,sell 订单被吃了3个,那么剩下的buy订单就不会被取消3次了 if buyo > 0 and len(sorted_buy_orders) > buy_index: # order_book.orders 按照从低到高价格排序,获取价格最低(也就是最远)的1个订单作为要取消的订单; # sorted_buy_orders = sorted(self.our_buy_orders(order_book.orders), # key=lambda order: order.price) print(sorted_buy_orders) self.logger.info( "Cancel buy order which exceed band order limit ") self.logger.info( str(sorted_buy_orders[buy_index].order_id)) self.logger.info(str(sorted_buy_orders[buy_index])) print(type(sorted_buy_orders[buy_index])) cancellable_orders.append(sorted_buy_orders[buy_index]) # self.bibox_api.cancel_order(sorted_buy_orders[index].order_id) buy_index = buy_index + 1 else: #如果没有对应取消订单,则 buy 订单数量增加 count_buy_order = count_buy_order + 1 #(2 sell) 以当前价格为基数,重新submit一个高价格的 sell 订单,补充 sell list # place sell a new order with higher price # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_sell_order = self.count_sell_orders(order_book.orders) band_sell_order_gap = self.band_order_limit - count_sell_order self.logger.info( "The number of sell order which need to submit " + str(band_sell_order_gap)) # while band_sell_order_gap > 0: # 外部已经有循环了,不需要这个循环了,否则在多订单被吃时,会加倍补充 # 这里只需要判断,控制数量就够了 if band_sell_order_gap > 0: current_price = self.get_last_price(self.pair()) self.logger.info("---**---current price is " + str(current_price)) price = float(current_price) * ( 1 + self.arbitrage_percent * (step + count_sell_order)) self.logger.info( "The higher price of new sell order is " + str(price)) pay_amount = self.each_order_amount * self.amount_disguise( ) # bix amount pay_amount = pay_amount + self.suffix_amount_identify( ) # add unique identify buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) count_sell_order = count_sell_order + 1 else: # buy order had been completed #(1-1 sell) to place a corresponding sell order price = float(cod.price) * (1 + self.arbitrage_percent) self.logger.info( "To submit new sell order with arbitrage price: " + str(price)) pay_amount = float(cod.amount) # bix amount buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), amount=Wad.from_number(pay_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) #(1-2 sell) 取消超出band order limits数量的 sell 订单 self.logger.info( "--------debug--------------Number of exceed the upper limit of sell order: " + str(count_sell_order + 1 - self.band_order_limit)) sello = count_sell_order + 1 - self.band_order_limit if sello > 0 and len(sorted_sell_orders) > sell_index: # order_book.orders 按照从高到低价格排序,获取价格最高(也就是最远)的 1 个订单作为要取消的订单; # sorted_sell_orders = sorted(self.our_sell_orders(order_book.orders), # key=lambda order: order.price, # reverse=True) print(sorted_sell_orders) self.logger.info( "Cancel sell order which exceed band order limit ") self.logger.info( str(sorted_sell_orders[sell_index].order_id)) self.logger.info(str(sorted_sell_orders[sell_index])) cancellable_orders.append( sorted_sell_orders[sell_index]) # self.bibox_api.cancel_order(sorted_sell_orders[index].order_id) sell_index = sell_index + 1 else: count_sell_order = count_sell_order + 1 #(2 buy) 以当前价格为基数,重新submit一个 buy 订单,补充 buy list # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_buy_order = self.count_buy_orders(order_book.orders) band_buy_order_gap = self.band_order_limit - count_buy_order self.logger.info( "The number of buy order which need to submit " + str(band_buy_order_gap)) if band_buy_order_gap > 0: #基础价格放在循环里的话,能快速反映当前价格,特保是激烈波动的时候;但是增加了请求次数 current_price = self.get_last_price(self.pair()) price = float(current_price) * ( 1 - self.arbitrage_percent * (step + count_buy_order)) self.logger.info( "The lower price order of new buy order is " + str(price)) tmp = self.each_order_amount * self.amount_disguise() pay_amount = tmp * price # eth money 25 buy_amount = tmp # bix amount 0.05 buy_amount = buy_amount + self.suffix_amount_identify( ) # add unique identify new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), amount=Wad.from_number(buy_amount), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # band_buy_order_gap = band_buy_order_gap - 1 count_buy_order = count_buy_order + 1 step = step + 1 # Cancel orders if len(cancellable_orders) > 0: self.order_book_manager.cancel_orders(cancellable_orders) time.sleep(1) while order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.info( "Sleep 1 s while order manager is in progress after cancelling exceed orders" ) time.sleep(1) # Submit new orders self.place_orders(new_orders) time.sleep(1) while order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.info( "Sleep 1 s while order manager is in progress after placing new orders" ) time.sleep(1) # update local orders, 前面有更新模块,与这边不完全相同,尤其是有成交的情况下,必须要更新 # 是这样吗? 似乎也不是的,有成交的情况下,下一次订单也会让 set(local) - set(order book)=0的,集合相减的特殊之处 # 如果这样就没有必要了。 # 是这样简单的复制更新,还是本地自己维护一个 id list 好呢? 也就是把(1)确定成交的从 local 删除; # (2)确定提交的add 到本地; # 缩进到循环: if len(completed_orders) > 0:,在出现两者不一致的时候,同步更新订单; # 但是这个会导致一个问题,就是初始化的订单里,有price 为0,导致两者不一致的情况,怎么办?这里解决了,是通过 order id对比而不是 # 直接的 order 对比,所以应该是解决了才对 # 默认有3秒的刷新周期,导致被成功取消的订单,不会马上出现在order_book_manager # 导致 local 比真实的多了,使得取消的订单在下一次被误认为是成交了 # 以上虽然 while 循环了,但是如果 while 循环的时候,有订单被成交了,这个订单就会丢掉,被误认为是撤销的订单了 self.logger.info( "Update local order with latest remote order at the end of synchronize cycle" ) self.local_orders = self.order_book_manager.get_order_book().orders # Cancel orders # cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), # our_sell_orders=self.our_sell_orders(order_book.orders), # target_price=target_price) # if len(cancellable_orders) > 0: # self.order_book_manager.cancel_orders(cancellable_orders) # print("there is " + str(len(cancellable_orders)) + " orders should be cancelled") # Place new orders # self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), # our_sell_orders=self.our_sell_orders(order_book.orders), # our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), # our_sell_balance=self.our_available_balance(order_book.balances, self.token_sell()), # target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount amount_symbol = self.token_sell() money = new_order_to_be_placed.buy_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.pay_amount money_symbol = self.token_buy() new_order_id = self.bibox_api.place_order( is_sell=new_order_to_be_placed.is_sell, amount=amount, amount_symbol=amount_symbol, money=money, money_symbol=money_symbol) return Order(new_order_id, 0, new_order_to_be_placed.is_sell, Wad(money / amount), amount, amount_symbol, money, money_symbol) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order)) def get_price(self, pair): self.bibox_api.get_all_trades()
class BiboxMarketMakerKeeper: """Keeper acting as a market maker on Bibox.""" logger = logging.getLogger() def __init__(self, args: list, **kwargs): parser = argparse.ArgumentParser(prog='bibox-market-maker-keeper') parser.add_argument("--rpc-host", type=str, default="localhost", help="JSON-RPC host (default: `localhost')") parser.add_argument("--rpc-port", type=int, default=8545, help="JSON-RPC port (default: `8545')") parser.add_argument("--rpc-timeout", type=int, default=10, help="JSON-RPC timeout (in seconds, default: 10)") parser.add_argument("--tub-address", type=str, required=True, help="Ethereum address of the Tub contract") parser.add_argument("--bibox-api-server", type=str, default="https://api.bibox.com", help="Address of the Bibox API server (default: 'https://api.bibox.com')") parser.add_argument("--bibox-api-key", type=str, required=True, help="API key for the Bibox API") parser.add_argument("--bibox-secret", type=str, required=True, help="Secret for the Bibox API") parser.add_argument("--bibox-timeout", type=float, default=9.5, help="Timeout for accessing the Bibox API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair on which the keeper should operate") parser.add_argument("--config", type=str, required=True, help="Buy/sell bands configuration file") parser.add_argument("--price-feed", type=str, help="Source of price feed. Tub price feed will be used if not specified") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of non-Tub price feed (in seconds, default: 120)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) self.web3 = kwargs['web3'] if 'web3' in kwargs else Web3(HTTPProvider(endpoint_uri=f"http://{self.arguments.rpc_host}:{self.arguments.rpc_port}", request_kwargs={"timeout": self.arguments.rpc_timeout})) self.tub = None #Tub(web3=self.web3, address=Address(self.arguments.tub_address)) self.vox = None #Vox(web3=self.web3, address=self.tub.vox()) logging.basicConfig(format='%(asctime)-15s %(levelname)-8s %(message)s', level=(logging.DEBUG if self.arguments.debug else logging.INFO)) logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO) logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.INFO) self.bibox_api = BiboxApi(api_server=self.arguments.bibox_api_server, api_key=self.arguments.bibox_api_key, secret=self.arguments.bibox_secret, timeout=self.arguments.bibox_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments.price_feed, self.arguments.price_feed_expiry, self.tub, self.vox) self.bibox_order_book_manager = BiboxOrderBookManager(bibox_api=self.bibox_api, pair=self.pair(), refresh_frequency=3) def main(self): with Lifecycle(self.web3) as lifecycle: lifecycle.wait_for_sync(False) lifecycle.initial_delay(10) lifecycle.on_startup(self.startup) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def startup(self): user_info = self.bibox_api.user_info(retry=True) self.logger.info(f"Bibox API key seems to be valid") self.logger.info(f"Accessing Bibox as user_id: '{user_info['user_id']}', email: '{user_info['email']}'") def shutdown(self): while True: try: our_orders = self.bibox_api.get_orders(self.bibox_order_book_manager.pair, retry=True) except: continue if len(our_orders) == 0: break self.cancel_orders(our_orders) self.bibox_order_book_manager.wait_for_order_cancellation() def price(self) -> Wad: return self.price_feed.get_price() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number(next(filter(lambda coin: coin['symbol'] == token, our_balances))['balance']) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands(self.bands_config) order_book = self.bibox_order_book_manager.get_order_book() target_price = self.price() if target_price is None: self.logger.warning("Cancelling all orders as no price feed available.") self.cancel_orders(order_book.orders) return # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.in_progress: self.logger.debug("Order book is in progress, not placing new orders") return # Place new orders self.create_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_balance(order_book.balances, self.token_sell()), target_price=target_price)) def cancel_orders(self, orders): for order in orders: self.bibox_order_book_manager.cancel_order(order.order_id) def create_orders(self, orders): for order in orders: amount = order.pay_amount if order.is_sell else order.buy_amount money = order.buy_amount if order.is_sell else order.pay_amount self.bibox_order_book_manager.place_order(is_sell=order.is_sell, amount=amount, amount_symbol=self.token_sell(), money=money, money_symbol=self.token_buy())
class BiboxMarketSurfer: """Keeper acting as a market maker on Bibox.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bibox-market-maker-keeper') parser.add_argument( "--bibox-api-server", type=str, default="https://api.bibox.com", help= "Address of the Bibox API server (default: 'https://api.bibox.com')" ) parser.add_argument("--bibox-api-key", type=str, required=True, help="API key for the Bibox API") parser.add_argument("--bibox-secret", type=str, required=True, help="Secret for the Bibox API") parser.add_argument( "--bibox-timeout", type=float, default=9.5, help= "Timeout for accessing the Bibox API (in seconds, default: 9.5)") parser.add_argument( "--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument( "--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument( "--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument( "--order-history-every", type=int, default=30, help= "Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument( "--refresh-frequency", type=int, default=3, help="Order book refresh frequency (in seconds, default: 3)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") parser.add_argument("--base_price", type=str, default=0.00205375, help="base price while initial base price ") parser.add_argument("--total_amount", type=str, default=5, help="the total assets for investing ") parser.add_argument( "--transaction_percent", type=str, default=0.02, help= "percent of total amount of each transaction or each order, fix percent 2%" ) parser.add_argument( "--arbitrage_percent", type=str, default=0.005, help= "the percent of current pirce as margin between two adjacent price orders, 0.5%" ) parser.add_argument( "--order_num", type=str, default=3, help="the number of orders in each sell and buy bands ") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.bibox_api = BiboxApi(api_server=self.arguments.bibox_api_server, api_key=self.arguments.bibox_api_key, secret=self.arguments.bibox_secret, timeout=self.arguments.bibox_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter( self.arguments) self.local_orders = [] self.base_price = 0.00243602 self.total_amount = 2000 self.each_order_percent = 0.1 # percent of total amount of each transaction or each order self.arbitrage_percent = 0.01 self.band_order_limit = 3 # the order count of sell or buy bands must less than limit self.each_order_amount = self.total_amount * self.each_order_percent # To implement abstract function with different exchanges API self.order_book_manager = OrderBookManager( refresh_frequency=self.arguments.refresh_frequency) self.order_book_manager.get_orders_with( lambda: self.bibox_api.get_orders(pair=self.pair(), retry=True)) self.order_book_manager.get_balances_with( lambda: self.bibox_api.coin_list(retry=True)) self.order_book_manager.cancel_orders_with( lambda order: self.bibox_api.cancel_order(order.order_id)) self.order_book_manager.enable_history_reporting( self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): # Place new orders while initialize the whole surfer system self.initialize_orders(self.base_price, self.each_order_amount, self.arbitrage_percent, self.band_order_limit) time.sleep( 5) # wait for order book manager to get placed orders 足够时间保证系统稳定返回 self.local_orders = self.order_book_manager.get_order_book().orders with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(10, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def initialize_orders(self, base_price, each_order_amount, arbitrage_percent, band_order_limit): orders = [] i = 1 while band_order_limit + 1 > i: # place sell order price = base_price * (1 + arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) pay_amount = each_order_amount * self.amount_disguise( ) #bix amount # add unique amount number to identify different order for result performance statics pay_amount = pay_amount + self.amount_identify() buy_amount = pay_amount * price #eth money orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) # place buy order, pay attention to rotate bix - eth price = base_price * (1 - arbitrage_percent * i) # pay_amount = Wad.min(band.avg_amount - total_amount, our_sell_balance, limit_amount) tmp = each_order_amount * self.amount_disguise( ) + self.amount_identify() pay_amount = tmp * price #eth money 25 buy_amount = tmp #bix amount 0.05 orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits.use_limit( time.time(), pay_amount))) i = i + 1 self.place_orders(orders) # 偶尔有 bug,提交的完成慢,导致 local orders 比 下一次 获取回来少, initial_delay 加长时间到15秒,时间太长也麻烦, # 会导致一开始提交就成交的那部分订单不会存到 local orders # 需要换地方,order_book_manager更新不及时的情况下,会导致返回的订单数据不全,或者订单里的参数默认为0的情况 # self.local_orders = self.order_book_manager.get_order_book().orders @staticmethod def amount_disguise(): rand = [ 0.8, 0.84, 0.88, 0.92, 0.95, 0.99, 1.03, 1.06, 1.09, 1.12, 1.16, 1.2 ] return rand[random.randint(0, 11)] @staticmethod def amount_identify(): return round(random.random() / 10000.0, 10) def shutdown(self): self.order_book_manager.cancel_all_orders(final_wait_time=30) def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number( next(filter(lambda coin: coin['symbol'] == token, our_balances))['balance']) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def count_sell_orders(self, our_orders: list) -> int: return len(list(filter(lambda order: order.is_sell, our_orders))) def count_buy_orders(self, our_orders: list) -> int: return len(list(filter(lambda order: not order.is_sell, our_orders))) def synchronize_orders(self): bands = Bands.read(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # print(type(self.local_orders)) # print(self.local_orders) # print(type(order_book.orders)) # print(order_book.orders) print("---**---The lenght of local_orders " + str(self.local_orders.__len__())) print("---**---The lenght of order_book.orders " + str(len(order_book.orders))) local_order_ids = set(order.order_id for order in self.local_orders) order_book_ids = set(order.order_id for order in order_book.orders) completed_order_ids = list(local_order_ids - order_book_ids) # 如果没有后续的更新 local orders,只有这里的更新模块,肯定有问题的,因为一旦有成交, # completed_order_ids 不为0,则永远更新不了local orders 了 # return if there none order be completed # 下面这种情况,只有在order_book订单完全"包含"local_order订单时,但是两者并不相等时,才会让本地订单等于远程订单; # 这种一般是远程订单比本地订单多,往往比如人工在系统提交了新的订单 if completed_order_ids.__len__() == 0: if local_order_ids.__len__() != order_book_ids.__len__(): print("update local order") self.local_orders = order_book.orders return # completed_orders = list(filter(lambda order: order.order_id in completed_order_ids, self.local_orders)) completed_orders = [ order for order in self.local_orders if order.order_id in completed_order_ids ] # completed_orders = list(filter(lambda order: order.order_id in local_order_ids, order_book.orders)) print("---**---The lenght of completed orders " + str(len(completed_orders))) print(completed_orders) # completed_orders_new = list(set(self.local_orders) - set(order_book.orders)) # print("---**---The lenght of completed new orders " + str(len(completed_orders_new))) # print(completed_orders_new) # completed_orders = [{'amount': Wad(2220000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': True, # 'money': Wad(52779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2294750000000000)}, {'amount': Wad(2990000000000000000), # 'amount_symbol': 'BIX', # 'created_at': 1528203670000, # 'is_sell': False, # 'money': Wad(55779250000000000), # 'money_symbol': 'ETH', # 'order_id': 606026215, # 'price': Wad(2394750000000000)}] # our_buy_orders = self.our_buy_orders(order_book.orders) # our_sell_orders = self.our_sell_orders(order_book.orders) # print(our_buy_orders) # print(our_sell_orders) # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug( "Order book is in progress, not placing new orders") return # if (self.local_orders.__len__() - len(order_book.orders) > 0): if len(completed_orders) > 0: print("--------- some orders have been done --------") new_orders = [] step = 1 count_sell_order = self.count_sell_orders(order_book.orders) count_buy_order = self.count_buy_orders(order_book.orders) for cod in completed_orders: # print(type(cod)) # print(cod.is_sell) # the completed order is sell order, buy order should be placed if cod.is_sell: # place buy order, pay attention to rotate bix - eth price = float(cod.price) * (1 - self.arbitrage_percent) print("----to submit a new buy order with price " + str(price)) pay_amount = float(cod.amount) * price # eth money 25 buy_amount = float(cod.amount) # bix amount 0.05 new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # 以当前价格为基数,重新submit一个高价格的 sell 订单,补充 sell list # place sell a new order with higher price # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_sell_order = self.count_sell_orders(order_book.orders) print(count_sell_order) band_sell_order_gap = self.band_order_limit - count_sell_order print("---band gap---- " + str(band_sell_order_gap)) # while band_sell_order_gap > 0: # 外部已经有循环了,不需要这个循环了,否则在多订单被吃时,会加倍补充 # 这里只需要判断,控制数量就够了 if band_sell_order_gap > 0: current_price = self.bibox_api.get_last_price( self.pair()) print("------current price---- " + str(current_price)) price = float(current_price) * ( 1 + self.arbitrage_percent * (step + count_sell_order)) print("----higher price to sell--- " + str(price)) pay_amount = self.each_order_amount * self.amount_disguise( ) # bix amount buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # step = step + 1 # band_sell_order_gap = band_sell_order_gap - 1 count_sell_order = count_sell_order + 1 else: # buy order had been completed # to place a sell order price = float(cod.price) * (1 + self.arbitrage_percent) print("----price--- sell--- ") print(price) pay_amount = float(cod.amount) # bix amount buy_amount = pay_amount * price # eth money new_orders.append( NewOrder(is_sell=True, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # 以当前价格为基数,重新submit一个 buy 订单,补充 buy list # 需要判断订单的数量是否小于band order limits,并且按照差异补充订单 # count_buy_order = self.count_buy_orders(order_book.orders) band_buy_order_gap = self.band_order_limit - count_buy_order print("---band gap----" + str(band_buy_order_gap)) # while band_buy_order_gap > 0: if band_buy_order_gap > 0: #基础价格放在循环里的话,能快速反映当前价格,特保是激烈波动的时候;但是增加了请求次数 current_price = self.bibox_api.get_last_price( self.pair()) price = float(current_price) * ( 1 - self.arbitrage_percent * (step + count_buy_order)) print("----lower price order to buy--- " + str(price)) tmp = self.each_order_amount * self.amount_disguise() pay_amount = tmp * price # eth money 25 buy_amount = tmp # bix amount 0.05 new_orders.append( NewOrder(is_sell=False, price=Wad.from_number(price), pay_amount=Wad.from_number(pay_amount), buy_amount=Wad.from_number(buy_amount), confirm_function=lambda: self.sell_limits. use_limit(time.time(), pay_amount))) # band_buy_order_gap = band_buy_order_gap - 1 count_buy_order = count_buy_order + 1 step = step + 1 self.place_orders(new_orders) # update local orders, 前面有更新模块,与这边不完全相同,尤其是有成交的情况下,必须要更新 # 是这样吗? 似乎也不是的,有成交的情况下,下一次订单也会让 set(local) - set(order book)=0的,集合相减的特殊之处 # 如果这样就没有必要了。 # 是这样简单的复制更新,还是本地自己维护一个 id list 好呢? 也就是把(1)确定成交的从 local 删除; # (2)确定提交的add 到本地; # 缩进到循环: if len(completed_orders) > 0:,在出现两者不一致的时候,同步更新订单; # 但是这个会导致一个问题,就是初始化的订单里,有price 为0,导致两者不一致的情况,怎么办?这里解决了,是通过 order id对比而不是 # 直接的 order 对比,所以应该是解决了才对 print("-----update local order------") self.local_orders = self.order_book_manager.get_order_book().orders # Cancel orders # cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), # our_sell_orders=self.our_sell_orders(order_book.orders), # target_price=target_price) # if len(cancellable_orders) > 0: # self.order_book_manager.cancel_orders(cancellable_orders) # print("there is " + str(len(cancellable_orders)) + " orders should be cancelled") # Place new orders # self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), # our_sell_orders=self.our_sell_orders(order_book.orders), # our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), # our_sell_balance=self.our_available_balance(order_book.balances, self.token_sell()), # target_price=target_price)[0]) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount amount_symbol = self.token_sell() money = new_order_to_be_placed.buy_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.pay_amount money_symbol = self.token_buy() new_order_id = self.bibox_api.place_order( is_sell=new_order_to_be_placed.is_sell, amount=amount, amount_symbol=amount_symbol, money=money, money_symbol=money_symbol) return Order(new_order_id, 0, new_order_to_be_placed.is_sell, Wad(money / amount), amount, amount_symbol, money, money_symbol) for new_order in new_orders: self.order_book_manager.place_order( lambda new_order=new_order: place_order_function(new_order)) def get_price(self, pair): self.bibox_api.get_all_trades()
class BiboxMarketMakerKeeper: """Keeper acting as a market maker on Bibox.""" logger = logging.getLogger() def __init__(self, args: list): parser = argparse.ArgumentParser(prog='bibox-market-maker-keeper') parser.add_argument("--bibox-api-server", type=str, default="https://api.bibox.com", help="Address of the Bibox API server (default: 'https://api.bibox.com')") parser.add_argument("--bibox-api-key", type=str, required=True, help="API key for the Bibox API") parser.add_argument("--bibox-secret", type=str, required=True, help="Secret for the Bibox API") parser.add_argument("--bibox-timeout", type=float, default=9.5, help="Timeout for accessing the Bibox API (in seconds, default: 9.5)") parser.add_argument("--pair", type=str, required=True, help="Token pair (sell/buy) on which the keeper will operate") parser.add_argument("--config", type=str, required=True, help="Bands configuration file") parser.add_argument("--price-feed", type=str, required=True, help="Source of price feed") parser.add_argument("--price-feed-expiry", type=int, default=120, help="Maximum age of the price feed (in seconds, default: 120)") parser.add_argument("--spread-feed", type=str, help="Source of spread feed") parser.add_argument("--spread-feed-expiry", type=int, default=3600, help="Maximum age of the spread feed (in seconds, default: 3600)") parser.add_argument("--order-history", type=str, help="Endpoint to report active orders to") parser.add_argument("--order-history-every", type=int, default=30, help="Frequency of reporting active orders (in seconds, default: 30)") parser.add_argument("--debug", dest='debug', action='store_true', help="Enable debug output") self.arguments = parser.parse_args(args) setup_logging(self.arguments) self.history = History() self.bibox_api = BiboxApi(api_server=self.arguments.bibox_api_server, api_key=self.arguments.bibox_api_key, secret=self.arguments.bibox_secret, timeout=self.arguments.bibox_timeout) self.bands_config = ReloadableConfig(self.arguments.config) self.price_feed = PriceFeedFactory().create_price_feed(self.arguments) self.spread_feed = create_spread_feed(self.arguments) self.order_history_reporter = create_order_history_reporter(self.arguments) self.order_book_manager = OrderBookManager(refresh_frequency=3) self.order_book_manager.get_orders_with(lambda: self.bibox_api.get_orders(pair=self.pair(), retry=True)) self.order_book_manager.get_balances_with(lambda: self.bibox_api.coin_list(retry=True)) self.order_book_manager.enable_history_reporting(self.order_history_reporter, self.our_buy_orders, self.our_sell_orders) self.order_book_manager.start() def main(self): with Lifecycle() as lifecycle: lifecycle.initial_delay(10) lifecycle.every(1, self.synchronize_orders) lifecycle.on_shutdown(self.shutdown) def shutdown(self): #TODO I don't think this approach makes sure all orders will always get cancelled!! while True: try: our_orders = self.bibox_api.get_orders(self.pair(), retry=True) except: continue if len(our_orders) == 0: break self.cancel_orders(our_orders) self.order_book_manager.wait_for_order_cancellation() def pair(self): return self.arguments.pair.upper() def token_sell(self) -> str: return self.arguments.pair.split('_')[0].upper() def token_buy(self) -> str: return self.arguments.pair.split('_')[1].upper() def our_available_balance(self, our_balances: list, token: str) -> Wad: return Wad.from_number(next(filter(lambda coin: coin['symbol'] == token, our_balances))['balance']) def our_sell_orders(self, our_orders: list) -> list: return list(filter(lambda order: order.is_sell, our_orders)) def our_buy_orders(self, our_orders: list) -> list: return list(filter(lambda order: not order.is_sell, our_orders)) def synchronize_orders(self): bands = Bands(self.bands_config, self.spread_feed, self.history) order_book = self.order_book_manager.get_order_book() target_price = self.price_feed.get_price() # Cancel orders cancellable_orders = bands.cancellable_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), target_price=target_price) if len(cancellable_orders) > 0: self.cancel_orders(cancellable_orders) return # Do not place new orders if order book state is not confirmed if order_book.orders_being_placed or order_book.orders_being_cancelled: self.logger.debug("Order book is in progress, not placing new orders") return # Place new orders self.place_orders(bands.new_orders(our_buy_orders=self.our_buy_orders(order_book.orders), our_sell_orders=self.our_sell_orders(order_book.orders), our_buy_balance=self.our_available_balance(order_book.balances, self.token_buy()), our_sell_balance=self.our_available_balance(order_book.balances, self.token_sell()), target_price=target_price)[0]) def cancel_orders(self, orders): for order in orders: self.order_book_manager.cancel_order(order.order_id, lambda order=order: self.bibox_api.cancel_order(order.order_id)) def place_orders(self, new_orders): def place_order_function(new_order_to_be_placed): amount = new_order_to_be_placed.pay_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.buy_amount amount_symbol = self.token_sell() money = new_order_to_be_placed.buy_amount if new_order_to_be_placed.is_sell else new_order_to_be_placed.pay_amount money_symbol = self.token_buy() new_order_id = self.bibox_api.place_order(is_sell=new_order_to_be_placed.is_sell, amount=amount, amount_symbol=amount_symbol, money=money, money_symbol=money_symbol) return Order(new_order_id, 0, new_order_to_be_placed.is_sell, Wad(0), amount, amount_symbol, money, money_symbol) for new_order in new_orders: self.order_book_manager.place_order(lambda new_order=new_order: place_order_function(new_order))