def test_failed_leg1(self): ex = ccxtExchangeWrapper.load_from_id("binance") #type: ccxtExchangeWrapper ex.set_offline_mode("test_data/markets.json", "test_data/tickers_maker.csv") ex.load_markets() ex.fetch_tickers() tickers = ex.fetch_tickers() # second fetch contains good triangle om = ActionOrderManager(ex) om.offline_order_updates = 10 good_triangle = self.good_triangle maker = SingleTriArbMakerDeal(currency1=good_triangle["cur1"], currency2=good_triangle["cur2"], currency3=good_triangle["cur3"], price1=good_triangle["leg1-price"], price2=good_triangle["leg2-price"], price3=good_triangle["leg3-price"], start_amount=0.01, min_amount_currency1=0.003, symbol1=good_triangle["symbol1"], symbol2=good_triangle["symbol2"], symbol3=good_triangle["symbol3"], commission=0.00075, commission_maker=0.0006, threshold=1.001, max_order1_updates=2000, max_order2_updates=2000, max_order3_updates=2000, uuid="test" ) maker.update_state(tickers) om.add_order(maker.order1) om.proceed_orders() maker.update_state(tickers) maker.order1.force_close() om.proceed_orders() maker.update_state(tickers) self.assertEqual("Failed", maker.status) self.assertEqual(0, maker.leg2_recovery_amount) self.assertEqual(0, maker.leg2_recovery_target) self.assertEqual(0, maker.leg3_recovery_amount) self.assertEqual(0, maker.leg3_recovery_target) self.assertEqual(0, maker.filled_start_amount) self.assertEqual(0, maker.result_amount) self.assertEqual(0, maker.gross_profit)
def init_order_manager(self): self.order_manager = ActionOrderManager(self.exchange, self.max_order_update_attempts, self.max_order_update_attempts, self.request_sleep) self.order_manager.log = self.log self.order_manager.LOG_INFO = self.LOG_INFO self.order_manager.LOG_ERROR = self.LOG_ERROR self.order_manager.LOG_DEBUG = self.LOG_DEBUG self.order_manager.LOG_CRITICAL = self.LOG_CRITICAL if self.not_request_trades: self.order_manager.request_trades = False # setting for offline mode if self.offline: self.exchange.trades_in_offline_order_update = False
def test_fok_threshold_order_manager_canceled_on_threshold(self): ex = ccxtExchangeWrapper.load_from_id("binance") # type: ccxtExchangeWrapper ex.set_offline_mode("test_data/markets.json", "test_data/tickers.csv") ex._offline_tickers = { 0: {"ETH/BTC": {"ask": 1, "bid": 0.99}}, 1: {"ETH/BTC": {"ask": 1, "bid": 0.95}}, 2: {"ETH/BTC": {"ask": 1, "bid": 0.99}}, 3: {"ETH/BTC": {"ask": 1, "bid": 0.99}}, 4: {"ETH/BTC": {"ask": 1, "bid": 0.99}}, 5: {"ETH/BTC": {"ask": 1, "bid": 0.99}}, 6: {"ETH/BTC": {"ask": 1, "bid": 0.99}}, 7: {"ETH/BTC": {"ask": 1, "bid": 0.95}}, } tickers = ex.fetch_tickers("ETH/BTC") taker_price = core.get_symbol_order_price_from_tickers("ETH","BTC", tickers)["price"] order = FokThresholdTakerPriceOrder.create_from_start_amount("ETH/BTC", "ETH", 1, "BTC", taker_price, taker_price_threshold=-0.01, threshold_check_after_updates=5) om = ActionOrderManager(ex) om.request_trades = False om.add_order(order) while len(om.get_open_orders()) > 0: om.proceed_orders() closed_order = om.get_closed_orders()[0] # type: FokThresholdTakerPriceOrder trade_order = closed_order.orders_history[0] # type: TradeOrder self.assertEqual("closed", closed_order.status) self.assertEqual("canceled", trade_order.status) self.assertEqual(["#below_threshold"], order.tags) # updates of order: # 1 - order created # 2 .. 5 - order updated without requesting tickers # 6 - request ticker # 7 - receives ticker and send command to cancel. 2nd ticker is "bad" because first was used to get price # - check's if it was canceled # 8 - updates trades order with the canceled response self.assertEqual(8, trade_order.update_requests_count)
def test_fok_threshold_order_manager_canceled_on_updates_count(self): ex = ccxtExchangeWrapper.load_from_id("binance") # type: ccxtExchangeWrapper ex.set_offline_mode("test_data/markets.json", "test_data/tickers.csv") ex._offline_tickers = { 0: {"ETH/BTC": {"ask": 1, "bid": 0.99}}, 1: {"ETH/BTC": {"ask": 1, "bid": 0.96}}, 2: {"ETH/BTC": {"ask": 1, "bid": 0.96}}, 3: {"ETH/BTC": {"ask": 1, "bid": 0.96}}, 4: {"ETH/BTC": {"ask": 1, "bid": 0.96}}, 5: {"ETH/BTC": {"ask": 1, "bid": 0.96}}, 6: {"ETH/BTC": {"ask": 1, "bid": 0.96}}, 7: {"ETH/BTC": {"ask": 1, "bid": 0.96}}, } tickers = ex.fetch_tickers("ETH/BTC") taker_price = core.get_symbol_order_price_from_tickers("ETH", "BTC", tickers)["price"] order = FokThresholdTakerPriceOrder.create_from_start_amount("ETH/BTC", "ETH", 1, "BTC", taker_price, max_order_updates=10, taker_price_threshold=-0.05, threshold_check_after_updates=5) om = ActionOrderManager(ex) om.request_trades = False om.add_order(order) while len(om.get_open_orders()) > 0: om.proceed_orders() closed_order = om.get_closed_orders()[0] # type: FokThresholdTakerPriceOrder trade_order = closed_order.orders_history[0] # type: TradeOrder self.assertEqual("closed", closed_order.status) self.assertEqual("canceled", trade_order.status) self.assertEqual(list(), order.tags) self.assertEqual(11, trade_order.update_requests_count)
def test_cancel_by_time(self): fok_order_time = FokThresholdTakerPriceOrder("ADA/ETH", 1000, 0.32485131 / 1000, "sell", max_order_updates=10, time_to_cancel=0.1) order_command = fok_order_time.update_from_exchange({}) ex = ccxtExchangeWrapper.load_from_id("binance") # type: ccxtExchangeWrapper ex.set_offline_mode("test_data/markets.json", "test_data/tickers.csv") om = ActionOrderManager(ex) om.request_trades = False om.add_order(fok_order_time) om.proceed_orders() time.sleep(0.11) om.proceed_orders() om.proceed_orders() self.assertEqual(fok_order_time.status, "closed") self.assertIn("#timeout", fok_order_time.tags)
def test_trade_order_report_from_trade_order_after_fill(self): order = ActionOrder.create_from_start_amount("ETH/BTC", "BTC", 1, "ETH", 0.01) ew = ccxtExchangeWrapper.load_from_id( "binace") # type: ccxtExchangeWrapper ew.set_offline_mode("test_data/markets.json", "test_data/tickers.csv") om = ActionOrderManager(ew) om.add_order(order) while len(om.get_open_orders()) > 0: om.proceed_orders() trade_order = om.get_closed_orders()[0].orders_history[ 0] # type: TradeOrder order_report = TradeOrderReport.from_trade_order( trade_order, datetime.datetime.now(tz=pytz.timezone("UTC")), supplementary={"order_no": 1}) self.assertEqual(order_report.symbol, trade_order.symbol) self.assertEqual(order_report.side, trade_order.side) self.assertEqual(order_report.amount, trade_order.amount) self.assertEqual(order_report.filled, trade_order.filled) self.assertEqual(order_report.cost, trade_order.cost) self.assertListEqual(order_report.trades, trade_order.trades) self.assertDictEqual(order_report.timestamp_closed, trade_order.timestamp_closed) self.assertDictEqual(order_report.timestamp_open, trade_order.timestamp_open) self.assertEqual(order_report.start_currency, trade_order.start_currency) self.assertEqual(order_report.start_currency, trade_order.start_currency) self.assertEqual( "fill", order_report.supplementary["parent_action_order"]["state"]) self.assertEqual(1, order_report.supplementary["order_no"])
class TriBot(Bot): # attributes of bot to be saved in reports. intended to be as a configuration parameters with config_ prefix CONFIG_PARAMETERS = ["share_balance_to_bid", "commission", "threshold", "threshold_order_book", "max_trades_updates", "order_update_total_requests", "order_update_requests_for_time_out", "order_update_time_out", "max_oder_books_fetch_attempts", "max_order_update_attempts", "request_sleep", "lap_time", "max_requests_per_lap", "sleep_on_tickers_error", "force_start_amount", "force_best_tri", "override_depth_amount", "skip_order_books", "recover_factor", "not_request_trades", "cancel_price_threshold", "fullthrottle"] def __init__(self, default_config: str, log_filename=None): self.session_uuid = str(uuid.uuid4()) self.fetch_number = 0 self.errors = 0 self.config_filename = default_config self.exchange_id = "" self.server_id = "" self.script_id = "" self.start_currency = list() self.ignore_currency = list() self.allowed_assets = list() self.share_balance_to_bid = float() self.min_amounts = dict() self.commission = float() self.threshold = float() self.threshold_order_book = float() self.balance_bid_thresholds = dict() self.api_key = dict() self.cancel_price_threshold = 0.0 # relative to taker's price threshold to cancel the order self.recover_factor = 0.0 # multiplier applied to target recovery amount self.max_trades_updates = 0 self.not_request_trades = False # if to request trades self.order_update_total_requests = 0 self.order_update_requests_for_time_out = 0 self.order_update_time_out = 0 self.last_update_time = datetime(1, 1, 1, 1, 1, 1) self.orders_settings = None self.max_oder_books_fetch_attempts = 0 self.max_order_update_attempts = 0 self.request_sleep = 0.0 # sleep time between requests in seconds self.fullthrottle = dict() # fullthrottle mode settings. dict of {"enabled":True, "start_at":10} self.state = "go" # or could be "wait" in fullthrottle mode self.start_ft_timestamp = 0.0 self.timer = ... # type: timer.Timer self.lap_time = float() self.max_requests_per_lap = 0.0 self.sleep_on_tickers_error = 0.0 # sleeping time when exception on receiving tickers self.test_balance = float() # start amount parameters self.force_start_amount = float() self.force_best_tri = bool() self.override_depth_amount = float() self.skip_order_books = False self.verbose = False self.debug = bool() self.run_once = False self.noauth = False self.offline = False self.offline_tickers_file = "test_data/tickers.csv" self.offline_order_books_file = "" self.offline_markets_file = "test_data/markets.json" self.offline_deal_uuid = "" self.offline_run_test = False # if to run the test self.recovery_server = "" self.tickers_file = str() self.logger = logging self.log_filename = log_filename self.logger = self.init_logging(self.log_filename) self.LOG_DEBUG = logging.DEBUG self.LOG_INFO = logging.INFO self.LOG_ERROR = logging.ERROR self.LOG_CRITICAL = logging.CRITICAL self.report_all_deals_filename = str() self.report_tickers_filename = str() self.report_deals_filename = str() self.report_prev_tickers_filename = str() self.report_dir = str() self.deals_file_id = int() self.reporter = None # type: TkgReporter # ;(((( # SQL Alchemy could not work together with Mongo!!! # self.mongo = dict() # mongo configuration # self.mongo_reporter = None # type: MongoReporter # prev config on mongo # "mongo": { # "note": "Should be disabled", # "enabled": false, # "#host": "mongodb://*****:*****@localhost:27017/?authSource=tkg_dev", # "host": "mongodb://<LOGIN>:<PASSWORD>@tkg-reporting-shard-00-00-izwsm.mongodb.net:27017,tkg-reporting-shard-00-01-izwsm.mongodb.net:27017,tkg-reporting-shard-00-02-izwsm.mongodb.net:27017/test?ssl=true&replicaSet=TKG-Reporting-shard-0&authSource=admin&retryWrites=true", # "db": "tkg_dev", # "tables": { # "trade_orders": "trade_orders", # "tri_results": "tri_results" # } # }, self.influxdb = None self.sqla = dict() # sqla configuration self.sqla_reporter = None # type: SqlaReporter self.exchange = ... # type: ccxtExchangeWrapper self.basic_triangles = list() self.basic_triangles_count = int() self.all_triangles = list() self.markets = dict() self.tickers = dict() self.deal_uuid = "" self.tri_list = list() self.tri_list_good = list() # self.recovery_data = list() self.balance = float() self.time = timer.Timer self.last_proceed_report = dict() self.order_manager = None # type: ActionOrderManager # load config from json def get_cli_parameters(self, args): return get_cli_parameters(args) # parse cli def set_from_cli(self, args): cli_args = self.get_cli_parameters(args) for i in cli_args.__dict__: attr_val = getattr(cli_args, i) if attr_val is not None: setattr(self, i, attr_val) # # init logging # def init_logging(self, file_log=None): log_formatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s") logger = logging.getLogger("tribot") if file_log is not None: file_handler = logging.FileHandler(file_log) file_handler.setFormatter(log_formatter) logger.addHandler(file_handler) console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(log_formatter) logger.addHandler(console_handler) logger.setLevel(logging.INFO) return logger def set_log_level(self, log_level): self.logger.setLevel(log_level) def log(self, level, msg, msg_list=None): if msg_list is None: self.logger.log(level, msg) else: self.logger.log(level, msg) for line in msg_list: self.logger.log(level, "... " + str(line)) def init_reports(self, directory): try: os.stat(directory) except: os.mkdir(directory) print("New directory created:", directory) # self.deals_file_id = utils.get_next_report_filename(directory, self.report_deals_filename) # self.report_deals_filename = self.report_deals_filename % (directory, self.deals_file_id) # self.report_prev_tickers_filename = self.report_prev_tickers_filename % (directory, self.deals_file_id) self.report_dir = directory def init_timer(self): self.timer = timer.Timer() self.timer.max_requests_per_lap = self.max_requests_per_lap self.timer.lap_time = self.lap_time def init_exchange(self): # exchange = getattr(ccxt, self.exchange_id) # self.exchange = exchange({'apiKey': self.api_key["apiKey"], 'secret': self.api_key["secret"] }) # self.exchange.load_markets() if not self.noauth: self.exchange = ccxtExchangeWrapper.load_from_id(self.exchange_id, self.api_key["apiKey"], self.api_key["secret"]) # type: ccxtExchangeWrapper else: self.exchange = ccxtExchangeWrapper.load_from_id(self.exchange_id) # type: ccxtExchangeWrapper # enabling throttling self.exchange.enable_requests_throttle(self.lap_time, self.max_requests_per_lap) def init_offline_mode(self): self.exchange.set_offline_mode(self.offline_markets_file, self.offline_tickers_file) if self.offline_order_books_file: self.exchange.load_offline_order_books_from_csv(self.offline_order_books_file) def init_test_run(self): self.log(self.LOG_INFO, "Init offline test. Will set run once to TRUE") self.deal_uuid = "test" self.exchange_id = "test" self.run_once = True self.init_reports("_" + self.exchange_id + "/") path = "_{}/".format(self.exchange_id) files = glob.glob(path + "test*") for f in files: self.log(self.LOG_INFO, "Deleting test file {}".format(f)) try: os.remove(f) except Exception as e: self.log(self.LOG_ERROR, "Could not delete file {}".format(f)) self.log(self.LOG_ERROR, "Exception: {}".format(type(e).__name__)) self.log(self.LOG_ERROR, "Exception body:", e.args) def init_order_manager(self): self.order_manager = ActionOrderManager(self.exchange, self.max_order_update_attempts, self.max_order_update_attempts, self.request_sleep) self.order_manager.log = self.log self.order_manager.LOG_INFO = self.LOG_INFO self.order_manager.LOG_ERROR = self.LOG_ERROR self.order_manager.LOG_DEBUG = self.LOG_DEBUG self.order_manager.LOG_CRITICAL = self.LOG_CRITICAL if self.not_request_trades: self.order_manager.request_trades = False # setting for offline mode if self.offline: self.exchange.trades_in_offline_order_update = False def load_markets(self): markets = self.exchange.load_markets() if len(self.allowed_assets) == 0: self.markets = markets else: self.markets = {k: v for k, v in markets.items() if (v["base"] in self.allowed_assets and v["quote"] in self.allowed_assets)} def set_triangles(self): self.basic_triangles = ta.get_basic_triangles_from_markets(self.markets) self.all_triangles = ta.get_all_triangles(self.basic_triangles, self.start_currency) # return True def proceed_triangles(self): if len(self.tri_list) == 0: self.tri_list = ta.fill_triangles(self.all_triangles, self.start_currency, self.tickers, self.commission) else: self.tri_list = ta.update_triangles(self.tri_list, tickers=self.tickers, commission=self.commission) return self.tri_list def load_balance(self): if self.test_balance > 0: self.balance = self.test_balance return self.test_balance else: if self.offline: self.exchange.set_offline_balance( {'free': { 'BTC': 1, 'USD': 123.00, "ETH": 10}, "BTC": {"free": 1}, "USDT": {"free": 123}, "ETH": {"free": 10} } ) self.balance = self.exchange.fetch_free_balance()[self.start_currency[0]] return self.balance # # get maximum balance to bid in respect to thresholds set in config # def max_balance_to_bid_from_thresholds(self, currency=None, balance=None, result=None, ob_result=None): currency = self.start_currency[0] if currency is None else currency balance = self.balance if balance is None else balance result = self.tri_list_good[0]["result"] if result is None else result ob_result = self.tri_list_good[0]["ob_result"] if ob_result is None else ob_result # balance_results_thresholds_config = self.balance_bid_thresholds[currency] balance_results_thresholds_config = dict() for l in self.balance_bid_thresholds[currency]: balance_results_thresholds_config[float(l["max_bid"])] = l balance_thresholds = sorted(list(balance_results_thresholds_config.keys())) try: i = bisect.bisect_left(balance_thresholds, balance) except IndexError: i = 0 if i >= len(balance_thresholds): i = len(balance_thresholds) - 1 while i >= 0: if result >= balance_results_thresholds_config[balance_thresholds[i]]['result_threshold'] and \ ob_result >= balance_results_thresholds_config[balance_thresholds[i]]["orderbook_result_threshold"]: to_bid = min(balance, balance_thresholds[i]) return to_bid else: i = i - 1 return None def get_order_books_async(self, symbols: list): """ returns the dict of {"symbol": OrderBook} in offline mode the order book is single line - ticker price and big amount :param symbols: list of symbols to get orderbooks :return: returns the dict of {"symbol": OrderBook} """ i = 0 ob_array = list() while len(ob_array) < 3 and i < self.max_oder_books_fetch_attempts: i += 1 try: ob_array = self.exchange.get_order_books_async(symbols, 10) except Exception as e: self.log(self.LOG_ERROR, "Error while fetching order books exchange_id:{} session_uuid:{}" " fetch_num:{}:". format(self.exchange_id, self.session_uuid, self.fetch_number)) self.log(self.LOG_ERROR, "Exception: {}".format(type(e).__name__)) self.log(self.LOG_ERROR, "Exception body:", e.args) self.log(self.LOG_ERROR, "Sleeping before next request") time.sleep(self.request_sleep) if len(ob_array) < 3: raise Exception("Could not fetch all order books. Fetched: {}".format(len(ob_array))) order_books = dict() for ob in ob_array: order_books[ob["symbol"]] = OrderBook(ob["symbol"], ob["asks"], ob["bids"]) if self.offline and "from_ticker" in ob and ob["from_ticker"]: self.log(self.LOG_INFO, "Order Book for {} created from TICKER".format(ob["symbol"])) return order_books def fetch_tickers(self): self.fetch_number += 1 self.tickers = self.exchange.fetch_tickers() return self.tickers def check_good_triangles(self, triangle: dict): threshold = self.threshold ignore_currencies = self.ignore_currency if ignore_currencies is None: if triangle['result'] is not None and triangle['result'] > threshold: return True if ignore_currencies is not None: if triangle["cur1"] not in ignore_currencies \ and triangle["cur2"] not in ignore_currencies \ and triangle["cur3"] not in ignore_currencies: if triangle['result'] is not None and triangle['result'] > threshold: return True return False def get_good_triangles(self, triangles_with_results=None): """ :return: sorted by result list of good triangles """ # tri_list = list(filter(lambda x: x['result'] > 0, self.tri_list)) tri_list = list() if triangles_with_results is not None: tri_list = triangles_with_results else: tri_list = self.tri_list tri_list = sorted(tri_list, key=lambda k: k['result'], reverse=True) # threshold = threshold if threshold is not None else self.threshold tri_list_good = list( filter(self.check_good_triangles, tri_list)) # self.tri_list_good = tri_list_good self.last_proceed_report = dict() self.last_proceed_report["best_result"] = tri_list[0] if triangles_with_results is not None: self.tri_list = tri_list_good return tri_list_good def start_amount_to_bid(self, working_triangle: dict, order_books: dict, force_best_tri=False, force_start_amount: float = 0.0, skip_order_books=False): """ Returns the amount to bid in first leg in accordance to parameters. :param working_triangle: working triangle dict, should contain following fields: result, ob_result, symbol1, symbol2, symbol3, leg{1,2,3}-order :param order_books: dict of order books, where keys are leg numbers and values are order_books objects for corresponding symbols. Ex: {1: order_books[working_triangle["symbol1"]], 2: order_books[working_triangle["symbol2"]], 3: order_books[working_triangle["symbol3"]]} :param force_best_tri: bot option :param force_start_amount: bot option :return: amount to bid or None in case of error """ if skip_order_books: working_triangle["ob_result"] = 100000 if not force_start_amount and not force_best_tri: bal_to_bid = self.max_balance_to_bid_from_thresholds(self.start_currency[0], self.balance, working_triangle["result"], working_triangle["ob_result"]) if bal_to_bid is None: return None elif force_best_tri and not force_start_amount: bal_to_bid = self.balance # we take max balances from initial thresholds bal_to_bid = self.max_balance_to_bid_from_thresholds(self.start_currency[0], self.balance, self.threshold, self.threshold_order_book) if force_start_amount: bal_to_bid = force_start_amount return bal_to_bid def finalize_start_amount(self, start_amount): """ check if need to restrict max bid to share_balance_to_bid :param start_amount: :return: """ if start_amount > self.balance * self.share_balance_to_bid: final_start_amount = self.balance * self.share_balance_to_bid self.log(self.LOG_INFO, "Start amount is greater than max allowed by share_balance_to_bid={}. Decrease to{} ".format( self.share_balance_to_bid, start_amount)) else: final_start_amount = start_amount return final_start_amount # getting the maximum amount to bid for the first trade from the settings and order book result def restrict_amount_to_bid_from_order_book(self, start_amount, working_triangle, order_books, force_best_tri=False): if force_best_tri: order_book_threshold = 0 # for filtering the results on order_book_threshold else: order_book_threshold = self.threshold_order_book / (1 - self.commission) ** 3 self.log(self.LOG_INFO, "Getting start bid with from the Order book (order_book_threshold={})....".format( order_book_threshold)) max_possible = ta.get_maximum_start_amount(self.exchange, working_triangle, {1: order_books[working_triangle["symbol1"]], 2: order_books[working_triangle["symbol2"]], 3: order_books[working_triangle["symbol3"]]}, start_amount, 10, self.min_amounts[self.start_currency[0]], order_book_threshold) if max_possible is None: self.log(self.LOG_INFO, "No good results when getting maximum bid from OB.... Skipping") # working_triangle["status"] = "OB STOP" return None, None, None start_amount = max_possible["amount"] expected_result = max_possible["result"] ob_result = expected_result * ((1 - self.commission) ** 3) if start_amount > self.min_amounts[self.start_currency[0]]: self.log(self.LOG_INFO, "Increase start amount: {} (was {})".format(start_amount, self.min_amounts[self.start_currency[0]])) return expected_result, ob_result, start_amount def log_order_create(self, order_manager: OrderManagerFok): self.log(self.LOG_INFO, "Tick {}: Order {} created. Filled dest curr:{} / {} ".format( order_manager.order.update_requests_count, order_manager.order.id, order_manager.order.filled_dest_amount, order_manager.order.amount_dest)) # here is the sleep between updates is implemented! needed to be fixed def log_order_update(self, order: TradeOrder): self.log(self.LOG_INFO, "Order {} update req# {}/{} (to timer {}). Status:{}. Filled amount:{} / {} ".format( order.id, order.update_requests_count, self.order_update_total_requests, self.order_update_requests_for_time_out, order.status, order.filled, order.amount)) # now_order = datetime.now() # # if order.status == "open" and \ # order.update_requests_count >= self.order_update_requests_for_time_out: # # if order.update_requests_count >= self.order_update_total_requests: # self.log(self.LOG_INFO, "...last update will no sleep") # # else: # self.log(self.LOG_INFO, "...reached the number of order updates for timeout") # # if (now_order - self.last_update_time).total_seconds() < self.order_update_time_out: # self.log(self.LOG_INFO, "...sleeping while order update for {}".format(self.order_update_time_out)) # time.sleep(self.order_update_time_out) # # self.last_update_time = datetime.now() def log_on_order_update_error(self, order_manager, exception): self.log(self.LOG_ERROR, "Error updating order_id: {}".format(order_manager.order.id)) self.log(self.LOG_ERROR, "Exception: {}".format(type(exception).__name__)) for ll in exception.args: self.log(self.LOG_ERROR, type(exception).__name__ + ll) return True def assign_updates_functions_for_order_manager(self): OrderManagerFok.on_order_create = lambda _order_manager: self.log_order_create(_order_manager) OrderManagerFok.on_order_update = lambda _order_manager: self.log_order_update(_order_manager) OrderManagerFok.on_order_update_error = lambda _order_manager, _exception: self.log_on_order_update_error( _order_manager, _exception) def do_trade(self, leg, symbol, start_currency, dest_currency, amount, side, price, amount_cancel_threshold): """ proceed with the trade and return TradeOrder :param symbol: str :param start_currency: str :param dest_currency: str :param amount: float :param side: str :param price: float :return: TradeOrder """ _amount_cancel_threshold = copy.copy(amount_cancel_threshold) self.log(self.LOG_INFO, "Amount cancel threshold {}. Price {}".format(_amount_cancel_threshold, price)) order_amount = core.base_amount_for_target_currency(start_currency, amount, symbol, price) if order_amount <= _amount_cancel_threshold: _amount_cancel_threshold = 0 self.log(self.LOG_INFO, "Order amount {} less cancel threshold {}. CT will be 0".format( order_amount, _amount_cancel_threshold)) # order = TradeOrder.create_limit_order_from_start_amount(symbol, start_currency, amount, dest_currency, price) if self.cancel_price_threshold == 0.0: self.log(self.LOG_INFO, "Proceeding order without threshold") order = FokOrder.create_from_start_amount(symbol, start_currency, amount, dest_currency, price, max_order_updates=self.order_update_total_requests, time_to_cancel=utils.dict_value_from_path( self.orders_settings, [str(leg), "time_to_cancel"]), cancel_threshold=_amount_cancel_threshold) else: self.log(self.LOG_INFO, "Proceeding order with taker price threshold from ticker{}".format( self.cancel_price_threshold)) order = FokThresholdTakerPriceOrder.create_from_start_amount( symbol, start_currency, amount, dest_currency, price, max_order_updates=self.order_update_total_requests, taker_price_threshold=self.cancel_price_threshold, threshold_check_after_updates=self.order_update_requests_for_time_out - 2, time_to_cancel=utils.dict_value_from_path(self.orders_settings, [str(leg), "time_to_cancel"]), cancel_threshold=_amount_cancel_threshold) trade_order = copy.deepcopy(order.get_active_order()) trade_order.tags = "" # if self.offline: # o = self.exchange.create_order_offline_data(order, 10) # self.exchange._offline_order = copy.copy(o) # self.exchange._offline_trades = copy.copy(o["trades"]) # self.exchange._offline_order_update_index = 0 # self.exchange._offline_order_cancelled = False # cancel_threshold = self.min_order_amount(symbol, price) # order_manager = OrderManagerFok(order, None, updates_to_kill=self.order_update_total_requests, # max_cancel_attempts=self.order_update_total_requests, # max_order_update_attempts=self.max_order_update_attempts, # request_sleep=self.request_sleep) self.order_manager.add_order(order) while len(self.order_manager.get_open_orders()) > 0: active_trade_order = self.order_manager.get_open_orders()[0].get_active_order() # self.log_order_update(active_trade_order) print("Order {} seconds from start# {}. Status:{}. Filled amount:{} / {} ".format( order.id, time.time() - order.timestamp, order.status, order.filled, order.amount)) if active_trade_order.update_requests_count <= self.order_update_requests_for_time_out: self.log(self.LOG_INFO, "number of order updates less than order_update_requests_for_time_out") else: # check timer sleep_time = self.exchange.requests_throttle.sleep_time() print("Current period time {cur_period_time}/{period}. " "Requests in curtent period {cur_requests_in_period}/{req_in_per}. ".format( cur_period_time=self.exchange.requests_throttle._current_period_time, period=self.exchange.requests_throttle.period, cur_requests_in_period=self.exchange.requests_throttle.total_requests_current_period, req_in_per=self.exchange.requests_throttle.requests_per_period)) print("Sleeping for {}s".format(sleep_time)) time.sleep(sleep_time) self.order_manager.proceed_orders() try: trade_order = self.order_manager.get_closed_orders()[0].orders_history[0] if self.order_manager.get_closed_orders()[0].tags is not None and \ len(self.order_manager.get_closed_orders()[0].tags) > 0: trade_order.tags = " ".join(self.order_manager.get_closed_orders()[0].tags) else: trade_order.tags = "" except Exception as exception: self.log(self.LOG_ERROR, "Error extracting trade order!") self.log(self.LOG_ERROR, "Exception: {}".format(type(exception).__name__)) for ll in exception.args: self.log(self.LOG_ERROR, type(exception).__name__ + ll) # trade_order = None return trade_order def get_trade_results(self, order: TradeOrder): results = list() i = 0 while bool(results) is not True and i < self.max_trades_updates: self.log(self.LOG_INFO, "getting trades #{}".format(i)) try: results = self.exchange.get_trades_results(order) except Exception as e: self.log(self.LOG_ERROR, type(e).__name__) self.log(self.LOG_ERROR, e.args) self.log(self.LOG_INFO, "retrying to get trades... after sleep for {}s".format(self.request_sleep)) time.sleep(self.request_sleep) self.log(self.LOG_INFO, "sleep done") i += 1 return results def order2_best_recovery_start_amount(self, filled_start_currency_amount, order2_amount, order2_filled): res = 0.0 if order2_amount > 0: res = filled_start_currency_amount - (order2_filled / order2_amount) * filled_start_currency_amount res = res * self.recover_factor return res def order3_best_recovery_start_amount(self, filled_start_currency_amount, order2_amount, order2_filled, order3_amoumt, order3_filled): res = 0.0 if order2_amount > 0 and order3_amoumt > 0: res = filled_start_currency_amount * (order2_filled / order2_amount) * (1 - (order3_filled / order3_amoumt)) res = res * self.recover_factor return res def create_recovery_data(self, deal_uuid, start_cur: str, dest_cur: str, start_amount: float, best_dest_amount: float, leg: int) -> dict: recovery_dict = dict() recovery_dict["deal-uuid"] = deal_uuid recovery_dict["symbol"] = core.get_symbol(start_cur, dest_cur, self.markets) recovery_dict["start_cur"] = start_cur recovery_dict["dest_cur"] = dest_cur recovery_dict["start_amount"] = start_amount recovery_dict["best_dest_amount"] = best_dest_amount recovery_dict["leg"] = leg # order leg to recover from recovery_dict["timestamp"] = time.time() return recovery_dict def print_recovery_data(self, recovery_data): self.log(self.LOG_INFO, "leg {}".format(recovery_data["leg"])) self.log(self.LOG_INFO, "Recover leg {}: {} {} -> {} {} ". format(recovery_data["leg"], recovery_data["start_cur"], recovery_data["start_amount"], recovery_data["dest_cur"], recovery_data["best_dest_amount"])) def send_recovery_request(self, recovery_data: dict): self.log(self.LOG_INFO, "Sending recovery request...") try: resp = rest_server.rest_call_json( "{}:{}/order/".format(self.recovery_server["host"], self.recovery_server["port"]), recovery_data, "PUT") except Exception as e: self.log(self.LOG_ERROR, "Could not send recovery request") self.log(self.LOG_ERROR, "Exception: {}".format(type(e).__name__)) self.log(self.LOG_ERROR, "Exception body:", e.args) return False self.log(self.LOG_INFO, "Response: {}".format(resp)) return resp def get_report_fields(self): report_fields = list([ "server-id", "exchange-id", "session-uuid", "fetch-number", "deal-uuid", "dbg", "triangle", "status", "start-qty", "start-filled", "finish-qty", "result-fact-diff", "for-recover", "leg2-recover-amount", "leg2-recover-target", "leg3-recover-amount", "leg3-recover-target", "result", "ob_result", "leg1-order-result", "leg1-filled", "leg1-price-fact", "leg1-ob-price", "leg1-price", "leg1-fee", "leg2-order-result", "leg2-filled", "leg2-price-fact", "leg2-ob-price", "leg2-price", "leg2-fee", "leg3-order-result", "leg3-filled", "leg3-price-fact", "leg3-ob-price", "leg3-price", "leg3-fee", "leg1-order-updates", "leg2-order-updates", "leg3-order-updates", "order1-internal_id", "order2-internal_id", "order3-internal_id", "cur1", "cur2", "cur3", "leg1-order", 'leg2-order', 'leg3-order', 'symbol1', 'symbol2', 'symbol3', "time_fetch", "time_proceed", "time_from_start", "errors", "time_after_deals", "balance", "timestamp", "timestamp_finish", "leg1-tags", "leg2-tags", "leg3-tags"]) for a in self.CONFIG_PARAMETERS: report_fields.append("_config_" + a) return report_fields def get_config_report(self): """ collect config report fields where keys are set in CONFIG_PARAMETERS :return: """ report = collections.OrderedDict() for f in self.CONFIG_PARAMETERS: if hasattr(self, f): report["_config_{}".format(f)] = getattr(self, f) report["_config_fullthrottle"] = self.fullthrottle["enabled"] return report def get_deal_report(self, working_triangle: dict, recovery_data, order1: TradeOrder, order2: TradeOrder = None, order3: TradeOrder = None, price1=None, price2=None, price3=None): report_fields = self.get_report_fields() report = collections.OrderedDict() wt = copy.copy(working_triangle) # adding report data which are not in working triangle try: timer_report = self.timer.timestamps() wt["timestamp"] = timer_report["time_from_start"] wt["timestamp_finish"] = timer_report["time_after_deals"] finally: pass wt["server-id"] = self.server_id wt["exchange-id"] = self.exchange_id wt["dbg"] = self.debug wt["live"] = self.force_best_tri wt["session-uuid"] = self.session_uuid wt["errors"] = self.errors wt["fetch_number"] = self.fetch_number wt["balance"] = self.balance wt["start-qty"] = float(order1.amount_start) if order1 is not None else 0.0 wt["start-filled"] = float(order1.filled_start_amount) if order1 is not None else 0.0 wt["status"] = "InRecovery" if len(recovery_data) > 0 else working_triangle["status"] wt["leg1-order-status"] = order1.status if order1 is not None else None wt["leg2-order-status"] = order2.status if order2 is not None else None wt["leg3-order-status"] = order3.status if order3 is not None else None wt["leg1-tags"] = order1.tags if order1 is not None else None wt["leg2-tags"] = order2.tags if order2 is not None else None wt["leg3-tags"] = order3.tags if order3 is not None else None wt["order1-internal_id"] = order1.internal_id if order1 is not None else None wt["order2-internal_id"] = order2.internal_id if order2 is not None else None wt["order3-internal_id"] = order3.internal_id if order3 is not None else None wt["leg1-filled"] = order1.filled / order1.amount if order1 is not None and order1.amount != 0 else 0.0 wt["leg2-filled"] = order2.filled / order2.amount if order2 is not None and order2.amount != 0 else 0.0 wt["leg3-filled"] = order3.filled / order3.amount if order3 is not None and order3.amount != 0 else 0.0 wt["leg1-order-updates"] = order1.update_requests_count if order1 is not None else None wt["leg2-order-updates"] = order2.update_requests_count if order2 is not None else None wt["leg3-order-updates"] = order3.update_requests_count if order3 is not None else None wt["leg1-price-fact"] = order1.cost / order1.filled if order1 is not None and order1.filled != 0 else 0.0 wt["leg2-price-fact"] = order2.cost / order2.filled if order2 is not None and order2.filled != 0 else 0.0 wt["leg3-price-fact"] = order3.cost / order3.filled if order3 is not None and order3.filled != 0 else 0.0 wt["leg1-ob-price"] = price1 wt["leg2-ob-price"] = price2 wt["leg3-ob-price"] = price3 wt["leg1-fee"], wt["leg2-fee"], wt["leg3-fee"] = (order.fees[order.dest_currency]["amount"] if order is not None and order.dest_currency in order.fees else None for order in (order1, order2, order3)) if order3 is not None and order1 is not None and order3.filled_dest_amount > 0: wt["finish-qty"] = order3.filled_dest_amount - order3.fees[order3.dest_currency]["amount"] else: wt["finish-qty"] = 0.0 wt["result-fact-diff"] = float(wt["finish-qty"] - order1.filled_start_amount) if order1 is not None else 0.0 wt["result-fact"] = wt["finish-qty"] / order1.filled_start_amount if order1 is not None and \ order1.filled_start_amount != 0 else 0.0 total_recover_amount = float(0.0) # collect recovery data for r in recovery_data: total_recover_amount += r["best_dest_amount"] wt["leg{}-recover-amount".format(r["leg"])] = r["start_amount"] # amount of CUR in leg sent to recover wt["leg{}-recover-target".format(r["leg"])] = r["best_dest_amount"] # amount of target CUR1 for recover wt["for-recover"] = total_recover_amount # collect timer data time_report = self.timer.results_dict() for f in time_report: report_fields.append(f) wt[f] = time_report[f] # copy working triangle data into report for f in report_fields: if f in wt: report[f] = wt[f] # insert config report report.update(self.get_config_report()) return report def sqla_report_from_report_dict(self, report: dict): report_sqla = DealReport( timestamp=datetime.fromtimestamp(report["timestamp_finish"], tz=pytz.timezone('UTC')), timestamp_start=datetime.fromtimestamp(report["timestamp"], tz=pytz.timezone('UTC')), exchange=report["exchange-id"], instance=report["server-id"], server=report["server-id"], deal_type="triarb", deal_uuid=report["deal-uuid"], status=report["status"], currency=report["cur1"], start_amount=report["start-filled"], result_amount=report["finish-qty"], gross_profit=report["result-fact-diff"], net_profit=0.0, config=self.get_config_report(), deal_data=report ) return report_sqla def get_orders_dict_report(self, order1: TradeOrder, order2: TradeOrder = None, order3: TradeOrder = None): report = list() if order1 is not None: report.append(order1.report()) if order2 is not None: report.append(order2.report()) if order3 is not None: report.append(order3.report()) return report def sqla_orders_report(self, deal_uuid, order1: TradeOrder, order2: TradeOrder = None, order3: TradeOrder = None): timestamp_now = datetime.now(tz=pytz.timezone("UTC")) report = list() if order1 is not None: report.append(TradeOrderReport.from_trade_order(order1, timestamp_now, deal_uuid=deal_uuid, supplementary={"leg": 1, "deal_state": "main"})) if order2 is not None: report.append(TradeOrderReport.from_trade_order(order2, timestamp_now, deal_uuid=deal_uuid, supplementary={"leg": 2, "deal_state": "main"})) if order3 is not None: report.append(TradeOrderReport.from_trade_order(order3, timestamp_now, deal_uuid=deal_uuid, supplementary={"leg": 3, "deal_state": "main"})) return report def log_report(self, report): for r in self.get_report_fields(): self.log(self.LOG_INFO, "{} = {}".format(r, report[r] if r in report else "None")) def send_remote_report(self, report, orders_dict_report=None, sqla_orders_report: list = None): if self.sqla is not None and "enabled" in self.sqla and self.sqla["enabled"]: try: self.log(self.LOG_INFO, "Sending report to sqla....") if self.sqla_reporter.session is None: self.sqla_reporter.new_session() self.log(self.LOG_INFO, ".. new SQL Session created ") deal_report = self.sqla_report_from_report_dict(report) self.sqla_reporter.session.add(deal_report) for o in sqla_orders_report: self.sqla_reporter.session.add(o) self.sqla_reporter.session.commit() self.log(self.LOG_INFO, ".... done") except Exception as e: self.log(self.LOG_ERROR, "SQLA: Error sending report") self.log(self.LOG_ERROR, "Exception: {}".format(type(e).__name__)) self.log(self.LOG_ERROR, "Exception body:", e.args) print("Exception in user code:") print("-" * 60) traceback.print_exc(file=sys.stdout) print("-" * 60) self.sqla_reporter.session.rollback() def reload_balance(self, result_fact_diff: float = 0.0): prev_balance = copy.copy(self.balance) i = 0 while i < self.max_trades_updates and not self.offline: try: self.load_balance() self.log(self.LOG_INFO, "Updating balance.. {}/{}".format(i, self.max_trades_updates)) if self.balance > (prev_balance + result_fact_diff) * 0.9: self.log(self.LOG_INFO, "Balance Updated: {} ( was: {})".format(self.balance, prev_balance)) return True except Exception as e: self.log(self.LOG_ERROR, "Error while fetching balance {}".format(self.exchange_id)) self.log(self.LOG_ERROR, "Exception: {}".format(type(e).__name__)) self.log(self.LOG_ERROR, "Exception body:", e.args) self.log(self.LOG_INFO, "Sleeping for {}s".format(self.request_sleep)) time.sleep(self.request_sleep) i += 1 self.log(self.LOG_INFO, "Balance is not updated...") return False def save_order_books(self, deal_uuid: str, order_books): ob_file_header = ["deal-uuid", "symbol", "ask", "ask-qty", "bid", "bid-qty"] order_book_storage = deal_uuid + "_ob" # deal_prefix = list([deal_uuid]) storage = DataStorage("_{}/".format(self.exchange_id)) storage.register(order_book_storage, ob_file_header) table_to_save = list() for i in order_books: ob_rows = order_books[i].csv_rows(10, True) table_to_save.extend(list(map(lambda x: [deal_uuid] + [order_books[i].symbol] + x, ob_rows))) storage.save_all(order_book_storage, table_to_save) return True def save_single_deal_csv(self, deal): write_header = False file_deals = "_{}/{}.csv".format(self.exchange_id, deal["deal-uuid"]) if not os.path.isfile(file_deals): write_header = True with open(file_deals, 'a', newline='') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=self.get_report_fields(), extrasaction="ignore") if write_header: writer.writeheader() writer.writerow(deal) @staticmethod def check_time_to_launch(start_at, timestamp): seconds_from_timestamp = datetime.fromtimestamp(timestamp).second if seconds_from_timestamp in start_at: return True return False def update_state(self, current_state: str, timestamp: float, fullthrottle_enabled: bool, start_at: list, lap_time: float, prev_throttle_start_timestamp: float): self.state = current_state if fullthrottle_enabled: # self.state = "wait" # timestamp_int_str = "{:.0f}".format(timestamp // 1) # str int part of timestamp # len_of_start_at = len(start_at[0]) if current_state == "wait" and self.check_time_to_launch(start_at, timestamp) and \ (timestamp - prev_throttle_start_timestamp > lap_time or prev_throttle_start_timestamp == 0): self.state = "go" # return self.state if current_state == "go" and timestamp - prev_throttle_start_timestamp > lap_time: self.state = "wait" # return self.state else: self.state = "go" # return self.state return self.state @staticmethod def print_logo(product=""): print('TTTTTTTTTT K K GGGGG') print(' T K K G') print(' T KKKK G') print(' T K K G GG') print(' T K K G G') print(' T K K GGGGG') print('-' * 36) print(' %s ' % product) print('-' * 36)
def test_recovery(self): ex = ccxtExchangeWrapper.load_from_id("binance") #type: ccxtExchangeWrapper ex.set_offline_mode("test_data/markets.json", "test_data/tickers_maker.csv") ex.load_markets() ex.fetch_tickers() tickers = ex.fetch_tickers() # second fetch contains good triangle om = ActionOrderManager(ex) om.offline_order_updates = 10 good_triangle = self.good_triangle maker = SingleTriArbMakerDeal(currency1=good_triangle["cur1"], currency2=good_triangle["cur2"], currency3=good_triangle["cur3"], price1=good_triangle["leg1-price"], price2=good_triangle["leg2-price"], price3=good_triangle["leg3-price"], start_amount=0.01, min_amount_currency1=0.003, symbol1=good_triangle["symbol1"], symbol2=good_triangle["symbol2"], symbol3=good_triangle["symbol3"], commission=0.00075, commission_maker=0.0006, threshold=1.001, max_order1_updates=2000, max_order2_updates=2000, max_order3_updates=2000, recover_factor_order2=1.2, recover_factor_order3=1.3, uuid="test" ) maker.update_state(tickers) om.add_order(maker.order1) while len(om.get_open_orders()) > 0: # !!!! # first we proceed the orders and we update the deal manager with new updated orders om.proceed_orders() maker.update_state(tickers) om.add_order(maker.order2) om.proceed_orders() # this will create order while maker.order2.get_active_order().update_requests_count < 6: om.proceed_orders() # this will fill order once for 1/10 of amount maker.update_state(tickers) print("") print("Force Cancel") print("") # let's force to close the order maker.order2.force_close() om.proceed_orders() maker.update_state(tickers) self.assertEqual("closed", maker.order2.status) self.assertLess(maker.order2.filled, maker.order2.amount) self.assertLess(0, maker.leg2_recovery_amount) self.assertLess(0, maker.leg2_recovery_target) self.assertEqual(maker.leg2_recovery_amount, maker.order2.start_amount - maker.order2.filled_start_amount) self.assertAlmostEqual(0.5 * maker.order2.amount, maker.leg2_recovery_amount, 6) self.assertAlmostEqual(1/2, maker.order2.filled / maker.order2.amount, 6) self.assertAlmostEqual(0.005*1.2, maker.leg2_recovery_target, 6) # proceed to leg3 self.assertEqual("order3_create", maker.state) om.add_order(maker.order3) while maker.order3.get_active_order().update_requests_count < 6: om.proceed_orders() # this will fill order once for 1/10 of amount maker.update_state(tickers) # let's force to close the order maker.order3.force_close() om.proceed_orders() maker.update_state(tickers) self.assertEqual(maker.leg3_recovery_amount, maker.order3.start_amount - maker.order3.filled_start_amount) self.assertAlmostEqual(1 / 2, maker.order3.filled / maker.order3.amount, 6) self.assertAlmostEqual(0.0025*1.3, maker.leg3_recovery_target, 6) self.assertAlmostEqual(0.5 * maker.order3.amount, maker.leg3_recovery_amount, 6) self.assertEqual("finished", maker.state) self.assertEqual("InRecovery", maker.status) self.assertEqual(0.01, maker.filled_start_amount) self.assertAlmostEqual((0.01/4) * good_triangle["result"], maker.result_amount, 4) self.assertAlmostEqual((0.01 / 4) * good_triangle["result"] - 0.01, maker.gross_profit, 4)
def test_run_ok(self): ex = ccxtExchangeWrapper.load_from_id("binance") # type: ccxtExchangeWrapper ex.set_offline_mode("test_data/markets.json", "test_data/tickers_maker.csv") ex.load_markets() ex.fetch_tickers() tickers = ex.fetch_tickers() # second fetch contains good triangle om = ActionOrderManager(ex) good_triangle = self.good_triangle maker = SingleTriArbMakerDeal(currency1=good_triangle["cur1"], currency2=good_triangle["cur2"], currency3=good_triangle["cur3"], price1=good_triangle["leg1-price"], price2=good_triangle["leg2-price"], price3=good_triangle["leg3-price"], start_amount=0.01, min_amount_currency1=0.003, symbol1=good_triangle["symbol1"], symbol2=good_triangle["symbol2"], symbol3=good_triangle["symbol3"], commission=0.00075, commission_maker=0.0006, threshold=1.001, max_order1_updates=2000, max_order2_updates=2000, max_order3_updates=2000, ) self.assertEqual("new", maker.state) maker.update_state(tickers) self.assertEqual("order1_create", maker.state) self.assertEqual("TRX/BTC", maker.order1.symbol) self.assertEqual(good_triangle["leg1-price"], maker.order1.price) self.assertEqual("buy", maker.order1.side) om.add_order(maker.order1) self.assertEqual("open", maker.order1.status) om.proceed_orders() self.assertEqual("open", maker.order1.status) maker.update_state(tickers) self.assertEqual("order1", maker.state) while len(om.get_open_orders()) > 0: # !!!! # first we proceed the orders and we update the deal manager with new updated orders om.proceed_orders() maker.update_state(tickers) # order1 is closed # second order creation phase self.assertEqual("order2_create", maker.state) # adding order2 and proceed om.add_order(maker.order2) om.proceed_orders() maker.update_state(tickers) self.assertEqual("order2", maker.state) while len(om.get_open_orders()) > 0: om.proceed_orders() maker.update_state(tickers) self.assertEqual("order3_create", maker.state) self.assertEqual(0, maker.leg2_recovery_amount) self.assertEqual(0, maker.leg2_recovery_target) om.add_order(maker.order3) om.proceed_orders() maker.update_state(tickers) self.assertEqual("order3", maker.state) while len(om.get_open_orders()) > 0: om.proceed_orders() maker.update_state(tickers) self.assertEqual("finished", maker.state) self.assertEqual(0, maker.leg3_recovery_amount) self.assertEqual(0, maker.leg3_recovery_target) self.assertEqual("OK", maker.status) self.assertEqual(0.01, maker.filled_start_amount) self.assertAlmostEqual((0.01) * good_triangle["result"], maker.result_amount, 4) self.assertAlmostEqual((0.01) * good_triangle["result"] - 0.01, maker.gross_profit, 4)