diff --git a/changelog.txt b/changelog.txt index 45b22e6..ec6c00f 100755 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,13 @@ +2026.06.03: +. Fixed tp mode 2 non-functional +. Fixed duster binance fee estimation +. Fixed executor variable shadowing breaking graceful shutdown +. Fixed infinite loop in cancel_order +. Fixed modifying running_traders during iteration +. Fixed missing "status/" prefix in old_long file paths +. Removed double TP order cancellation while switching to short. +. Added locks to prevent race conditions on running_traders. + 2025.12.01: . Modified log output of new_market_order. . Modified Kucoin's case in min_amount_of_base. diff --git a/duster.py b/duster.py index e6db56b..729b632 100644 --- a/duster.py +++ b/duster.py @@ -189,7 +189,8 @@ class duster: if self.broker.get_exchange_name()=="binance": #CCXT still to this day does not take Binance fees into account. try: - fee_rate = self.broker.fetch_market["maker"] if order["type"]=="limit" else self.broker.fetch_market["taker"] + market = self.broker.fetch_maker(self.duster_status["pair"]) + fee_rate = market["maker"] if order["type"]=="limit" else market["taker"] except Exception as e: self.broker.logger.log_this(f"Exception fetching market information: {e}. Using default fee rate of 0.1%",1,f"{base}{quote}") fee_rate = 0.001 diff --git a/exchange_wrapper.py b/exchange_wrapper.py index dac5d56..7b980b3 100755 --- a/exchange_wrapper.py +++ b/exchange_wrapper.py @@ -622,22 +622,40 @@ class Broker: :return: 0 if order was succesfully canceled, 1 if not ''' - tries = self.retries//2 - while tries>0: + cancel_attempts = self.retries//2 + while cancel_attempts > 0: try: - while self.get_order(id,symbol)["status"]=="open": - self.exchange.cancel_order(id,symbol) - time.sleep(self.wait_time) - return 0 + self.exchange.cancel_order(id, symbol) + time.sleep(self.wait_time) + if self.get_order(id, symbol)["status"] != "open": + return 0 except Exception as e: - if self.get_order(id,symbol)["status"]=="canceled": + if self.get_order(id, symbol)["status"] == "canceled": return 0 self.logger.log_this(f"Exception in cancel_order: id {id} - exception: {e}",1) if no_retries: break - time.sleep(self.wait_time) - tries-=1 + cancel_attempts -= 1 + time.sleep(self.wait_time) return 1 + + + # tries = self.retries//2 + # while tries>0: + # try: + # while self.get_order(id,symbol)["status"]=="open": + # self.exchange.cancel_order(id,symbol) + # time.sleep(self.wait_time) + # return 0 + # except Exception as e: + # if self.get_order(id,symbol)["status"]=="canceled": + # return 0 + # self.logger.log_this(f"Exception in cancel_order: id {id} - exception: {e}",1) + # if no_retries: + # break + # time.sleep(self.wait_time) + # tries-=1 + # return 1 def amount_to_precision(self,pair,amount): diff --git a/main.py b/main.py index d17425d..cff87bc 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ from sys import argv from os import _exit as os_exit from json import load from datetime import date -from threading import Thread +from threading import Thread, Lock from waitress import serve from concurrent.futures import ThreadPoolExecutor, as_completed @@ -18,7 +18,7 @@ import exchange_wrapper import trader -version = "2025.12.01" +version = "2026.06.03" ''' Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors @@ -43,7 +43,7 @@ executor = None def shutdown_handler(signum, _): broker.logger.log_this(f"Received signal {signum}, shutting down.", 2) if executor: - executor.shutdown(wait=True, timeout=5) + executor.shutdown(wait=True) os_exit(0) # Register signals for shutdown handler @@ -119,10 +119,11 @@ def add_instance(base: str, quote: str) -> int: #Check if the pair is already running pair = f"{base}{quote}" - for instance in running_traders: - if f"{instance.base}{instance.quote}"==pair: - broker.logger.log_this(f"Pair already running, duplicate traders are not allowed",1,pair) - return 1 + with traders_lock: + for instance in running_traders: + if f"{instance.base}{instance.quote}"==pair: + broker.logger.log_this(f"Pair already running, duplicate traders are not allowed",1,pair) + return 1 #Initialize the trader object and add the pair to the tickers list instances_to_add.append(trader.trader(broker,f"{base}/{quote}")) @@ -143,7 +144,8 @@ def initialize_instance(base: str, quote: str) -> int: int: 0 if successful ''' broker.logger.log_this(f"Initializing {f'{base}/{quote}'}") - running_traders.append(trader.trader(broker,f'{base}/{quote}')) + with traders_lock: + running_traders.append(trader.trader(broker,f'{base}/{quote}')) if f'{base}{quote}' not in tickers: tickers.append(f'{base}{quote}') return 0 @@ -246,33 +248,34 @@ def restart_pair_no_json(base: str, quote: str) -> int: try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.set_pause(True, "Restarting trader") - #Backing up old status file - instance.status.save_to_file(is_backup=True) - - broker.logger.log_this(f"Cancelling old take profit order",2,symbol) - try: - old_tp_order = instance.status.get_take_profit_order() - broker.cancel_order(old_tp_order["id"],old_tp_order["symbol"]) - except Exception as e: - broker.logger.log_this(f"Error canceling old take profit order: {e}",2,symbol) - - broker.logger.log_this(f"Cancelling old take safety orders",2,symbol) - for item in instance.status.get_safety_orders(): + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.set_pause(True, "Restarting trader") + #Backing up old status file + instance.status.save_to_file(is_backup=True) + + broker.logger.log_this(f"Cancelling old take profit order",2,symbol) try: - broker.cancel_order(item["id"],item["symbol"]) + old_tp_order = instance.status.get_take_profit_order() + broker.cancel_order(old_tp_order["id"],old_tp_order["symbol"]) except Exception as e: - broker.logger.log_this(f"Error canceling old safety order: {e}",2,symbol) - - try: - running_traders.remove(instance) - except ValueError: - broker.logger.log_this(f"Instance {instance.status.get_pair()} not found in running_traders.",1,instance.status.get_pair()) - add_instance(base,quote) - return 0 - return 1 + broker.logger.log_this(f"Error canceling old take profit order: {e}",2,symbol) + + broker.logger.log_this(f"Cancelling old take safety orders",2,symbol) + for item in instance.status.get_safety_orders(): + try: + broker.cancel_order(item["id"],item["symbol"]) + except Exception as e: + broker.logger.log_this(f"Error canceling old safety order: {e}",2,symbol) + + try: + running_traders.remove(instance) + except ValueError: + broker.logger.log_this(f"Instance {instance.status.get_pair()} not found in running_traders.",1,instance.status.get_pair()) + add_instance(base,quote) + return 0 + return 1 except Exception as e: broker.logger.log_this(f"Exception in restart_pair_no_json: {e}",1,symbol) return 1 @@ -282,6 +285,7 @@ def main_routine(): global last_market_reload global reload_interval global screen_buffer + global executor executor = ThreadPoolExecutor(max_workers=len(broker.get_config()["pairs"])+worker_threads_overprovisioning) is_testnet = "TESTNET " if broker.get_config()["is_sandbox"] else "" @@ -290,42 +294,51 @@ def main_routine(): while True: #Restart traders that have the restart flag raised and remove traders that have the quit flag raised - for instance in running_traders: - if instance.restart and instance.config.get_attempt_restart(): - broker.logger.log_this(f"Restarting trader",1,instance.status.get_pair()) - restart_pair_no_json(instance.base,instance.quote) - if instance.quit: - #Here, check if a duster is needed - broker.logger.log_this(f"{broker.get_exchange_name()} | Quit flag raised, removing trader.",0,instance.status.get_pair()) - broker.logger.log_this(f"{broker.get_exchange_name()} | Quit flag raised, removing trader: {instance.status.get_pair()}",-1) #Forced message to TG - if f"{instance.base}{instance.quote}" in tickers: - tickers.remove(f"{instance.base}{instance.quote}") - broker.remove_pair_from_config(f"{instance.base}{instance.quote}") - broker.rewrite_config_file() - try: - running_traders.remove(instance) - except ValueError: - broker.logger.log_this(f"Instance {instance.status.get_pair()} not found in running_traders.",1,instance.status.get_pair()) + to_restart = [] + to_remove = [] + with traders_lock: + for instance in running_traders: + if instance.restart and instance.config.get_attempt_restart(): + to_restart.append(instance) + if instance.quit: + to_remove.append(instance) + for instance in to_restart: + broker.logger.log_this(f"Restarting trader",1,instance.status.get_pair()) + restart_pair_no_json(instance.base,instance.quote) + for instance in to_remove: + #Here, check if a duster is needed + broker.logger.log_this(f"{broker.get_exchange_name()} | Quit flag raised, removing trader.",0,instance.status.get_pair()) + broker.logger.log_this(f"{broker.get_exchange_name()} | Quit flag raised, removing trader: {instance.status.get_pair()}",-1) #Forced message to TG + if f"{instance.base}{instance.quote}" in tickers: + tickers.remove(f"{instance.base}{instance.quote}") + broker.remove_pair_from_config(f"{instance.base}{instance.quote}") + broker.rewrite_config_file() + try: + running_traders.remove(instance) + except ValueError: + broker.logger.log_this(f"Instance {instance.status.get_pair()} not found in running_traders.",1,instance.status.get_pair()) #Adds pending traders if bool(instances_to_add): - for instance in instances_to_add: - running_traders.append(instance) - instances_to_add.clear() + with traders_lock: + for instance in instances_to_add: + running_traders.append(instance) + instances_to_add.clear() #Prepares the trader threads futures = [] pairs_to_fetch = [] online_pairs = [] - - for instance in running_traders: - pairs_to_fetch.append(instance.status.get_pair()) + with traders_lock: + for instance in running_traders: + pairs_to_fetch.append(instance.status.get_pair()) open_orders = broker.fetch_open_orders(pairs_to_fetch) - for instance in running_traders: - future = executor.submit(instance.check_status, open_orders) - futures.append(future) - online_pairs.append(f"{instance.base}{instance.quote}") + with traders_lock: + for instance in running_traders: + future = executor.submit(instance.check_status, open_orders) + futures.append(future) + online_pairs.append(f"{instance.base}{instance.quote}") #Fetch prices price_list = broker.get_prices(pairs_to_fetch) @@ -343,24 +356,26 @@ def main_routine(): short_traders_status_strings = [] paused_traders_status_strings = [] global_status["paused_traders"].clear() - for instance in running_traders: - if not instance.config.get_is_short(): - curr += int(instance.status.get_so_amount()) # For the safety order occupancy percentage calculation - top += int(instance.config.get_no_of_safety_orders()) - if "status_string" in instance.get_status_dict(): - long_traders_status_strings.append(str(instance)) - elif "status_string" in instance.get_status_dict(): - short_traders_status_strings.append(str(instance)) - try: - if instance.status.get_pair() in price_list and price_list[instance.status.get_pair()] is not None: - instance.get_status_dict()["price"] = price_list[instance.status.get_pair()] - except Exception as e: - broker.logger.log_this(f"Exception while querying for pair price, key not present on price_list dictionary: {e}",1,instance.status.get_pair()) - - #Add paused traders to the paused trader list - if instance.pause: - global_status["paused_traders"].append(instance.status.get_pair()) - paused_traders_status_strings.append(f"{cyan}Paused pairs: {list(global_status['paused_traders'])}{white}") + + with traders_lock: + for instance in running_traders: + if not instance.config.get_is_short(): + curr += int(instance.status.get_so_amount()) # For the safety order occupancy percentage calculation + top += int(instance.config.get_no_of_safety_orders()) + if "status_string" in instance.get_status_dict(): + long_traders_status_strings.append(str(instance)) + elif "status_string" in instance.get_status_dict(): + short_traders_status_strings.append(str(instance)) + try: + if instance.status.get_pair() in price_list and price_list[instance.status.get_pair()] is not None: + instance.get_status_dict()["price"] = price_list[instance.status.get_pair()] + except Exception as e: + broker.logger.log_this(f"Exception while querying for pair price, key not present on price_list dictionary: {e}",1,instance.status.get_pair()) + + #Add paused traders to the paused trader list + if instance.pause: + global_status["paused_traders"].append(instance.status.get_pair()) + paused_traders_status_strings.append(f"{cyan}Paused pairs: {list(global_status['paused_traders'])}{white}") #Delete no longer used data del price_list @@ -374,7 +389,8 @@ def main_routine(): #Updates some global status variables prior to deletion of those if len(running_traders)!=len(global_status["online_workers"]): - global_status["online_workers"] = [instance.status.get_pair() for instance in running_traders] + with traders_lock: + global_status["online_workers"] = [instance.status.get_pair() for instance in running_traders] #Prints general info instance_uptime = int(time.time()) - instance_start_time @@ -409,9 +425,10 @@ def main_routine(): #Toggle pauses if toggle_pauses: - for instance in running_traders: - if instance.status.get_pair() in toggle_pauses: - instance.pause = not instance.pause + with traders_lock: + for instance in running_traders: + if instance.status.get_pair() in toggle_pauses: + instance.pause = not instance.pause toggle_pauses.clear() #Checks if market reload is due @@ -1505,9 +1522,10 @@ def unwrapped_return_worker_status(base,quote): dict: The status dictionary of the trader. ''' symbol = f"{base}/{quote}" - for instance in running_traders: - if instance.status.get_pair() == symbol: - return jsonify(instance.status.get_status()) + with traders_lock: + for instance in running_traders: + if instance.status.get_pair() == symbol: + return jsonify(instance.status.get_status()) return jsonify({"Error": "Worker does not exist"}) @@ -1518,8 +1536,8 @@ def unwrapped_return_all_worker_status(): Returns: dict: The status dictionary of all traders. ''' - - return {instance.status.get_pair(): instance.status.get_status() for instance in running_traders} + with traders_lock: + return {instance.status.get_pair(): instance.status.get_status() for instance in running_traders} def unwrapped_add_pair(base,quote): @@ -1538,10 +1556,11 @@ def unwrapped_add_pair(base,quote): symbol = f"{base}/{quote}" #Check if the trader is already running - for instance in running_traders: - if symbol==instance.status.get_pair(): - broker.logger.log_this(f"Pair already running",1,symbol) - return jsonify({"Error": "Pair already running"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + broker.logger.log_this(f"Pair already running",1,symbol) + return jsonify({"Error": "Pair already running"}) #Check if the market exists and it's open if not broker.validate_market(symbol): @@ -1571,10 +1590,11 @@ def unwrapped_remove_pair(base,quote): ''' try: - symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.quit = True + symbol = f"{base}/{quote}" + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.quit = True return jsonify({"Success": "Pair to be removed"}) except Exception as e: broker.logger.log_this(f"Exception while removing instance: {e}",1,symbol) @@ -1639,15 +1659,16 @@ def unwrapped_switch_to_long(base,quote,calculate_profits): #Close trader and orders and pull info our of the orders if f"{base}{quote}" not in broker.get_pairs(): return jsonify({"Error": "Pair not running"}) - for instance in running_traders: - if f"{base}/{quote}"==instance.status.get_pair(): - instance.set_pause(True, "Switching to long mode") - if instance.switch_to_long(ignore_old_long=ignore_old_long,double_check_price=False)==1: - return jsonify({"Error": "Error in switch_to_long()"}) - if instance.start_trader()==1: - instance.quit = True - return jsonify({"Error": "Error switching to long mode (wAPI)"}) - return jsonify({"Success": "Pair switched to long mode"}) + with traders_lock: + for instance in running_traders: + if f"{base}/{quote}"==instance.status.get_pair(): + instance.set_pause(True, "Switching to long mode") + if instance.switch_to_long(ignore_old_long=ignore_old_long,double_check_price=False)==1: + return jsonify({"Error": "Error in switch_to_long()"}) + if instance.start_trader()==1: + instance.quit = True + return jsonify({"Error": "Error switching to long mode (wAPI)"}) + return jsonify({"Success": "Pair switched to long mode"}) return jsonify({"Error": "Pair not found"}) @@ -1667,27 +1688,29 @@ def unwrapped_switch_to_short(base,quote): symbol = f"{base}/{quote}" if f"{base}{quote}" not in broker.get_pairs(): return jsonify({"Error": "Pair not running"}) - for instance in running_traders: - if symbol==instance.status.get_pair() and instance.switch_to_short()==1: - return jsonify({"Error": "Error in switch_to_short()"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair() and instance.switch_to_short()==1: + return jsonify({"Error": "Error in switch_to_short()"}) #Restart instance try: broker.logger.log_this(f"Reinitializing trader",2,symbol) - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.status.set_take_profit_order(instance.broker.empty_order) - instance.so = instance.broker.empty_order + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.status.set_take_profit_order(instance.broker.empty_order) + instance.so = instance.broker.empty_order - #Reloading config file - instance.config.load_from_file() + #Reloading config file + instance.config.load_from_file() - #Enabling autoswitch - instance.config.set_autoswitch(True) - if instance.start_trader()==1: - instance.quit = True - return jsonify({"Error": "Error switching to short mode (wAPI)"}) - return jsonify({"Success": "Pair switched to short mode"}) + #Enabling autoswitch + instance.config.set_autoswitch(True) + if instance.start_trader()==1: + instance.quit = True + return jsonify({"Error": "Error switching to short mode (wAPI)"}) + return jsonify({"Success": "Pair switched to short mode"}) except Exception as e: broker.logger.log_this(f"Exception while reinitializing instance: {e}",1,symbol) return jsonify({"Error": "Can't initialize trader"}) @@ -1709,7 +1732,7 @@ def unwrapped_load_old_long(base,quote): #Load the file try: symbol = f"{base}/{quote}" - with open(f"{base}{quote}.oldlong") as ol: + with open(f"status/{base}{quote}.oldlong") as ol: old_long = load(ol) except Exception as e: broker.logger.log_this(f"Exception while loading old_long file: {e}",1,symbol) @@ -1723,11 +1746,12 @@ def unwrapped_load_old_long(base,quote): #Maybe here we could also check that the keys have the proper values #Creates (or modifies) a key in the status dictionary and assigns the contents of the file to that same key. - for instance in running_traders: - if instance.status.get_pair()==symbol: - instance.get_status_dict()["old_long"]=old_long - instance.update_status(True) - return jsonify({"Success": "old_long file loaded to status_dict"}) + with traders_lock: + for instance in running_traders: + if instance.status.get_pair()==symbol: + instance.get_status_dict()["old_long"]=old_long + instance.update_status(True) + return jsonify({"Success": "old_long file loaded to status_dict"}) return jsonify({"Error": "Pair not found"}) @@ -1747,14 +1771,15 @@ def unwrapped_view_old_long(base,quote,from_file): try: symbol = f"{base}/{quote}" if int(from_file)==1: - with open(f"{base}{quote}.oldlong") as ol: + with open(f"status/{base}{quote}.oldlong") as ol: old_long = load(ol) return jsonify(old_long) - for instance in running_traders: - if symbol==instance.status.get_pair(): - if "old_long" in instance.get_status_dict(): - return jsonify(instance.get_status_dict()["old_long"]) - return jsonify({"Error": "No old_long info found"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + if "old_long" in instance.get_status_dict(): + return jsonify(instance.get_status_dict()["old_long"]) + return jsonify({"Error": "No old_long info found"}) return jsonify({"Error": "Pair not found"}) except Exception as e: broker.logger.log_this(f"Exception while viewing old_long info: {e}",1,symbol) @@ -1776,16 +1801,17 @@ def unwrapped_switch_to_long_price(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - if "old_long" in instance.get_status_dict(): - #minimum_switch_price = (old_target - quote_already_in)/base_left - old_target = instance.get_status_dict()["old_long"]["tp_price"]*instance.get_status_dict()["old_long"]["tp_amount"] - base_left = instance.get_status_dict()["old_long"]["tp_amount"]-instance.get_status_dict()["base_bought"] - minimum_switch_price = (old_target - instance.get_status_dict()["quote_spent"])/base_left - return jsonify({"switch_price": minimum_switch_price}) - return jsonify({"Error": "No old_long info found"}) - return jsonify({"Error": "Pair not found"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + if "old_long" in instance.get_status_dict(): + #minimum_switch_price = (old_target - quote_already_in)/base_left + old_target = instance.get_status_dict()["old_long"]["tp_price"]*instance.get_status_dict()["old_long"]["tp_amount"] + base_left = instance.get_status_dict()["old_long"]["tp_amount"]-instance.get_status_dict()["base_bought"] + minimum_switch_price = (old_target - instance.get_status_dict()["quote_spent"])/base_left + return jsonify({"switch_price": minimum_switch_price}) + return jsonify({"Error": "No old_long info found"}) + return jsonify({"Error": "Pair not found"}) except Exception as e: broker.logger.log_this(f"Exception while viewing old_long info: {e}",1,symbol) return jsonify({"Error": f"{e}"}) @@ -1807,17 +1833,18 @@ def unwrapped_add_safety_orders(base,quote,amount): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.set_pause(True, "Adding safety orders") - instance.status.set_no_of_safety_orders(instance.status.get_no_of_safety_orders()+int(amount)) - broker.logger.log_this("Recalculating safety price table...",1,symbol) - instance.status.set_safety_price_table(instance.calculate_safety_prices(instance.status.get_start_price(),instance.config.get_no_of_safety_orders(),instance.config.get_safety_order_deviance())) - broker.logger.log_this(f"Done. Added {amount} safety orders",1,symbol) - instance.update_status(True) - instance.set_pause(False) - return jsonify({"Success": f"Done. Added {amount} safety orders"}) - return jsonify({"Error": "Pair not found"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.set_pause(True, "Adding safety orders") + instance.status.set_no_of_safety_orders(instance.status.get_no_of_safety_orders()+int(amount)) + broker.logger.log_this("Recalculating safety price table...",1,symbol) + instance.status.set_safety_price_table(instance.calculate_safety_prices(instance.status.get_start_price(),instance.config.get_no_of_safety_orders(),instance.config.get_safety_order_deviance())) + broker.logger.log_this(f"Done. Added {amount} safety orders",1,symbol) + instance.update_status(True) + instance.set_pause(False) + return jsonify({"Success": f"Done. Added {amount} safety orders"}) + return jsonify({"Error": "Pair not found"}) except Exception as e: broker.logger.log_this(f"{e}",2,symbol) return jsonify({"Error": "Error adding safety orders"}) @@ -1837,14 +1864,15 @@ def unwrapped_base_add_so_calculation(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - free_base = instance.fetch_free_base() - if free_base is None: - return jsonify({"Error": "Can't fetch amount of free base on the exchange"}) - amount_of_orders = instance.base_add_calculation(free_base) - return jsonify({"Amount": amount_of_orders, "Free base on exchange": free_base}) - return jsonify({"Error": "Can't find the pair in the running instances"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + free_base = instance.fetch_free_base() + if free_base is None: + return jsonify({"Error": "Can't fetch amount of free base on the exchange"}) + amount_of_orders = instance.base_add_calculation(free_base) + return jsonify({"Amount": amount_of_orders, "Free base on exchange": free_base}) + return jsonify({"Error": "Can't find the pair in the running instances"}) except Exception as e: broker.logger.log_this(f"{e}",2,symbol) return jsonify({"Error": "Error in unwrapped_base_add_so_calculation"}) @@ -1865,9 +1893,10 @@ def unwrapped_mod_tp_level(base,quote,amount): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.config.set_tp_level(float(amount)) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.config.set_tp_level(float(amount)) broker.logger.log_this("Done. The change will take effect when the next take profit order is placed",2,symbol) return jsonify({"Success": "Success. The change will take effect when the next TP order is placed"}) except Exception: @@ -1890,10 +1919,11 @@ def unwrapped_mod_order_size(base,quote,amount): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.config.set_order_size(float(amount)) - instance.config.save_to_file() + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.config.set_order_size(float(amount)) + instance.config.save_to_file() broker.logger.log_this("Done. The change will take effect when the next deal is started",2,symbol) return jsonify({"Success": "Success. The change will take effect when the next deal is started"}) except Exception: @@ -1916,10 +1946,11 @@ def unwrapped_mod_concurrent_safety_orders(base,quote,amount): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.config.set_concurrent_safety_orders(int(amount)) - instance.config.save_to_file() + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.config.set_concurrent_safety_orders(int(amount)) + instance.config.save_to_file() broker.logger.log_this("Done. The change will take effect as new safety orders are sent or filled",2,symbol) return jsonify({"Success": "Success. The change will take effect as new safety orders are sent or filled"}) except Exception: @@ -1942,10 +1973,11 @@ def unwrapped_mod_boosted_concurrent_safety_orders(base,quote,amount): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.config.set_boosted_concurrent_safety_orders(int(amount)) - instance.config.save_to_file() + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.config.set_boosted_concurrent_safety_orders(int(amount)) + instance.config.save_to_file() broker.logger.log_this("Done. The change will take effect as new safety orders are sent or filled",2,symbol) return jsonify({"Success": "Success. The change will take effect as new safety orders are sent or filled"}) except Exception: @@ -1983,13 +2015,13 @@ def unwrapped_mod_global_tp_level(amount): Returns: jsonify: A jsonified dictionary detailing the outcome of the operation ''' - - for instance in running_traders: - try: - instance.config.set_tp_level(float(amount)) - broker.logger.log_this("Done. The change will take effect when the next take profit order is placed",2) - except Exception: - broker.logger.log_this("Error changing percentage. Ignoring.",2) + with traders_lock: + for instance in running_traders: + try: + instance.config.set_tp_level(float(amount)) + broker.logger.log_this("Done. The change will take effect when the next take profit order is placed",2) + except Exception: + broker.logger.log_this("Error changing percentage. Ignoring.",2) return jsonify({"Success": "Success. The change will take effect when the next TP order is placed"}) @@ -2007,16 +2039,17 @@ def unwrapped_last_call(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.status.set_stop_when_profit(not instance.status.get_stop_when_profit()) - instance.update_status(True) - if instance.status.get_stop_when_profit(): - instance.config.set_autoswitch(False) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.status.set_stop_when_profit(not instance.status.get_stop_when_profit()) instance.update_status(True) - return jsonify({"Success": "Trader scheduled to go offline when profit is reached"}) - return jsonify({"Success": "Last call cancelled"}) - return jsonify({"Error": "Trader does not exist"}) + if instance.status.get_stop_when_profit(): + instance.config.set_autoswitch(False) + instance.update_status(True) + return jsonify({"Success": "Trader scheduled to go offline when profit is reached"}) + return jsonify({"Success": "Last call cancelled"}) + return jsonify({"Error": "Trader does not exist"}) except Exception: return jsonify({"Error": "Halp"}) @@ -2041,12 +2074,13 @@ def unwrapped_deferred_last_call(base,quote,yyyymmdd): limit = time_to_unix(year,month,day) if limit==0: return jsonify({"Error": "Can't convert date to unix"}) - for instance in running_traders: - if f"{base}{quote}"==instance.status.get_pair(): - instance.config.set_programmed_stop_time(limit) - instance.config.set_programmed_stop(True) - #save config file to disk - instance.broker.rewrite_config_file() + with traders_lock: + for instance in running_traders: + if f"{base}{quote}"==instance.status.get_pair(): + instance.config.set_programmed_stop_time(limit) + instance.config.set_programmed_stop(True) + #save config file to disk + instance.broker.rewrite_config_file() return jsonify({"Success": f"Trader scheduled to go offline when profit is reached after {limit}"}) except Exception: return jsonify({"Error": "Halp"}) @@ -2068,13 +2102,14 @@ def unwrapped_toggle_pause(base,quote): try: symbol = f"{base}/{quote}" toggle_pauses.append(symbol) - for instance in running_traders: - if instance.status.get_pair()==symbol: - if instance.pause: - instance.status.set_pause_reason("") - return jsonify({"Success": "Trader will be resumed"}) - instance.status.set_pause_reason("User requested pause") - return jsonify({"Success": "Trader will be paused"}) + with traders_lock: + for instance in running_traders: + if instance.status.get_pair()==symbol: + if instance.pause: + instance.status.set_pause_reason("") + return jsonify({"Success": "Trader will be resumed"}) + instance.status.set_pause_reason("User requested pause") + return jsonify({"Success": "Trader will be paused"}) return jsonify({"Error": "Trader does not exist"}) except Exception: return jsonify({"Error": "Halp"}) @@ -2088,10 +2123,11 @@ def unwrapped_global_last_call(): jsonify: A jsonified dictionary detailing the outcome of the operation. ''' try: - for instance in running_traders: - instance.status.set_stop_when_profit(True) - instance.config.set_autoswitch(False) - broker.logger.log_this("Modified flag",2,f"{instance.base}/{instance.quote}") + with traders_lock: + for instance in running_traders: + instance.status.set_stop_when_profit(True) + instance.config.set_autoswitch(False) + broker.logger.log_this("Modified flag",2,f"{instance.base}/{instance.quote}") return jsonify({"Success": "All traders scheduled to go offline when profit is reached"}) except Exception: return jsonify({"Error": "Halp"}) @@ -2105,9 +2141,10 @@ def unwrapped_cancel_global_last_call(): jsonify: A jsonified dictionary detailing the outcome of the operation. ''' try: - for instance in running_traders: - instance.status.set_stop_when_profit(False) - broker.logger.log_this("Modified flag",2,f"{instance.base}/{instance.quote}") + with traders_lock: + for instance in running_traders: + instance.status.set_stop_when_profit(False) + broker.logger.log_this("Modified flag",2,f"{instance.base}/{instance.quote}") return jsonify({"Success": "Last call canceled"}) except Exception: return jsonify({"Error": "Halp"}) @@ -2126,58 +2163,58 @@ def unwrapped_add_quote(base,quote,amount): Returns: json: A jsonified dictionary detailing the outcome of the operation. ''' - - for instance in running_traders: - if f"{base}/{quote}"==instance.status.get_pair(): - if instance.config.get_is_short(): - return jsonify({"Error": "Quote can't be added to short traders"}) - instance.set_pause(True, "Adding quote") - new_average_price = (instance.status.get_quote_spent()+float(amount))/(instance.status.get_base_bought()+(float(amount)/instance.status.get_price())) - broker.logger.log_this(f"Your new average buy price will be {new_average_price} {quote}",2,instance.status.get_pair()) - broker.logger.log_this(f"Your new take profit price price will be {new_average_price*instance.get_tp_level()} {quote}",2,instance.status.get_pair()) - new_order = broker.new_market_order(instance.status.get_pair(),float(amount),"buy") - if new_order is None: - broker.logger.log_this("Error: Market order returned None",2,instance.status.get_pair()) - instance.set_pause(False) - return jsonify({"Error": "Market order returned None"}) - while True: - time.sleep(broker.get_wait_time()) - returned_order = broker.get_order(new_order["id"],instance.status.get_pair()) - if returned_order==broker.empty_order: - broker.logger.log_this("Problems sending the order",2,instance.status.get_pair()) + with traders_lock: + for instance in running_traders: + if f"{base}/{quote}"==instance.status.get_pair(): + if instance.config.get_is_short(): + return jsonify({"Error": "Quote can't be added to short traders"}) + instance.set_pause(True, "Adding quote") + new_average_price = (instance.status.get_quote_spent()+float(amount))/(instance.status.get_base_bought()+(float(amount)/instance.status.get_price())) + broker.logger.log_this(f"Your new average buy price will be {new_average_price} {quote}",2,instance.status.get_pair()) + broker.logger.log_this(f"Your new take profit price price will be {new_average_price*instance.get_tp_level()} {quote}",2,instance.status.get_pair()) + new_order = broker.new_market_order(instance.status.get_pair(),float(amount),"buy") + if new_order is None: + broker.logger.log_this("Error: Market order returned None",2,instance.status.get_pair()) instance.set_pause(False) - return jsonify({"Error": "Problems sending the order"}) - elif returned_order["status"]=="expired": - instance.set_pause(False) - return jsonify({"Error": "New order expired"}) - elif returned_order["status"]=="closed": - broker.logger.log_this("Order sent",2,instance.status.get_pair()) - new_fees_in_base, new_fees_in_quote = instance.parse_fees(returned_order) - instance.status.set_fees_paid_in_base(instance.status.get_fees_paid_in_base() + new_fees_in_base) - instance.status.set_fees_paid_in_quote(instance.status.get_fees_paid_in_quote() + new_fees_in_quote) - instance.status.set_base_bought(instance.status.get_base_bought() + returned_order["filled"] - new_fees_in_base) - instance.status.set_quote_spent(instance.status.get_quote_spent()+returned_order["cost"]) - broker.logger.log_this("Cancelling old take profit order and sending a new one",2,instance.status.get_pair()) - attempts = 5 - while broker.cancel_order(instance.status.get_take_profit_order()["id"],instance.status.get_pair())==1: - broker.logger.log_this("Can't cancel old take profit order, retrying...",2,instance.status.get_pair()) - time.sleep(broker.get_wait_time()) - attempts-=1 - if attempts==0: - broker.logger.log_this("Can't cancel old take profit order, cancelling...",2,instance.status.get_pair()) - instance.set_pause(False) - return jsonify({"Error": "Can't cancel old take profit order."}) - instance.status.set_take_profit_price(instance.status.get_quote_spent()/instance.status.get_base_bought()*instance.get_tp_level()) - instance.status.set_take_profit_order(broker.new_limit_order(instance.status.get_pair(),instance.status.get_base_bought(),"sell",instance.status.get_take_profit_price())) - instance.update_status(True) - break - else: - broker.logger.log_this("Waiting for initial order to get filled",2,instance.status.get_pair()) - broker.logger.log_this(f"{returned_order}",2,instance.status.get_pair()) + return jsonify({"Error": "Market order returned None"}) + while True: time.sleep(broker.get_wait_time()) - instance.set_pause(False) - broker.logger.log_this("Done",2,instance.status.get_pair()) - return jsonify({"Success": "Quote added successfully"}) + returned_order = broker.get_order(new_order["id"],instance.status.get_pair()) + if returned_order==broker.empty_order: + broker.logger.log_this("Problems sending the order",2,instance.status.get_pair()) + instance.set_pause(False) + return jsonify({"Error": "Problems sending the order"}) + elif returned_order["status"]=="expired": + instance.set_pause(False) + return jsonify({"Error": "New order expired"}) + elif returned_order["status"]=="closed": + broker.logger.log_this("Order sent",2,instance.status.get_pair()) + new_fees_in_base, new_fees_in_quote = instance.parse_fees(returned_order) + instance.status.set_fees_paid_in_base(instance.status.get_fees_paid_in_base() + new_fees_in_base) + instance.status.set_fees_paid_in_quote(instance.status.get_fees_paid_in_quote() + new_fees_in_quote) + instance.status.set_base_bought(instance.status.get_base_bought() + returned_order["filled"] - new_fees_in_base) + instance.status.set_quote_spent(instance.status.get_quote_spent()+returned_order["cost"]) + broker.logger.log_this("Cancelling old take profit order and sending a new one",2,instance.status.get_pair()) + attempts = 5 + while broker.cancel_order(instance.status.get_take_profit_order()["id"],instance.status.get_pair())==1: + broker.logger.log_this("Can't cancel old take profit order, retrying...",2,instance.status.get_pair()) + time.sleep(broker.get_wait_time()) + attempts-=1 + if attempts==0: + broker.logger.log_this("Can't cancel old take profit order, cancelling...",2,instance.status.get_pair()) + instance.set_pause(False) + return jsonify({"Error": "Can't cancel old take profit order."}) + instance.status.set_take_profit_price(instance.status.get_quote_spent()/instance.status.get_base_bought()*instance.get_tp_level()) + instance.status.set_take_profit_order(broker.new_limit_order(instance.status.get_pair(),instance.status.get_base_bought(),"sell",instance.status.get_take_profit_price())) + instance.update_status(True) + break + else: + broker.logger.log_this("Waiting for initial order to get filled",2,instance.status.get_pair()) + broker.logger.log_this(f"{returned_order}",2,instance.status.get_pair()) + time.sleep(broker.get_wait_time()) + instance.set_pause(False) + broker.logger.log_this("Done",2,instance.status.get_pair()) + return jsonify({"Success": "Quote added successfully"}) return jsonify({"Error": "Something horrible happened :S"}) @@ -2213,12 +2250,13 @@ def unwrapped_toggle_cleanup(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - instance.config.set_cleanup(not instance.config.get_cleanup()) - if instance.config.get_cleanup(): - return jsonify({"Success": "Cleanup turned ON"}) - return jsonify({"Success": "Cleanup turned OFF"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + instance.config.set_cleanup(not instance.config.get_cleanup()) + if instance.config.get_cleanup(): + return jsonify({"Success": "Cleanup turned ON"}) + return jsonify({"Success": "Cleanup turned OFF"}) except Exception as e: broker.logger.log_this(f"Exception while toggling cleanup: {e}",1,symbol) return jsonify({"Error": "Halp"}) @@ -2239,13 +2277,14 @@ def unwrapped_force_trader_close(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - outcome = instance.force_close() - if outcome==0: - return jsonify({"Success": "Trader closed position successfully"}) - return jsonify({"Error": "Error while forcing trader to close position"}) - return jsonify({"Error": "Trader not found"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + outcome = instance.force_close() + if outcome==0: + return jsonify({"Success": "Trader closed position successfully"}) + return jsonify({"Error": "Error while forcing trader to close position"}) + return jsonify({"Error": "Trader not found"}) except Exception as e: broker.logger.log_this(f"Exception while forcing trader to close position: {e}",1,symbol) return jsonify({"Error": "Halp"}) @@ -2265,16 +2304,17 @@ def unwrapped_toggle_autoswitch(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - if instance.config.get_autoswitch(): - broker.logger.log_this("Autoswitch turned OFF",1,symbol) - instance.config.set_autoswitch(False) - return jsonify({"Success": "Autoswitch is now OFF"}) - else: - broker.logger.log_this("Autoswitch turned ON",1,symbol) - instance.config.set_autoswitch(True) - return jsonify({"Success": "Autoswitch is now ON"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + if instance.config.get_autoswitch(): + broker.logger.log_this("Autoswitch turned OFF",1,symbol) + instance.config.set_autoswitch(False) + return jsonify({"Success": "Autoswitch is now OFF"}) + else: + 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) @@ -2295,16 +2335,17 @@ def unwrapped_toggle_liquidate_after_switch(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - if instance.config.get_liquidate_after_switch(): - broker.logger.log_this("Liquidate after switch turned OFF",1,symbol) - instance.config.set_liquidate_after_switch(False) - return jsonify({"Success": "Liquidate after switch is now OFF"}) - else: - 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"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + if instance.config.get_liquidate_after_switch(): + broker.logger.log_this("Liquidate after switch turned OFF",1,symbol) + instance.config.set_liquidate_after_switch(False) + return jsonify({"Success": "Liquidate after switch is now OFF"}) + else: + 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) @@ -2325,16 +2366,17 @@ def unwrapped_toggle_check_old_long_price(base,quote): try: symbol = f"{base}/{quote}" - for instance in running_traders: - if symbol==instance.status.get_pair(): - if instance.config.get_check_old_long_price(): - broker.logger.log_this("Check OFF",1,symbol) - instance.config.set_check_old_long_price(False) - return jsonify({"Success": "Old long price check turned OFF"}) - else: - 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"}) + with traders_lock: + for instance in running_traders: + if symbol==instance.status.get_pair(): + if instance.config.get_check_old_long_price(): + broker.logger.log_this("Check OFF",1,symbol) + instance.config.set_check_old_long_price(False) + return jsonify({"Success": "Old long price check turned OFF"}) + else: + 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) @@ -2439,7 +2481,8 @@ def unwrapped_trader_time(): ''' try: - return jsonify({"Time": max(instance.last_time_seen for instance in running_traders)}) + with traders_lock: + return jsonify({"Time": max(instance.last_time_seen for instance in running_traders)}) except Exception as e: broker.logger.log_this(f"Exception while retrieving trader_time: {e}",1) return jsonify({"Error": str(e)}) @@ -2634,6 +2677,7 @@ if __name__=="__main__": broker = exchange_wrapper.Broker(exchange,read_config,argv[1]) #Also passes the config filename #Declaring some variables + traders_lock = Lock() running_traders = [] instances_to_add = [] online_pairs = [] diff --git a/trader.py b/trader.py index 3f68595..87fea2f 100755 --- a/trader.py +++ b/trader.py @@ -560,8 +560,6 @@ class trader: self.broker.logger.log_this("Can't cancel the take profit order. Can't switch mode",1,self.status.get_pair()) self.set_pause(False) return 1 - if self.status.get_take_profit_order()["id"]!="": - self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) #Save the old take profit order info for later use self.broker.logger.log_this("Saving state in status_dict",2,self.status.get_pair()) @@ -1307,7 +1305,8 @@ class trader: if len(self.config.get_tp_table())>=order_index: tp_level = self.config.get_tp_table()[order_index] #Custom percentage table tp_level = self.config.get_tp_table()[-1] - tp_level = self.config.get_tp_level() + else: + tp_level = self.config.get_tp_level() elif self.config.get_tp_mode()==3: #Linear percentage table profit_table = self.linear_space(self.config.get_tp_level()+0.005,self.config.get_tp_level()-0.005,self.status.get_no_of_safety_orders()) tp_level = profit_table[-1]