diff --git a/changelog.txt b/changelog.txt index 73b2ae5..3cf9052 100755 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +next: +. Default wait time now 0.5 seconds. +. Now the trader supports multiple safety orders at the same time. +. Removed endpoint /reload_safety_orders. +. Removed forcing orders when importing a trader. Maybe it will be reinstated at a later date. + 2025.08.19: . Improved log trimming. diff --git a/config_handler.py b/config_handler.py index 4243999..617d351 100644 --- a/config_handler.py +++ b/config_handler.py @@ -14,6 +14,7 @@ class ConfigHandler: "order_size": self.broker.get_default_order_size(), "no_of_safety_orders": 30, "max_short_safety_orders": 45, + "max_concurrent_safety_orders": 5, "safety_order_deviance": 2, "safety_order_scale": 0.0105, "dynamic_so_deviance": True, @@ -68,6 +69,9 @@ class ConfigHandler: def get_max_short_safety_orders(self): return self.config_dictionary["max_short_safety_orders"] + def get_max_concurrent_safety_orders(self): + return self.config_dictionary["max_concurrent_safety_orders"] + def get_safety_order_deviance(self): return self.config_dictionary["safety_order_deviance"] @@ -173,6 +177,13 @@ class ConfigHandler: self.config_dictionary["max_short_safety_orders"] = max_short_safety_orders return 0 + def set_max_concurrent_safety_orders(self, max_concurrent_safety_orders: int): + # if not isinstance(max_concurrent_safety_orders, int): + # self.broker.logger.log_this(f"Max concurrent safety orders provided is not an integer",1,self.get_pair()) + # return 1 + self.config_dictionary["max_concurrent_safety_orders"] = max_concurrent_safety_orders + return 0 + def set_safety_order_deviance(self, safety_order_deviance: int): # if not isinstance(safety_order_deviance, int): # self.broker.logger.log_this(f"Safety order deviance provided is not an integer",1,self.get_pair()) diff --git a/exchange_wrapper.py b/exchange_wrapper.py index 09b5302..538eb0f 100755 --- a/exchange_wrapper.py +++ b/exchange_wrapper.py @@ -14,7 +14,7 @@ class Broker: self.broker_config = broker_config self.exchange = exchange self.last_price = 0 - self.wait_time = 1 #Default wait time for API breathing room + self.wait_time = .5 #Default wait time for API breathing room self.cooldown_multiplier = 2 #Default cooldown multiplier value if "cooldown_multiplier" in self.broker_config: self.cooldown_multiplier = self.broker_config["cooldown_multiplier"] @@ -581,7 +581,7 @@ class Broker: return [] - def get_closed_orders(self,no_retries=False): #It should return a list of all opened orders + def get_closed_orders(self,pair=None,no_retries=False): #It should return a list of all opened orders ''' Returns a list of all the open orders on the exchange @@ -592,7 +592,7 @@ class Broker: retries = self.retries while retries>0: try: - return self.exchange.fetch_closed_orders() + return self.exchange.fetch_closed_orders(pair) except Exception as e: self.logger.log_this(f"Exception in get_closed_orders: {e}",1) if no_retries: diff --git a/main.py b/main.py index 65eb396..e524087 100644 --- a/main.py +++ b/main.py @@ -39,6 +39,7 @@ worker_threads_overprovisioning = 3 #Number of worker threads to create over #Only use 0 if you are sure that you won't be adding any. executor = None +#Shutdown handler def shutdown_handler(signum, _): broker.logger.log_this(f"Received signal {signum}, shutting down as gracefully as possible...", 2) if executor: @@ -86,7 +87,7 @@ def time_to_unix(year: str, month: str, day: str) -> int: return 0 -def import_instance(base: str, quote: str, forced_tp_id = None, forced_so_id = None) -> int: +def import_instance(base: str, quote: str) -> int: ''' Imports an previously running trader instance from the status file. @@ -98,7 +99,7 @@ def import_instance(base: str, quote: str, forced_tp_id = None, forced_so_id = N int: 0 if successful ''' broker.logger.log_this(f"Importing {base}/{quote}") - instances_to_add.append(trader.trader(broker,f"{base}/{quote}",is_import=True,forced_tp_id=forced_tp_id,forced_so_id=forced_so_id)) + instances_to_add.append(trader.trader(broker,f"{base}/{quote}",is_import=True)) if f"{base}{quote}" not in tickers: tickers.append(f"{base}{quote}") return 0 @@ -690,9 +691,7 @@ def import_pair(): data = request.json base = data["base"] quote = data["quote"] - forced_tp_id = data["forced_tp_id"] if "forced_tp_id" in data else None - forced_so_id = data["forced_so_id"] if "forced_so_id" in data else None - return unwrapped_import_pair(base,quote,forced_tp_id,forced_so_id) + return unwrapped_import_pair(base,quote) except Exception as e: print(e) return jsonify({'Error': 'Halp'}) @@ -1060,7 +1059,7 @@ def toggle_cleanup(): return jsonify({'Error': 'Halp'}) -@base_api.route("/toggle_autoswitch", methods=['POST']) #type:ignore +@base_api.route("/toggle_autoswitch", methods=['POST']) def toggle_autoswitch(): ''' POST request @@ -1084,8 +1083,8 @@ def toggle_autoswitch(): return jsonify({'Error': 'Halp'}) -@base_api.route("/toggle_liquidate_after_switch", methods=['POST']) #type:ignore -def toggle_liquidate_after_switch(): #type:ignore +@base_api.route("/toggle_liquidate_after_switch", methods=['POST']) +def toggle_liquidate_after_switch(): ''' POST request @@ -1108,7 +1107,7 @@ def toggle_liquidate_after_switch(): #type:ignore return jsonify({'Error': 'Halp'}) -@base_api.route("/toggle_check_old_long_price", methods=['POST'])#type:ignore +@base_api.route("/toggle_check_old_long_price", methods=['POST']) def toggle_check_old_long_price(): ''' POST request @@ -1338,30 +1337,7 @@ def reload_markets(): return unwrapped_reload_markets() -@base_api.route("/reload_safety_order", methods=['POST']) -def reload_safety_order(): - ''' - POST request - - Parameters: - None - ''' - - if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys: - return jsonify({'Error': 'API key invalid'}), 401 - try: - if request.json is None: - return jsonify({'Error': 'request.json is None'}) - data = request.json - base = data["base"] - quote = data["quote"] - return unwrapped_reload_safety_order(base,quote) - except Exception as e: - print(e) - return jsonify({'Error': 'Halp'}) - - -@base_api.route("/reload_trader_config", methods=['POST'])#type:ignore +@base_api.route("/reload_trader_config", methods=['POST']) def reload_trader_config(): ''' POST request @@ -1512,15 +1488,13 @@ def unwrapped_restart_pair(base,quote): return jsonify({"Error": "Halp"}) -def unwrapped_import_pair(base,quote,forced_tp_id = None, forced_so_id = None): +def unwrapped_import_pair(base,quote): ''' Imports a previously running pair Parameters: base (str): The base currency of the pair quote (str): The quote currency of the pair - forced_tp_id (str): The ID of the take profit order to use - forced_so_id (str): The ID of the stop order to use Returns: jsonified dictionary detailing the outcome of the operation. @@ -1528,7 +1502,7 @@ def unwrapped_import_pair(base,quote,forced_tp_id = None, forced_so_id = None): try: symbol = f"{base}/{quote}" - import_instance(base,quote,forced_tp_id,forced_so_id) + import_instance(base,quote) broker.add_pair_to_config(f"{base}{quote}") broker.rewrite_config_file() broker.logger.log_this(f"Done",2,symbol) @@ -2120,6 +2094,7 @@ def unwrapped_toggle_autoswitch(base,quote): broker.logger.log_this("Autoswitch turned ON",1,symbol) instance.config.set_autoswitch(True) return jsonify({"Success": "Autoswitch is now ON"}) + return jsonify({"Error": "Trader not running"}) except Exception as e: broker.logger.log_this(f"Exception while toggling autoswitch: {e}",1,symbol) return jsonify({"Error": "Halp"}) @@ -2148,6 +2123,7 @@ def unwrapped_toggle_liquidate_after_switch(base,quote): broker.logger.log_this("Liquidate after switch turned ON",1,symbol) instance.config.set_liquidate_after_switch(True) return jsonify({"Success": "Liquidate after switch is now ON"}) + return jsonify({"Error": "Trader not running"}) except Exception as e: broker.logger.log_this(f"Exception while toggling liquidate after switch: {e}",1,symbol) return jsonify({"Error": "Halp"}) @@ -2176,6 +2152,7 @@ def unwrapped_toggle_check_old_long_price(base,quote): broker.logger.log_this("Check ON",1,symbol) instance.config.set_check_old_long_price(True) return jsonify({"Success": "Old long price check turned ON"}) + return jsonify({"Error": "Trader not running"}) except Exception as e: broker.logger.log_this(f"Exception while toggling check_old_long_price: {e}",1,symbol) return jsonify({"Error": "Halp"}) @@ -2360,30 +2337,7 @@ def unwrapped_reload_markets(): return jsonify({"Success": "Markets reloaded successfully"}) except Exception as e: broker.logger.log_this(f"Exception while reloading markets: {e}",1) - return jsonify({"Error": "Markets couldn't be reloaded"}) - - -def unwrapped_reload_safety_order(base,quote): - ''' - Reloads the safety order of a trader. - - Parameters: - base (str): The base currency of the trader. - quote (str): The quote currency of the trader. - - Returns: - jsonify: A jsonified dictionary detailing the outcome of the operation. - ''' - try: - symbol = f"{base}/{quote}" - for trader in running_traders: - if trader.config.get_pair()==symbol: - trader.config.load_from_file() - return jsonify({"Success": "Safety order reloaded successfully"}) - return jsonify({"Error": "Trader not found"}) - except Exception as e: - broker.logger.log_this(f"Exception while reloading safety order: {e}",1,symbol) - return jsonify({"Error": "Safety order couldn't be reloaded"}) + return jsonify({"Error": "Markets couldn't be reloaded"}) def unwrapped_get_balance(coin): diff --git a/status_handler.py b/status_handler.py index d9d5d20..a192bd3 100644 --- a/status_handler.py +++ b/status_handler.py @@ -12,7 +12,7 @@ class StatusHandler: "pair": f"{base}/{quote}", "take_profit_order": broker.get_empty_order(), "take_profit_price": 0.0, - "safety_order": broker.get_empty_order(), + "safety_orders": [], "next_so_price": 0.0, "order_size": 0.0, "partial_profit": 0.0, @@ -58,8 +58,11 @@ class StatusHandler: def get_take_profit_price(self): return self.status_dictionary["take_profit_price"] - def get_safety_order(self): - return self.status_dictionary["safety_order"] + def get_safety_orders(self): + """ + Returns the list of open safety orders + """ + return self.status_dictionary["safety_orders"] def get_next_so_price(self): return self.status_dictionary["next_so_price"] @@ -181,8 +184,11 @@ class StatusHandler: self.status_dictionary["so_order_id"] = order_id return 0 - def set_safety_order(self, order): - self.status_dictionary["safety_order"] = order + def set_safety_orders(self, orders: list): + """ + Replaces the whole safety orders list + """ + self.status_dictionary["safety_orders"] = orders return 0 def set_next_so_price(self, price: float): @@ -381,6 +387,21 @@ class StatusHandler: self.status_dictionary["deal_order_history"] = deal_history return 0 + def add_safety_order(self, order): + """ + Appends a newly-created safety order to the internal list + """ + self.status_dictionary["safety_orders"].append(order) + return 0 + + def remove_safety_order_by_id(self, order_id: str): + """ + Removes an order from the list (mostly used when that order is filled or canceled) + """ + orders = self.get_safety_orders() + self.status_dictionary["safety_orders"] = [order for order in orders if order["id"] != order_id] + return 0 + def clear_deal_order_history(self): self.status_dictionary["deal_order_history"] = [] return 0 diff --git a/trader.py b/trader.py index 180c2f7..ec836d0 100755 --- a/trader.py +++ b/trader.py @@ -50,7 +50,7 @@ class trader: self.status.set_pause_reason("Initialization") if is_import: - self.load_imported_trader(forced_tp_order_id=forced_tp_id, forced_safety_order_id=forced_so_id) + self.load_imported_trader() return None # An alternative would be to set up a variable like self.is_initalized to false and finish the initialization here. @@ -104,15 +104,6 @@ class trader: def get_market_reload_period(self) -> float: return self.market_reload_period - - def reload_safety_order(self) -> int: - ''' - Reloads the safety order. - ''' - - self.status.set_safety_order(self.broker.get_order(self.status.get_safety_order()["id"],self.config.get_pair())) - return 0 - def start_trader(self) -> int: ''' @@ -124,7 +115,7 @@ class trader: self.status.set_so_amount(0) self.status.clear_deal_order_history() self.status.set_take_profit_order(self.broker.get_empty_order()) - self.status.set_safety_order(self.broker.get_empty_order()) + self.status.set_safety_orders([]) #Reloads the market new_market_data = self.broker.fetch_market(self.config.get_pair()) @@ -217,11 +208,9 @@ class trader: #Wait until the first order gets filled self.status.set_pause_reason("start_trader - waiting for the first order to get filled") while True: - #Wait a bit longer, to catch a bug: - #Sometimes the amount of base taken into account by the trader is lower than the amount bought, + #Wait a bit longer, sometimes a recently filled market order is not updated quickly enough. + # When that happens, the amount of base taken into account by the trader is lower than the amount bought, # which ends up misrepresenting the trade cost per unit of base, which causes the take profit price to skyrocket. - # Maybe is the first market order getting "closed" before is fully filled? - # Or is there an error later in the trader? time.sleep(self.broker.get_wait_time()) returned_order = self.broker.get_order(first_order["id"],self.config.get_pair()) if returned_order==self.broker.get_empty_order(): @@ -278,13 +267,15 @@ class trader: self.status.set_start_price(self.broker.price_to_precision(self.config.get_pair(),self.status.get_quote_spent()/self.status.get_base_bought())) self.status.set_safety_price_table(self.calculate_safety_prices(self.status.get_start_price(),self.config.get_no_of_safety_orders(),self.config.get_safety_order_deviance())) - # Send the first safety order - self.status.set_pause_reason("start_trader - sending safety order") - self.broker.logger.log_this("Sending safety order...",2,self.config.get_pair()) - if self.send_new_safety_order(self.status.get_order_size())==0: - self.broker.logger.log_this("Safety order sent",2,self.config.get_pair()) + # Send the initial batch of safety orders + self.status.set_pause_reason("start_trader - sending safety orders") + self.broker.logger.log_this("Sending safety orders...",2,self.config.get_pair()) + max_initial_safety_orders = min(self.config.get_max_concurrent_safety_orders(),self.config.get_no_of_safety_orders()) #To never send more than the max amount of safety orders + orders_placed = self.send_new_safety_order_batch(max_initial_safety_orders) + if orders_placed is not None: + self.broker.logger.log_this(f"{orders_placed}/{max_initial_safety_orders} safety orders placed",2,self.config.get_pair()) else: - self.broker.logger.log_this("Error sending safety order. Cancelling take profit order and aborting",1,self.config.get_pair()) + self.broker.logger.log_this("Error sending safety orders. Cancelling take profit order and aborting",1,self.config.get_pair()) self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.config.get_pair()) return 1 @@ -312,9 +303,8 @@ class trader: self.status.set_next_so_price(self.status.get_safety_price_table()[self.status.get_so_amount()]) except Exception as e: self.broker.logger.log_this(f"Is safety_price_table populated? Exception: {e} | Safety price table: {self.status.get_safety_price_table()} | Safety order index: {self.status.get_so_amount()}",1,self.config.get_pair()) - if self.status.get_safety_order() is not None and self.status.get_safety_order()["price"] is not None and self.status.get_safety_order()!=self.broker.get_empty_order(): - self.status.set_next_so_price(self.status.get_safety_order()["price"]) - + if self.status.get_safety_orders()!=[]: + self.status.set_next_so_price(self.status.get_safety_orders()[0]["price"]) self.status.set_is_paused(self.pause) self.status.set_is_short(self.config.get_is_short()) self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) @@ -519,9 +509,6 @@ class trader: if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None, can't switch to short",1,self.config.get_pair()) return 1 - if self.status.get_safety_order() is None: - self.broker.logger.log_this("Safety order is None, can't switch to short",1,self.config.get_pair()) - return 1 #Pauses trader self.pause = True @@ -623,10 +610,8 @@ class trader: #Cancel open orders try: - if self.status.get_safety_order() is not None: - self.broker.cancel_order(self.status.get_safety_order()["id"],self.config.get_pair()) - else: - self.broker.logger.log_this("Take profit order is None",1,self.config.get_pair()) + for order in self.status.get_safety_orders(): + self.broker.cancel_order(order["id"],self.config.get_pair()) except Exception as e: self.broker.logger.log_this(f"Error in cancel_order while cancelling safety order. Exception: {e}",1,self.config.get_pair()) try: @@ -666,7 +651,7 @@ class trader: self.status.set_fees_paid_in_quote(0) self.status.set_fees_paid_in_base(0) self.status.set_take_profit_order(self.broker.get_empty_order()) - self.status.set_safety_order(self.broker.get_empty_order()) + self.status.set_safety_orders([]) self.status.set_safety_price_table([]) self.status.set_so_amount(0) @@ -732,39 +717,33 @@ class trader: self.status.save_to_file(is_backup=True) self.restart = True return 1 - if self.status.get_safety_order() is None: - self.status.set_pause_reason(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.config.get_pair()} | Safety order is None")) - self.broker.logger.log_this("Error. Safety order is None",1,self.config.get_pair()) - self.status.set_safety_order(self.broker.get_empty_order()) #Save the order in history. if self.broker.get_follow_order_history(): self.status.update_deal_order_history(filled_order) - # Cancel the current safety order (first check if there is something to cancel) - already_counted = False - if self.status.get_safety_order()["id"]=="": - self.broker.logger.log_this("There is no safety order to cancel",2,self.config.get_pair()) - elif self.broker.cancel_order(self.status.get_safety_order()["id"],self.config.get_pair())==1: - self.broker.logger.log_this("Old safety order probably filled. Can't cancel.",1,self.config.get_pair()) - closed_order = self.broker.get_order(self.status.get_safety_order()["id"],self.config.get_pair()) - if closed_order!=self.broker.get_empty_order() and closed_order["status"]=="closed": - self.status.set_base_bought(self.status.get_base_bought() + closed_order["filled"]) - self.status.set_quote_spent(self.status.get_quote_spent() + closed_order["cost"]) - #Save the order - if self.broker.get_follow_order_history(): - self.status.update_deal_order_history(closed_order) - already_counted = True - - #IF NOT SHORT - Check if the SO was partially filled. If so, add the amounts to total_amount_of_base and total_amount_of_quote - if not self.config.get_is_short() and self.status.get_safety_order()["id"]!="" and not already_counted: - old_so_order = self.broker.get_order(self.status.get_safety_order()["id"],self.config.get_pair()) - if old_so_order["filled"]>0: - self.broker.logger.log_this(f"Old safety order is partially filled, ID: {old_so_order['id']}",1,self.config.get_pair()) - if self.broker.get_follow_order_history(): - self.status.update_deal_order_history(old_so_order) - self.status.set_base_bought(self.status.get_base_bought() + old_so_order["filled"] - self.parse_fees(old_so_order)[0]) - self.status.set_quote_spent(self.status.get_quote_spent() + old_so_order["cost"]) + #Cancel all the safety orders ASAP + for order in self.status.get_safety_orders(): + self.broker.cancel_order(order["id"]) + #Check if some safety orders were filled + for order in self.status.get_safety_orders(): + closed_order = self.broker.get_order(order["id"],self.config.get_pair()) + if closed_order["filled"]==0: + #If this order wasn't filled, it is safe to assume that no order coming after this one was. + break + #Sum the filled amounts + self.broker.logger.log_this(f"Old safety order is partially filled, ID: {closed_order['id']}",1,self.config.get_pair()) + self.status.set_base_bought(self.status.get_base_bought() + closed_order["filled"] - self.parse_fees(closed_order)[0]) + self.status.set_quote_spent(self.status.get_quote_spent() + closed_order["cost"]) + #Save the order + if self.broker.get_follow_order_history(): + self.status.update_deal_order_history(closed_order) + if closed_order["remaining"]!=0: + #If this order is not completely filled, it is safe to assume that no order coming after this one was partially filled. + break + #Now we can clear the safety order list + self.status.set_safety_orders([]) + if not self.broker.check_for_duplicate_profit_in_db(filled_order): self.status.set_pause_reason("calculating profit") @@ -836,59 +815,92 @@ class trader: self.broker.logger.log_this(self.trader_restart_errors[restart_trader],1,self.config.get_pair()) return restart_trader - - def new_so_routine(self, filled_order: dict, send_new_so: bool) -> int: - ''' - Handles all the bureaucracy prior and after sending a new safety order - :param filled_order: dict - :param send_new_so: bool - :return: 0 OK, 1 not enough funds, if can't cancel old TP, can't send new TP - ''' + + def send_new_safety_order_batch(self, amount: int): + """ + Sends a new safety order batch to the broker + :param amount: int - The amount of safety orders to send. + :return: The amount of orders succesfully sent. None if an error occurs. - #Let's do some type checking first + If the amount of orders returned is less than the amount expected, we should not try to send more safety orders. + """ + + if amount<1: + return 0 + orders_to_place = min(self.config.get_no_of_safety_orders()-self.status.get_so_amount(),amount) + if orders_to_place<1: + return 0 + orders_placed = 0 + for _ in range(orders_to_place): + so_size = self.gib_so_size(self.status.get_order_size(),self.status.get_so_amount()+1,self.config.get_safety_order_scale()) + if self.config.get_is_short(): + new_order = self.broker.new_limit_order(self.config.get_pair(),so_size,"sell",self.status.get_safety_price_table()[self.status.get_so_amount()+1]) + else: + new_order = self.broker.new_limit_order(self.config.get_pair(),so_size/self.status.get_safety_price_table()[self.status.get_so_amount()+1],"buy",self.status.get_safety_price_table()[self.status.get_so_amount()+1]) + if new_order==1: + self.broker.logger.log_this("Not enough balance to send a new safety order",1,self.config.get_pair()) + return orders_placed + elif new_order is None: + self.broker.logger.log_this("new_limit_order returned None",1,self.config.get_pair()) + return orders_placed + orders_placed+=1 + self.status.add_safety_order(new_order) + self.status.set_so_amount(self.status.get_so_amount()+1) + return orders_placed + + + def renew_tp_and_so_routine(self, filled_safety_orders: list): + ''' + Modifies the current take profit order and sends a new safety order + :return: 0 OK, 1 take profit order is None, 2 not enough funds, 3 can't cancel TP (filled?), 4 can't send new TP + ''' + safety_orders_to_remove_by_id = [] + + #Check if current TP order is valid if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None, can't send a new safety order",1,self.config.get_pair()) - return 1 - if self.status.get_safety_order() is None: #I don't think this is necessary - self.broker.logger.log_this("Safety order is None, can't send a new safety order",1,self.config.get_pair()) - return 1 - + return 1 + + #Pause the trader self.pause = True - self.status.set_pause_reason("new_so_routine") - - # Save the order + self.status.set_pause_reason("renew_tp_and_so_routine") + + #Save the order if self.broker.get_follow_order_history(): - self.status.update_deal_order_history(filled_order) + for item in filled_safety_orders: + self.status.update_deal_order_history(item) + + #Add the amount filled in the last safety orders to the totals + for order in filled_safety_orders: + safety_orders_to_remove_by_id.append(order["id"]) + new_fees_base,new_fees_quote = self.parse_fees(order) + self.status.set_fees_paid_in_quote(self.status.get_fees_paid_in_quote() + new_fees_quote) + self.status.set_base_bought(self.status.get_base_bought() + order["filled"] - new_fees_base) + self.status.set_quote_spent(self.status.get_quote_spent() + order["cost"]) + + #Remove the filled safety orders from the list + if safety_orders_to_remove_by_id!=[]: + new_order_list = [] + #Remove filled orders from the list + for order in self.status.get_safety_orders(): + if order["id"] not in safety_orders_to_remove_by_id: + new_order_list.append(order) + self.status.set_safety_orders(new_order_list) - # Add the amount filled in the last safety order to the totals - new_fees_base,new_fees_quote = self.parse_fees(filled_order) - self.status.set_fees_paid_in_quote(self.status.get_fees_paid_in_quote() + new_fees_quote) - self.status.set_base_bought(self.status.get_base_bought() + filled_order["filled"] - new_fees_base) - self.status.set_quote_spent(self.status.get_quote_spent() + filled_order["cost"]) - #Cooldown time.sleep(self.broker.get_wait_before_new_safety_order()) - # Send the new safety order. If all expected safety orders are filled, it assigns an empty order to self.status.get_safety_order() - if send_new_so: - self.broker.logger.log_this("Sending a new safety order",2,self.config.get_pair()) - if self.send_new_safety_order(self.status.get_order_size())==1: - error_string = "Problems sending the new safety order. Maybe not enough funds?" - self.broker.logger.log_this(error_string,1,self.config.get_pair()) - self.status.set_pause_reason(error_string) - return 1 - else: - self.status.set_safety_order(self.broker.get_empty_order()) - self.status.set_so_amount(self.status.get_so_amount()+1) - - # Cancel the old tp order + #Send new SO(s) + orders_sent = self.send_new_safety_order_batch(len(filled_safety_orders)) + + #Cancel old TP order if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.config.get_pair())==1: error_string = f"{self.config.get_pair()} | {self.status.get_take_profit_order()['id']} | Old TP order probably filled. Can't cancel. This trader should be restarted" self.broker.logger.log_this(f"Old take profit order is probably filled, can't cancel. This trader should be restarted. Order ID: {self.status.get_take_profit_order()['id']}",1,self.config.get_pair()) self.status.set_pause_reason(error_string) - return 2 - - # Check if the old tp order was partially filled. If so, update the previous two variables accordingly + return 2 + + #Check if old TP order was partially filled # TODO: This should also be taken into account for the profit calculation # Do the partial profit calculation and save it for later old_tp_order = self.broker.get_order(self.status.get_take_profit_order()["id"],self.config.get_pair()) @@ -903,29 +915,31 @@ class trader: # self.status.set_partial_profit(self.status.get_partial_profit()+old_tp_order["cost"]-(old_tp_order["filled"]*current_deal_price)-self.parse.fees(old_tp_order)[1]) # self.update_status(True) # - # Maybe here we shouldn't substract fees yet, but add them up to the check. - # self.status.set_base_bought(self.status.get_base_bought() - old_tp_order["filled"] - self.parse_fees(old_tp_order)[0]) self.status.set_quote_spent(self.status.get_quote_spent() - old_tp_order["cost"]) self.status.set_fees_paid_in_quote(self.status.get_fees_paid_in_quote() + self.parse_fees(old_tp_order)[1]) - #self.status.set_fees_paid_in_base(self.status.get_fees_paid_in_base() + self.parse_fees(old_tp_order)[0]) + #self.status.set_fees_paid_in_base(self.status.get_fees_paid_in_base() + self.parse_fees(old_tp_order)[0]) #Cooldown - time.sleep(self.broker.get_wait_time()) - - # Send the new take profit order + time.sleep(self.broker.get_wait_time()) + + #Send new TP order if self.send_new_tp_order()==1: error_string = "Problems sending the new take profit order" self.broker.logger.log_this("Problems sending the new take profit order",1,self.config.get_pair()) self.status.set_pause_reason(error_string) - return 3 - - # Update the status_dict and that's it + return 4 + + #Update status dict self.update_status(True) + + #Toggle the pause flag self.pause = False self.status.set_pause_reason("") + + #Done return 0 - + def check_old_long_price(self) -> int: ''' @@ -1024,18 +1038,15 @@ class trader: #Extract ids from order list open_orders_ids = [order["id"] for order in open_orders if order["symbol"]==self.config.get_pair()] - #Checks if the orders are valid + #Checks if the take profit order is valid if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None",1,self.config.get_pair()) return 1 - if self.status.get_safety_order() is None: - #Here, would it be wise to attempt to reload the safety order from the status dict? - self.broker.logger.log_this("Safety order is None",1,self.config.get_pair()) - self.status.set_safety_order(self.broker.get_empty_order()) - #return 1 if self.status.get_take_profit_order()["id"]=="": self.broker.logger.log_this(f"Take profit order missing. Stopping trader. No order ID was provided.",1,self.config.get_pair()) - self.broker.cancel_order(self.status.get_safety_order()["id"],self.config.get_pair()) + #Cancelling safety orders + for item in self.status.get_safety_orders(): + self.broker.cancel_order(item["id"],self.config.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True @@ -1051,8 +1062,9 @@ class trader: if tp_status["filled"]>0: return self.take_profit_routine(tp_status) self.broker.logger.log_this(f"Take profit order closed but not filled, 0 filled. Stopping trader. Order ID: {self.status.get_take_profit_order()['id']}",1,self.config.get_pair()) - #Cancelling safety order and stopping trader - self.broker.cancel_order(self.status.get_safety_order()["id"],self.config.get_pair()) + #Cancelling safety orders + for item in self.status.get_safety_orders(): + self.broker.cancel_order(item["id"],self.config.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True @@ -1073,46 +1085,52 @@ class trader: self.broker.logger.log_this(f"Take profit order search returned empty order. Order ID: {tp_status['id']}",1,self.config.get_pair()) return 1 - # Check if safety order is filled - if self.status.get_safety_order()["id"] not in open_orders_ids and self.status.get_so_amount()<=self.config.get_no_of_safety_orders(): - so_status = self.broker.get_order(self.status.get_safety_order()["id"],self.config.get_pair()) - tp_order_status = self.broker.get_order(self.status.get_take_profit_order()["id"],self.config.get_pair()) - - #Now we check 2 things: - #1. That the prior safety order status is indeed closed (or canceled) - #2. That the take profit order is still opened (if not, the deal must have closed, both orders closing is quite common in high variance scenarios) - if so_status["status"] in ["closed", "canceled", ""] and tp_order_status["status"]=="open": - #Switch to short if all safety orders are sent and autoswitch is enabled. - #May get into trouble if the trader is short of funds - if not self.config.get_is_short() and self.status.get_so_amount()==self.config.get_no_of_safety_orders() and self.config.get_autoswitch(): - self.switch_to_short() + # Check if any safety order is filled + filled_ids = [] + for order in self.status.get_safety_orders(): + if order["id"] not in open_orders_ids: + filled_ids.append(order["id"]) + + if filled_ids!=[]: + closed_orders = self.broker.get_closed_orders(self.config.get_pair()) + filled_orders = [item for item in closed_orders if item["id"] in filled_ids and item["status"]=="closed"] #maybe item["status"] in ["closed", "canceled", ""]? + renew_outcome = self.renew_tp_and_so_routine(filled_orders) + #0 OK, 1 take profit order is None, 2 not enough funds, 3 can't cancel TP (filled?), 4 can't send new TP + if renew_outcome==1: + self.broker.logger.log_this(f"Error in trader: TP order is None. Restart will be attempted. renew_tp_and_so_routine returned 1",0,self.config.get_pair()) + if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) - self.restart = True - return 0 - a = self.new_so_routine(so_status,self.status.get_so_amount()short) + #Commented out because i'm not sure where this should go + #if not self.config.get_is_short() and self.status.get_so_amount()==self.config.get_no_of_safety_orders() and self.config.get_autoswitch(): + # self.switch_to_short() + # self.status.save_to_file(is_backup=True) + # self.restart = True + # return 0 #Render status line(s) self.status.set_status_string(self.generate_status_strings()) @@ -1245,28 +1263,6 @@ class trader: return 1 - def send_new_safety_order(self, size: float) -> int: - ''' - Sends a new safety order to the exchange - ''' - so_size = self.gib_so_size(size,self.status.get_so_amount()+1,self.config.get_safety_order_scale()) #safety_order_scale: safety order growth factor - if self.config.get_is_short(): - new_order = self.broker.new_limit_order(self.config.get_pair(),so_size,"sell",self.status.get_safety_price_table()[self.status.get_so_amount()+1]) - else: - new_order = self.broker.new_limit_order(self.config.get_pair(),so_size/self.status.get_safety_price_table()[self.status.get_so_amount()+1],"buy",self.status.get_safety_price_table()[self.status.get_so_amount()+1]) - if new_order==1: - self.status.set_safety_order(self.broker.get_empty_order()) - self.broker.logger.log_this("Not enough balance to send a new safety order",1,self.config.get_pair()) - #elif new_order in [None,self.broker.get_empty_order()] #MAYUBE THIS CONDITIONAL IS BETTER - elif new_order is None: - self.status.set_safety_order(None) - return 1 - else: - self.status.set_safety_order(new_order) - self.status.set_so_amount(self.status.get_so_amount()+1) - return 0 - - def telegram_bot_sendprofit(self,profit,order,base_profit=0) -> int: ''' Sends the Telegram notification when profit is met @@ -1351,9 +1347,6 @@ class trader: if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None",1,self.config.get_pair()) return 1 - if self.status.get_safety_order() is None: - self.broker.logger.log_this("Safety order is None",1,self.config.get_pair()) - return 1 #Replace the current take profit order with a new one with new quote currency self.broker.logger.log_this("Replacing take profit order",2,self.config.get_pair()) @@ -1361,12 +1354,13 @@ class trader: if self.status.get_take_profit_order()==self.broker.get_empty_order(): return 1 - #Replace the current safety order (if any) with a new one with the new quote currency - if self.status.get_safety_order()!=self.broker.get_empty_order(): + #Replace the current safety orders (if any) with new ones with the new quote currency + #This is WRONG: We need to build a list of the newly sent orders and assign them with self.status.set_safety_orders() + new_order_list = [] + for order in self.status.get_safety_orders(): self.broker.logger.log_this("Replacing safety order",2,self.config.get_pair()) - self.status.set_safety_order(self.quote_currency_replace_order(self.status.get_safety_order(),new_quote)) - if self.status.get_safety_order()==self.broker.get_empty_order(): - return 1 + new_order_list.append(self.quote_currency_replace_order(order,new_quote)) + self.status.set_safety_orders(new_order_list) #Calls switch_quote_currency_config self.broker.logger.log_this("Modifying config file",2,self.config.get_pair()) @@ -1582,7 +1576,7 @@ class trader: return status_string - def load_imported_trader(self, forced_tp_order_id = None, forced_safety_order_id = None) -> int: + def load_imported_trader(self) -> int: ''' Loads status dictionary, orders and sets up variables ''' @@ -1596,22 +1590,21 @@ class trader: self.config.set_no_of_safety_orders(self.status.get_no_of_safety_orders()) #If this is not loaded from status_dict, it will ignore if safety orders were added at runtime #Refresh take profit order - order_id = self.status.get_take_profit_order()["id"] if forced_tp_order_id is None else forced_tp_order_id + order_id = self.status.get_take_profit_order()["id"] self.status.set_take_profit_order(self.broker.get_order(order_id,self.config.get_pair())) if self.status.get_take_profit_order()==self.broker.get_empty_order(): self.broker.logger.log_this("Couldn't load take profit order (broker returned empty order). Aborting.",1,self.config.get_pair()) self.quit = True return 1 - #Refresh safety order - order_id = self.status.get_safety_order()["id"] if forced_safety_order_id is None else forced_safety_order_id - self.status.set_safety_order(self.broker.get_order(order_id,self.config.get_pair())) - if self.status.get_safety_order()==self.broker.get_empty_order() and self.status.get_so_amount()