diff --git a/changelog.txt b/changelog.txt index d2fe967..f8674e9 100755 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,9 @@ -2025.09.01: +2025.09.04: . Fixed bug in unwrapped_last_call(). +. Now the trader supports multiple safety orders at the same time. +. Removed forcing orders when importing a trader. Maybe it will be reinstated at a later date. +. Removed endpoint /reload_safety_orders. +. New endpoints: /mod_concurrent_safety orders, /mod_boosted_concurrent_safety_orders and /force_trader_close. 2025.08.19: . Improved log trimming. diff --git a/config_handler.py b/config_handler.py index 4243999..9c6c376 100644 --- a/config_handler.py +++ b/config_handler.py @@ -14,6 +14,8 @@ class ConfigHandler: "order_size": self.broker.get_default_order_size(), "no_of_safety_orders": 30, "max_short_safety_orders": 45, + "concurrent_safety_orders": 3, + "boosted_concurrent_safety_orders": 5, "safety_order_deviance": 2, "safety_order_scale": 0.0105, "dynamic_so_deviance": True, @@ -35,6 +37,9 @@ class ConfigHandler: "force_restart_if_retries_exhausted": False, "check_old_long_price": False #switch_to_short should flip this to True unless stated } + # if self.broker.get_exchange_name()=="kucoin": + # self.default_config_dictionary["concurrent_safety_orders"]=1 + # self.default_config_dictionary["boosted_concurrent_safety_orders"]=1 self.config_file_path = f"configs/{pair.split('/')[0]}{pair.split('/')[1]}.json" self.config_dictionary = self.default_config_dictionary.copy() @@ -68,6 +73,12 @@ class ConfigHandler: def get_max_short_safety_orders(self): return self.config_dictionary["max_short_safety_orders"] + def get_concurrent_safety_orders(self): + return self.config_dictionary["concurrent_safety_orders"] + + def get_boosted_concurrent_safety_orders(self): + return self.config_dictionary["boosted_concurrent_safety_orders"] + def get_safety_order_deviance(self): return self.config_dictionary["safety_order_deviance"] @@ -173,6 +184,20 @@ class ConfigHandler: self.config_dictionary["max_short_safety_orders"] = max_short_safety_orders return 0 + def set_concurrent_safety_orders(self, concurrent_safety_orders: int): + # if not isinstance(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["concurrent_safety_orders"] = concurrent_safety_orders + return 0 + + def set_boosted_concurrent_safety_orders(self, boosted_concurrent_safety_orders: int): + # if not isinstance(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["boosted_concurrent_safety_orders"] = boosted_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..15453e6 100755 --- a/exchange_wrapper.py +++ b/exchange_wrapper.py @@ -379,6 +379,11 @@ class Broker: if self.get_exchange_name()=="binance": a = self.exchange.fetch_last_prices(pair_list) return {x: a[x]["price"] for x in a.keys()} + elif self.get_exchange_name()=="kucoin": + a = self.exchange.fetch_tickers(pair_list) + if pair_list is None: + return {x: a[x]["close"] for x in a.keys()} + return {x: a[x]["close"] for x in a.keys() if x in pair_list} else: a = self.exchange.fetch_tickers() if pair_list is None: @@ -533,7 +538,7 @@ class Broker: if pairs is None: pairs = [] try: - if self.get_exchange_name()=="binance": + if self.get_exchange_name()in ["binance","kucoin"]: return self.get_opened_orders_binance(pairs) return self.get_opened_orders() except Exception as e: @@ -581,7 +586,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 +597,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: @@ -654,16 +659,15 @@ class Broker: :return: 0 if order was succesfully canceled, 1 if not ''' - pair = symbol tries = self.retries//2 while tries>0: try: - while self.get_order(id,pair)["status"]=="open": - self.exchange.cancel_order(id,symbol=pair) + 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,pair)["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: @@ -724,26 +728,25 @@ class Broker: ''' retries = self.retries//2 - pair = symbol while retries>0: try: if self.get_exchange_name()=="gateio" and side=="buy" and not amount_in_base: - new_order = self.exchange.create_market_buy_order_with_cost(pair, size) + new_order = self.exchange.create_market_buy_order_with_cost(symbol, size) else: order_book = self.get_order_book(symbol) if order_book=={}: self.logger.log_this(f"new_simulated_market_order. Order book returned an empty dictionary",1,symbol) return self.empty_order if amount_in_base or side!="buy": - base_amount = self.amount_to_precision(pair,size) + base_amount = self.amount_to_precision(symbol,size) else: avg_price = self.average_price_depth(order_book,size,"sell") base_amount = size/avg_price if avg_price is not None else size/self.get_ticker_price(symbol) price = self.find_minimum_viable_price(order_book,base_amount,side) #Maybe check for slippage here instead of within the trader itself? idk - new_order = self.exchange.create_order(pair,"limit",side,base_amount,price) + new_order = self.exchange.create_order(symbol,"limit",side,base_amount,price) time.sleep(self.wait_time) - return self.get_order(new_order["id"],pair) + return self.get_order(new_order["id"],symbol) except Exception as e: self.logger.log_this(f"new_simulated_market_order exception: {e}",1,symbol) if no_retries: @@ -802,24 +805,23 @@ class Broker: if self.broker_config["simulate_market_orders"]: return self.new_simulated_market_order(symbol,size,side,amount_in_base=amount_in_base) retries = self.retries - pair = symbol while retries>0: try: if side=="buy": to_buy = float(size) if not amount_in_base: - to_buy = float(size)/self.get_top_ask_price(pair) - amount = self.amount_to_precision(pair,to_buy) + to_buy = float(size)/self.get_top_ask_price(symbol) + amount = self.amount_to_precision(symbol,to_buy) else: - amount = self.amount_to_precision(pair,size) #Market sell orders are always nominated in base currency + amount = self.amount_to_precision(symbol,size) #Market sell orders are always nominated in base currency - order_to_send = self.exchange.create_order(pair,"market",side,amount) + order_to_send = self.exchange.create_order(symbol,"market",side,amount) time.sleep(self.wait_time) # Wait a bit more when dealing with Kucoin - return self.get_order(order_to_send["id"],pair) + return self.get_order(order_to_send["id"],symbol) except Exception as e: - self.logger.log_this(f"Exception in new_market_order: {e}",1,pair) + self.logger.log_this(f"Exception in new_market_order: {e}",1,symbol) if no_retries: break time.sleep(self.wait_time) @@ -867,6 +869,40 @@ class Broker: return "the lowest price limit for sell orders is" in str(error_object).lower() + def new_limit_orders(self, orders: list) -> list: + sent_orders = [] + #Send the orders + tries = self.retries + while tries>=0: + try: + sent_orders = self.exchange.create_orders(orders) + except Exception as e: + self.logger.log_this(f"Exception while sending safety orders: {e}",1) + tries-=1 + time.sleep(self.wait_time) + if tries==0: + return [] + + #Retrieve the orders from the exchange by id to confirm that they were sent + #Specially for OKX, since the orders that create_orders return are empty (only id is present) + returned_orders = [] + for order in sent_orders: + tries = self.retries + while tries>=0: + try: + returned_orders.append(self.get_order(order["id"],order["symbol"])) + time.sleep(self.wait_time) + except Exception as e: + self.logger.log_this(f"Exception while retrieving safety orders: {e}",1) + tries-=1 + if tries==0: + if self.get_exchange_name()=="okex": + return returned_orders + returned_orders.append(order) #In the case of the other exchanges, we just assume that the order was sent and append it. + time.sleep(self.wait_time) + return returned_orders + + def new_limit_order(self,symbol,size,side,price,no_retries=False): ''' Sends a new limit order. @@ -879,20 +915,13 @@ class Broker: ''' tries = self.retries - pair = symbol while tries>=0: try: - order_to_send = self.exchange.create_order(pair,"limit",side,self.amount_to_precision(pair,size),price) + order_to_send = self.exchange.create_order(symbol,"limit",side,self.amount_to_precision(symbol,size),price) time.sleep(self.wait_time) - return self.get_order(order_to_send["id"],pair) - #if order_to_send["amount"] is not None: # Because Kucoin etc etc - # return self.get_order(order_to_send["id"],pair) # - #self.logger.log_this(f"Error sending order: Null order returned",2,pair) # - #self.cancel_order(order_to_send["id"],symbol,no_retries=True) # - #retries-=1 - + return self.get_order(order_to_send["id"],symbol) except Exception as e: - self.logger.log_this(f"Exception in new_limit_order - Side: {side} - Size: {size} - {self.amount_to_precision(pair,size)} - Exception: {e}",1,symbol) + self.logger.log_this(f"Exception in new_limit_order - Side: {side} - Size: {size} - {self.amount_to_precision(symbol,size)} - Exception: {e}",1,symbol) if self.not_enough_balance_error(e): if tries<=self.retries//2: #Halves the amount of retries if there is a balance error. return 1 @@ -923,10 +952,9 @@ class Broker: if id=="": return self.empty_order tries = self.retries - pair = symbol while tries>0: try: - return self.exchange.fetch_order(id,symbol=pair) + return self.exchange.fetch_order(id,symbol) except Exception as e: self.logger.log_this(f"Exception in get_order: {e}",1,symbol) if no_retries: @@ -944,10 +972,9 @@ class Broker: :return: The market information. ''' tries = self.retries - pair = symbol while tries>0: try: - return self.exchange.market(pair) + return self.exchange.market(symbol) except Exception as e: self.logger.log_this(f"Exception in fetch_market: {e}",1,symbol) if no_retries: @@ -965,10 +992,9 @@ class Broker: :return: The ticker information. ''' tries = self.retries - pair = symbol while tries>0: try: - return self.exchange.fetch_ticker(pair) + return self.exchange.fetch_ticker(symbol) except Exception as e: self.logger.log_this(f"Exception in get_ticker: {e}") if no_retries: @@ -1032,8 +1058,8 @@ class Broker: :param pair: pair :return: step size - ''' + market = self.fetch_market(pair) if market is None: return None @@ -1133,22 +1159,14 @@ class Logger: #Append to log list self.log_list.append(text) - - #Trim log list - #self.log_list = self.log_list[-self.log_list_max_length:] - except Exception as e: print("Can't write log file") - print(e) - + print(e) if level<1: self.send_tg_message(f"{self.broker_config['exchange'].capitalize()} | {pair_data}{message}",ignore_config=level==-1) return 0 -class Order: - def __init__(self, order: dict = {}): - pass \ No newline at end of file diff --git a/main.py b/main.py index 495dfc3..7f8065b 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ import exchange_wrapper import trader -version = "2025.09.01" +version = "2025.09.04" ''' Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors @@ -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 @@ -247,7 +248,7 @@ def restart_pair_no_json(base: str, quote: str) -> int: symbol = f"{base}/{quote}" order_list = broker.fetch_full_orders(tickers) for instance in running_traders: - if symbol==instance.config.get_pair(): + if symbol==instance.status.get_pair(): instance.pause = True #Backing up old status file instance.status.save_to_file(is_backup=True) @@ -262,7 +263,7 @@ def restart_pair_no_json(base: str, quote: str) -> int: try: running_traders.remove(instance) except ValueError: - broker.logger.log_this(f"Instance {instance.config.get_pair()} not found in running_traders.",1,instance.config.get_pair()) + 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 @@ -276,7 +277,7 @@ def main_routine(): global reload_interval global screen_buffer - executor = ThreadPoolExecutor(max_workers=len(running_traders)+worker_threads_overprovisioning) + executor = ThreadPoolExecutor(max_workers=len(broker.get_config()["pairs"])+worker_threads_overprovisioning) is_testnet = "TESTNET " if broker.get_config()["is_sandbox"] else "" exchange_version_label = f"{bright_white}{broker.get_config()['exchange'].upper()} {is_testnet}{white}| DCAv2 {version} | CCXT v{ccxt.__version__}" separator_line = blue + "="*80 + white @@ -285,12 +286,12 @@ def main_routine(): #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.config.get_pair()) + 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.config.get_pair()) - broker.logger.log_this(f"{broker.get_exchange_name()} | Quit flag raised, removing trader: {instance.config.get_pair()}",-1) #Forced message to TG + 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}") @@ -298,7 +299,7 @@ def main_routine(): try: running_traders.remove(instance) except ValueError: - broker.logger.log_this(f"Instance {instance.config.get_pair()} not found in running_traders.",1,instance.config.get_pair()) + 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): @@ -310,21 +311,21 @@ def main_routine(): futures = [] pairs_to_fetch = [] online_pairs = [] - open_orders = broker.fetch_open_orders(tickers) + + 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}") - pairs_to_fetch.append(instance.config.get_pair()) - - #Delete no longer used data - del open_orders #Fetch prices price_list = broker.get_prices(pairs_to_fetch) #Here, assign the prices to the dusters (if any) - for future in as_completed(futures): try: future.result() @@ -339,21 +340,21 @@ def main_routine(): global_status["paused_traders"].clear() for instance in running_traders: if not instance.config.get_is_short(): - curr += int(instance.get_status_dict()["so_amount"]) # For the safety order occupancy percentage calculation - top += int(instance.config.get_no_of_safety_orders()) # It shows the percentage of safety orders not filled - if "status_string" in instance.get_status_dict(): # status_strings + 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.config.get_pair() in price_list and price_list[instance.config.get_pair()] is not None: - instance.get_status_dict()["price"] = price_list[instance.config.get_pair()] + 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.config.get_pair()) + 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.config.get_pair()) + 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 @@ -368,7 +369,7 @@ 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.config.get_pair() for instance in running_traders] + global_status["online_workers"] = [instance.status.get_pair() for instance in running_traders] #Prints general info instance_uptime = int(time.time()) - instance_start_time @@ -404,7 +405,7 @@ def main_routine(): #Toggle pauses if toggle_pauses: for instance in running_traders: - if instance.config.get_pair() in toggle_pauses: + if instance.status.get_pair() in toggle_pauses: instance.pause = not instance.pause toggle_pauses.clear() @@ -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'}) @@ -850,6 +849,58 @@ def mod_order_size(): return jsonify({'Error': 'Halp'}) +@base_api.route("/mod_concurrent_safety_orders", methods=['POST']) +def mod_concurrent_safety_orders(): + ''' + POST request + + Parameters: + base: str + quote: str + amount: int + ''' + + 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"] + amount = data["amount"] + return unwrapped_mod_concurrent_safety_orders(base,quote,amount) + except Exception as e: + print(e) + return jsonify({'Error': 'Halp'}) + + +@base_api.route("/mod_boosted_concurrent_safety_orders", methods=['POST']) +def mod_boosted_concurrent_safety_orders(): + ''' + POST request + + Parameters: + base: str + quote: str + amount: int + ''' + + 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"] + amount = data["amount"] + return unwrapped_mod_boosted_concurrent_safety_orders(base,quote,amount) + except Exception as e: + print(e) + return jsonify({'Error': 'Halp'}) + + @base_api.route("/mod_default_order_size", methods=['POST']) def mod_default_order_size(): ''' @@ -1060,7 +1111,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 +1135,32 @@ 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("/force_trader_close", methods=['POST']) +def force_trader_close(): + ''' + POST request + + Parameters: + base: str + quote: str + ''' + + 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_force_trader_close(base,quote) + except Exception as e: + print(e) + return jsonify({'Error': 'Halp'}) + + +@base_api.route("/toggle_liquidate_after_switch", methods=['POST']) +def toggle_liquidate_after_switch(): ''' POST request @@ -1108,7 +1183,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 +1413,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 @@ -1453,7 +1505,7 @@ def unwrapped_add_pair(base,quote): #Check if the trader is already running for instance in running_traders: - if symbol==instance.config.get_pair(): + if symbol==instance.status.get_pair(): broker.logger.log_this(f"Pair already running",1,symbol) return jsonify({"Error": "Pair already running"}) @@ -1487,7 +1539,7 @@ def unwrapped_remove_pair(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + if symbol==instance.status.get_pair(): instance.quit = True return jsonify({"Success": "Pair to be removed"}) except Exception as e: @@ -1512,15 +1564,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 +1578,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) @@ -1556,7 +1606,7 @@ def unwrapped_switch_to_long(base,quote,calculate_profits): 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.config.get_pair(): + if f"{base}/{quote}"==instance.status.get_pair(): instance.pause = True if instance.switch_to_long(ignore_old_long=ignore_old_long)==1: return jsonify({"Error": "Error in switch_to_long()"}) @@ -1584,14 +1634,14 @@ def unwrapped_switch_to_short(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.config.get_pair() and instance.switch_to_short()==1: + 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.config.get_pair(): + if symbol==instance.status.get_pair(): instance.status.set_take_profit_order(instance.broker.empty_order) instance.so = instance.broker.empty_order @@ -1640,7 +1690,7 @@ def unwrapped_load_old_long(base,quote): #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.config.get_pair()==symbol: + 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"}) @@ -1667,7 +1717,7 @@ def unwrapped_view_old_long(base,quote,from_file): old_long = load(ol) return jsonify(old_long) for instance in running_traders: - if symbol==instance.config.get_pair(): + 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"}) @@ -1693,7 +1743,7 @@ def unwrapped_switch_to_long_price(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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"] @@ -1724,7 +1774,7 @@ def unwrapped_add_safety_orders(base,quote,amount): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + if symbol==instance.status.get_pair(): instance.pause = True #x.no_of_safety_orders += int(amount) instance.config.set_no_of_safety_orders(instance.config.get_no_of_safety_orders()+int(amount)) @@ -1755,7 +1805,7 @@ def unwrapped_base_add_so_calculation(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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"}) @@ -1783,7 +1833,7 @@ def unwrapped_mod_tp_level(base,quote,amount): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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"}) @@ -1808,8 +1858,9 @@ def unwrapped_mod_order_size(base,quote,amount): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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: @@ -1817,6 +1868,58 @@ def unwrapped_mod_order_size(base,quote,amount): return jsonify({"Error": "Error changing order size"}) +def unwrapped_mod_concurrent_safety_orders(base,quote,amount): + ''' + Modifies the amount of safety orders that a trader keeps opened at the same time. + + Parameters: + base (str): The base currency of the pair. + quote (str): The quote currency of the pair. + amount (str): The new amount. + + Returns: + jsonify: A jsonified dictionary detailing the outcome of the operation + ''' + + 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() + 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: + broker.logger.log_this("Error changing safety orders amount. Ignoring...",2,symbol) + return jsonify({"Error": "Error changing safety orders amount"}) + + +def unwrapped_mod_boosted_concurrent_safety_orders(base,quote,amount): + ''' + Modifies the amount of safety orders that a trader keeps opened at the same time while boosted. + + Parameters: + base (str): The base currency of the pair. + quote (str): The quote currency of the pair. + amount (str): The new amount. + + Returns: + jsonify: A jsonified dictionary detailing the outcome of the operation + ''' + + 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() + 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: + broker.logger.log_this("Error changing safety orders amount. Ignoring...",2,symbol) + return jsonify({"Error": "Error changing safety orders amount"}) + + def unwrapped_mod_default_order_size(amount): ''' Modifies the default order size of a broker. @@ -1872,7 +1975,7 @@ def unwrapped_last_call(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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(): @@ -1906,7 +2009,7 @@ def unwrapped_deferred_last_call(base,quote,yyyymmdd): if limit==0: return jsonify({"Error": "Can't convert date to unix"}) for instance in running_traders: - if f"{base}{quote}"==instance.config.get_pair(): + 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 @@ -1933,7 +2036,7 @@ def unwrapped_toggle_pause(base,quote): symbol = f"{base}/{quote}" toggle_pauses.append(symbol) for instance in running_traders: - if instance.config.get_pair()==symbol: + if instance.status.get_pair()==symbol: if instance.pause: instance.status.set_pause_reason("") return jsonify({"Success": "Trader will be resumed"}) @@ -1972,7 +2075,7 @@ def unwrapped_cancel_global_last_call(): 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"}) + return jsonify({"Success": "Last call canceled"}) except Exception: return jsonify({"Error": "Halp"}) @@ -1992,55 +2095,55 @@ def unwrapped_add_quote(base,quote,amount): ''' for instance in running_traders: - if f"{base}/{quote}"==instance.config.get_pair(): + 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.pause = True 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.config.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.config.get_pair()) - new_order = broker.new_market_order(instance.config.get_pair(),float(amount),"buy") + 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.config.get_pair()) + broker.logger.log_this("Error: Market order returned None",2,instance.status.get_pair()) instance.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.config.get_pair()) + 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.config.get_pair()) + broker.logger.log_this("Problems sending the order",2,instance.status.get_pair()) instance.pause = False return jsonify({"Error": "Problems sending the order"}) elif returned_order["status"]=="expired": instance.pause = False return jsonify({"Error": "New order expired"}) elif returned_order["status"]=="closed": - broker.logger.log_this("Order sent",2,instance.config.get_pair()) + 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.config.get_pair()) + 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.config.get_pair())==1: - broker.logger.log_this("Can't cancel old take profit order, retrying...",2,instance.config.get_pair()) + 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.config.get_pair()) + broker.logger.log_this("Can't cancel old take profit order, cancelling...",2,instance.status.get_pair()) instance.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.config.get_pair(),instance.status.get_base_bought(),"sell",instance.status.get_take_profit_price())) + 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.config.get_pair()) - broker.logger.log_this(f"{returned_order}",2,instance.config.get_pair()) + 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.pause = False - broker.logger.log_this("Done",2,instance.config.get_pair()) + broker.logger.log_this("Done",2,instance.status.get_pair()) return jsonify({"Success": "Quote added successfully"}) return jsonify({"Error": "Something horrible happened :S"}) @@ -2078,7 +2181,7 @@ def unwrapped_toggle_cleanup(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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"}) @@ -2089,6 +2192,32 @@ def unwrapped_toggle_cleanup(base,quote): return jsonify({"Error": "Task failed successfully"}) +def unwrapped_force_trader_close(base,quote): + ''' + Forces a trader to close the position. + + Parameters: + base (str): The base currency of the pair to close + quote (str): The quote currency of the pair to close + + Returns: + jsonify: A jsonified dictionary detailing the outcome of the operation. + ''' + + 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"}) + except Exception as e: + broker.logger.log_this(f"Exception while forcing trader to close position: {e}",1,symbol) + return jsonify({"Error": "Halp"}) + + def unwrapped_toggle_autoswitch(base,quote): ''' Signals a trader to enable or disable autoswitch. @@ -2104,7 +2233,7 @@ def unwrapped_toggle_autoswitch(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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) @@ -2113,6 +2242,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"}) @@ -2132,7 +2262,7 @@ def unwrapped_toggle_liquidate_after_switch(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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) @@ -2141,10 +2271,12 @@ 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"}) + def unwrapped_toggle_check_old_long_price(base,quote): ''' Signals to the trader if it should compare the current price to the old_long price stored in the old_long dictionary. @@ -2160,7 +2292,7 @@ def unwrapped_toggle_check_old_long_price(base,quote): try: symbol = f"{base}/{quote}" for instance in running_traders: - if symbol==instance.config.get_pair(): + 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) @@ -2169,6 +2301,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"}) @@ -2190,7 +2323,7 @@ def unwrapped_switch_quote_currency(base,quote,new_quote): try: symbol = f"{base}/{quote}" for trader in running_traders: - if symbol==trader.config.get_pair(): + if symbol==trader.status.get_pair(): #Pause the trader trader.pause = True @@ -2353,30 +2486,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): @@ -2413,7 +2523,7 @@ def unwrapped_reload_trader_config(base,quote): ''' symbol = f"{base}/{quote}" for trader in running_traders: - if trader.config.get_pair() == symbol: + if trader.status.get_pair() == symbol: if trader.config.load_from_file()==0: return jsonify({"Success": "Config file reloaded"}) return jsonify({"Error": "Error reloading config file"}) diff --git a/status_handler.py b/status_handler.py index d9d5d20..b219e4c 100644 --- a/status_handler.py +++ b/status_handler.py @@ -12,7 +12,8 @@ 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": [], + "safety_orders_filled": 0, "next_so_price": 0.0, "order_size": 0.0, "partial_profit": 0.0, @@ -23,7 +24,7 @@ class StatusHandler: "quote_spent": 0.0, "base_bought": 0.0, "so_amount": 0, - "no_of_safety_orders": "", + "no_of_safety_orders": 0, "safety_price_table": [], "deal_uptime": 0.0, "total_uptime": 0.0, @@ -58,8 +59,14 @@ 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_safety_orders_filled(self): + return self.status_dictionary["safety_orders_filled"] def get_next_so_price(self): return self.status_dictionary["next_so_price"] @@ -148,6 +155,10 @@ class StatusHandler: def get_status_file_path(self): return self.status_file_path + def set_pair(self, trading_pair): + self.pair = trading_pair + return 0 + def set_status_file_path(self, new_file_path: str): # if not isinstance(new_file_path, str): # self.broker.logger.log_this(f"File path provided is not a string",1,self.get_pair()) @@ -181,8 +192,15 @@ 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_safety_orders_filled(self, amount: int): + self.status_dictionary["safety_orders_filled"] = amount return 0 def set_next_so_price(self, price: float): @@ -381,6 +399,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/todo.txt b/todo.txt index 81a5c9e..81df0fd 100755 --- a/todo.txt +++ b/todo.txt @@ -1,32 +1,25 @@ Mandatory: ========= -1. Stats webpage. -2. Maintain local orderbooks for each trading pair, which enables: +0. Stats webpage. +1. Maintain local orderbooks for each trading pair, which enables: 2a. Smart order pricing: Prioritization of fill speed over instant profit or vice versa -3. Proper handling of order price too high/low in OKX (rare, it happens when under heavy volatility). -4. Multiple safety orders open at the same time (to catch big volatility spikes more effectively) -5. Things that should be objects (it's not 1994): - * Orders. - * Config (Mostly done). - * Status (Mostly done). -6. API documentation. -7. Implement api key hashing. -8. Dockerize. -9. Cache generated status strings, only recalculate when prices change. -10. Inspect orderbook liquidity prior to changing mode from short to long (big sell market order needs to have liquidity). +2. Proper handling of order price too high/low in OKX (rare, it happens when under heavy volatility). +3. API documentation. +4. Implement api key hashing. +5. Dockerize. +6. Earn should be integrated into the instance, in order to be able to invest the idle funds from the short traders. Would be nice to have: ===================== 0. Trader order: alphabetical; by uptime; by safety orders, by percentage_to_completion. (Although this may be more suitable for the web and mobile apps) 1. Local implementation of amount_to_precision, cost_to_precision and price_to_precision. (Unless the plan is to continue to use CCXT forever) -2. Instead of cancelling and resending the take profit order, you could just edit it (Kucoin only supports editing on high frequency orders) -3. Round-robin trading pairs: Instead of a fixed list of trading pairs, after n closed deals the trader is terminated and a new one spawns, picking the trading pair +2. Instead of cancelling and resending the take profit order, edit it (Kucoin only supports editing on high frequency orders) +3. When autoswitching to long, instead of using a big market order, the last safety order should be a sell order of all the available funds. +4. Round-robin trading pairs: Instead of a fixed list of trading pairs, after n closed deals the trader is terminated and a new one spawns, picking the trading pair from a pre-populated list (the trading pairs can be selected by using Yang-Zhang, Parkinson or another volatility indicator) This could be very benefitial, since it limits the long time commitment to a small list of trading pairs, enabling the instance to react to market trends very rapidly. -4. Earn should also use funds from short traders. - 4b. Should Earn be integrated to the instance? Maybe it's a good idea?: diff --git a/trader.py b/trader.py index 180c2f7..2e1c9b2 100755 --- a/trader.py +++ b/trader.py @@ -5,7 +5,7 @@ from config_handler import ConfigHandler from status_handler import StatusHandler class trader: - def __init__(self, broker, pair: str, is_import: bool = False, forced_tp_id = None, forced_so_id = None): + def __init__(self, broker, pair: str, is_import: bool = False): #Flags self.pause = True @@ -34,6 +34,7 @@ class trader: self.low_price_cache = None self.mid_price_cache = None self.high_price_cache = None + self.concurrent_so_amount_cache = None if self.config.get_is_short(): #Check if there is an old_long file. If so, load it. @@ -50,8 +51,11 @@ 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 + else: + #Only reloads the value from config file if the trader wasn't running previously. + self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) # An alternative would be to set up a variable like self.is_initalized to false and finish the initialization here. # Then, in the main loop, check if self.is_initalized is false. If it is, run start_trader. @@ -75,7 +79,7 @@ class trader: def __str__(self): return self.status.get_status_string() - + def get_color(self, color): ''' Returns white if color does not exist @@ -91,7 +95,6 @@ class trader: return colors[color] if color in colors else "\033[0;37;40m" - def get_status_dict(self): return self.status.get_status() @@ -104,15 +107,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: ''' @@ -120,84 +114,86 @@ class trader: ''' #Perhaps we should search for open buy orders from a crashed trader and cancel them? - #Reset some variables + #Resets some variables 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([]) + self.status.set_safety_orders_filled(0) + self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) #Reloads the market - new_market_data = self.broker.fetch_market(self.config.get_pair()) + new_market_data = self.broker.fetch_market(self.status.get_pair()) if new_market_data is not None: self.market = new_market_data self.pause = True self.status.set_pause_reason("start_trader") - if self.config.get_is_short(): - self.broker.logger.log_this("Calculating optimal order size...",2,self.config.get_pair()) + if self.status.get_is_short(): + self.broker.logger.log_this("Calculating optimal order size...",2,self.status.get_pair()) #Get minimum order size from exchange - self.broker.logger.log_this("Fetching minimum order size...",2,self.config.get_pair()) - min_base_size = self.broker.get_min_base_size(self.config.get_pair()) + self.broker.logger.log_this("Fetching minimum order size...",2,self.status.get_pair()) + min_base_size = self.broker.get_min_base_size(self.status.get_pair()) if min_base_size is None: - self.broker.logger.log_this("Can't fetch the minimum order size",1,self.config.get_pair()) + self.broker.logger.log_this("Can't fetch the minimum order size",1,self.status.get_pair()) return 1 #Fetch the amount of free base available on the exchange - self.broker.logger.log_this("Fetching free base currency on the exchange...",2,self.config.get_pair()) + self.broker.logger.log_this("Fetching free base currency on the exchange...",2,self.status.get_pair()) free_base = self.fetch_free_base() if free_base is None: - self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.config.get_pair()) + self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.status.get_pair()) return 1 #Buy missing base sold because of rounding errors (rare) if self.status.get_old_long()!={}: - diff = self.broker.amount_to_precision(self.config.get_pair(), self.status.get_old_long()["tp_amount"] - free_base) + diff = self.broker.amount_to_precision(self.status.get_pair(), self.status.get_old_long()["tp_amount"] - free_base) if diff>min_base_size: - self.broker.logger.log_this(f"Buying missing {diff} {self.base}",1,self.config.get_pair()) - self.broker.new_market_order(self.config.get_pair(),diff,"buy",amount_in_base=True) + self.broker.logger.log_this(f"Buying missing {diff} {self.base}",1,self.status.get_pair()) + self.broker.new_market_order(self.status.get_pair(),diff,"buy",amount_in_base=True) time.sleep(self.broker.get_wait_time()*2) #Re-quering for the amount of base currency on the exchange free_base = self.fetch_free_base() if free_base is None: - self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.config.get_pair()) + self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.status.get_pair()) return 1 #Calculate order size and amount of safety orders - self.broker.logger.log_this("Calculating the order size...",2,self.config.get_pair()) + self.broker.logger.log_this("Calculating the order size...",2,self.status.get_pair()) order_size,no_of_safety_orders = self.calculate_order_size(free_base,min_base_size,self.config.get_max_short_safety_orders()) if order_size is None or no_of_safety_orders is None: - self.broker.logger.log_this("Can't calculate optimal size",1,self.config.get_pair()) + self.broker.logger.log_this("Can't calculate optimal size",1,self.status.get_pair()) return 1 - self.config.set_order_size(order_size) - self.config.set_no_of_safety_orders(no_of_safety_orders) - self.broker.logger.log_this(f"Order size: {self.broker.amount_to_precision(self.config.get_pair(),order_size)}. Amount of safety orders: {no_of_safety_orders}",2,self.config.get_pair()) + self.status.set_order_size(order_size) + self.status.set_no_of_safety_orders(no_of_safety_orders) + self.broker.logger.log_this(f"Order size: {self.broker.amount_to_precision(self.status.get_pair(),order_size)}. Amount of safety orders: {no_of_safety_orders}",2,self.status.get_pair()) #Write the changes to the config file self.config.save_to_file() else: #Check order size self.status.set_pause_reason("start_trader - checking order size") - self.broker.logger.log_this("Checking for order size",2,self.config.get_pair()) - minimum_order_size_allowed = self.broker.get_min_quote_size(self.config.get_pair()) + self.broker.logger.log_this("Checking for order size",2,self.status.get_pair()) + minimum_order_size_allowed = self.broker.get_min_quote_size(self.status.get_pair()) if minimum_order_size_allowed is not None and minimum_order_size_allowed>self.config.get_order_size(): - self.broker.logger.log_this(f"Order size too small. Minimum order size is {minimum_order_size_allowed} {self.quote}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Order size too small. Minimum order size is {minimum_order_size_allowed} {self.quote}",1,self.status.get_pair()) if minimum_order_size_allowedself.status.get_safety_orders_filled(): + self.status.set_next_so_price(self.status.get_safety_price_table()[self.status.get_safety_orders_filled()]) + else: + self.status.set_next_so_price(0) 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()) + #self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) self.status.set_deal_uptime(int(time.time()) - self.status.get_deal_start_time()) self.status.set_total_uptime(int(time.time()) - self.status.get_start_time()) self.status.set_tp_mode(self.config.get_tp_mode()) @@ -325,7 +321,7 @@ class trader: self.status.set_autoswitch(self.config.get_autoswitch()) self.status.set_liquidate_after_switch(self.config.get_liquidate_after_switch()) except Exception as e: - self.broker.logger.log_this(f"Can't update status dictionary. Exception: {e}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Can't update status dictionary. Exception: {e}",1,self.status.get_pair()) return 1 if write_to_disk: @@ -409,7 +405,7 @@ class trader: # try: # fee_rate = self.market["maker"] if order["type"]=="limit" else self.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,self.config.get_pair()) + # self.broker.logger.log_this(f"Exception fetching market information: {e}. Using default fee rate of 0.1%",1,self.status.get_pair()) # fee_rate = 0.001 # # if order["side"]=="buy": @@ -443,22 +439,22 @@ class trader: balance_to_clean = self.fetch_free_base() if balance_to_clean is None: - self.broker.logger.log_this("Can't fetch free base",1,self.config.get_pair()) + self.broker.logger.log_this("Can't fetch free base",1,self.status.get_pair()) return 1 #balance_to_clean /= 2 #Maybe it's a good idea, sort of DCAing the dust. - min_base_size = self.broker.get_min_base_size(self.config.get_pair()) - minimum_cleanup_size = self.status.get_safety_order()["amount"]*2 # type: ignore + min_base_size = self.broker.get_min_base_size(self.status.get_pair()) + minimum_cleanup_size = self.status.get_safety_orders()[0]["amount"]*2 if balance_to_clean-minimum_cleanup_size >= min_base_size: - self.broker.logger.log_this(f"Balance to clean: {balance_to_clean-minimum_cleanup_size} {self.base}",2,self.config.get_pair()) - self.broker.logger.log_this("Sending cleanup order...",2,self.config.get_pair()) - cleanup_order = self.broker.new_limit_order(self.config.get_pair(),balance_to_clean-minimum_cleanup_size,"sell",self.status.get_take_profit_price()) + self.broker.logger.log_this(f"Balance to clean: {balance_to_clean-minimum_cleanup_size} {self.base}",2,self.status.get_pair()) + self.broker.logger.log_this("Sending cleanup order...",2,self.status.get_pair()) + cleanup_order = self.broker.new_limit_order(self.status.get_pair(),balance_to_clean-minimum_cleanup_size,"sell",self.status.get_take_profit_price()) if cleanup_order not in [None,self.broker.get_empty_order()]: - self.broker.logger.log_this("Cleanup successful",2,self.config.get_pair()) + self.broker.logger.log_this("Cleanup successful",2,self.status.get_pair()) return 0 - self.broker.logger.log_this("Problems with the cleanup order",1,self.config.get_pair()) + self.broker.logger.log_this("Problems with the cleanup order",1,self.status.get_pair()) return 1 - self.broker.logger.log_this("No cleanup needed",2,self.config.get_pair()) + self.broker.logger.log_this("No cleanup needed",2,self.status.get_pair()) return 0 @@ -472,16 +468,16 @@ class trader: while amount_of_so>minimum_amount_of_safety_orders: optimal_order_size = self.return_optimal_order_size(free_base,min_base_size,amount_of_so,self.config.get_safety_order_scale()) #safety_order_scale: safety order growth factor if optimal_order_size!=0: - self.broker.logger.log_this(f"Optimal order size is {optimal_order_size}",2,self.config.get_pair()) + self.broker.logger.log_this(f"Optimal order size is {optimal_order_size}",2,self.status.get_pair()) break amount_of_so-=1 if optimal_order_size==0: - self.broker.logger.log_this("Not enough base to switch. Order size would be too small",1,self.config.get_pair()) + self.broker.logger.log_this("Not enough base to switch. Order size would be too small",1,self.status.get_pair()) self.pause = False self.status.set_pause_reason("") return None,None if optimal_order_size int: ''' When profit is reached, this method is called to handle the profit calculations, the closing of orders, @@ -724,48 +748,62 @@ class trader: self.deals_timestamps.append(time.time()) self.deals_timestamps = self.deals_timestamps[-self.config.get_boosted_deals_range():] - #Let's do some type checking first if self.status.get_take_profit_order() is None: - self.status.set_pause_reason(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.config.get_pair()} | TP order is None")) - self.broker.logger.log_this("Error. Take profit order is None, trader will be restarted",0,self.config.get_pair()) + self.status.set_pause_reason(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.status.get_pair()} | TP order is None")) + self.broker.logger.log_this("Error. Take profit order is None, trader will be restarted",0,self.status.get_pair()) 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"],self.status.get_pair()) + + #Check if some safety orders were filled + partial_filled_amount = 0 + for order in self.status.get_safety_orders(): + closed_order = self.broker.get_order(order["id"],self.status.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 + partial_filled_amount+=closed_order["filled"] + #Better than this, the total filled and total cost can be used to send a sell market order of the partial filled amount, and add that to the profit. + self.broker.logger.log_this(f"Old safety order is partially filled, ID: {closed_order['id']}, {closed_order['filled']}/{closed_order['amount']} {self.base} filled",1,self.status.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 partial_filled_amount!=0 and partial_filled_amount>self.broker.get_min_base_size(self.status.get_pair()): + # #send a sell market order and sum the profits + # market_order = self.broker.new_market_order(self.status.get_pair(),partial_filled_amount,"sell",amount_in_base=True) + # #Wait for it to be filled + # tries = self.broker.get_retries() + # while True: + # time.sleep(self.broker.get_wait_time()) + # partial_fill_order = self.broker.get_order(market_order["id"],self.status.get_pair()) + # if partial_fill_order["status"]=="closed": + # break + # tries-=1 + # if tries==0: + # self.broker.logger.log_this("Partial fill sell order not filling.",1,self.status.get_pair()) + # break + # #Sum the profit + + if not self.broker.check_for_duplicate_profit_in_db(filled_order): self.status.set_pause_reason("calculating profit") # Calculate the profit @@ -774,6 +812,9 @@ class trader: else: profit = filled_order["cost"]-self.status.get_quote_spent()-self.status.get_fees_paid_in_quote()-self.parse_fees(filled_order)[1] profit+=self.status.get_partial_profit() + + #Reset partial profit + self.status.set_partial_profit(0) #Checks if some base was left over. base_profit = max(self.status.get_base_bought()-filled_order["filled"],0) #To avoid negative numbers in base_profit @@ -782,14 +823,14 @@ class trader: if profit>0: #Negative profits are not saved because the cleanup takes care of the unsold base currency (the notorious small change issue that plagues some exchanges) self.profit_to_db(profit,filled_order["id"],self.broker.get_write_order_history()) else: #For logging purposes - self.broker.logger.log_this(f"NEGATIVE PROFIT - Total amount of base: {self.status.get_base_bought()}, base in the order: {filled_order['amount']}, base filled: {filled_order['filled']}, base 'profit': {base_profit}",1,self.config.get_pair()) + self.broker.logger.log_this(f"NEGATIVE PROFIT - Total amount of base: {self.status.get_base_bought()}, base in the order: {filled_order['amount']}, base filled: {filled_order['filled']}, base 'profit': {base_profit}",1,self.status.get_pair()) self.telegram_bot_sendprofit(profit,filled_order,base_profit=base_profit) # Print profit message on screen extra = ' and {:.4f}'.format(base_profit) + f" {self.base}" if base_profit>0 else "" - self.broker.logger.log_this(f"Trader closed a deal. Profit: {'{:.4f}'.format(profit)} {self.quote}{extra}",2,self.config.get_pair()) - self.broker.logger.log_this(f"Fill price: {filled_order['price']} {self.quote}",2,self.config.get_pair()) - self.broker.logger.log_this(f"Safety orders triggered: {self.status.get_so_amount()-1}",2,self.config.get_pair()) + self.broker.logger.log_this(f"Trader closed a deal. Profit: {'{:.4f}'.format(profit)} {self.quote}{extra}",2,self.status.get_pair()) + self.broker.logger.log_this(f"Fill price: {filled_order['price']} {self.quote}",2,self.status.get_pair()) + self.broker.logger.log_this(f"Safety orders triggered: {self.status.get_safety_orders_filled()}",2,self.status.get_pair()) self.status.set_pause_reason("take_profit_routine - check time limit") #Checks if there is a time limit for the trader @@ -798,7 +839,7 @@ class trader: self.status.set_pause_reason("take_profit_routine - if stop_when_profit") if self.status.get_stop_when_profit(): #Signal to stop when trade is closed - self.broker.logger.log_this("Pair shutting down. So long and thanks for all the fish",0,self.config.get_pair()) + self.broker.logger.log_this("Pair shutting down. So long and thanks for all the fish",0,self.status.get_pair()) self.quit = True return 1 @@ -807,18 +848,18 @@ class trader: self.status.set_pause_reason("Checking slippage") if self.config.get_check_slippage(): - self.broker.logger.log_this("Checking slippage...",2,self.config.get_pair()) - price_to_compare = self.broker.get_top_bid_price(self.config.get_pair()) if self.config.get_is_short() else self.broker.get_top_ask_price(self.config.get_pair()) + self.broker.logger.log_this("Checking slippage...",2,self.status.get_pair()) + price_to_compare = self.broker.get_top_bid_price(self.status.get_pair()) if self.config.get_is_short() else self.broker.get_top_ask_price(self.status.get_pair()) if abs(filled_order["price"]-price_to_compare)/filled_order["price"]>self.broker.get_slippage_default_threshold(): - self.broker.logger.log_this(f"Slippage threshold exceeded, waiting for cooldown and restarting trader",1,self.config.get_pair()) + self.broker.logger.log_this(f"Slippage threshold exceeded, waiting for cooldown and restarting trader",1,self.status.get_pair()) time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier()) #The trader is restarted by the instance instead of by itself to allow a couple of more seconds for the price to return to normal. #This could also be the default behavior. self.pause = False self.restart = True return 1 - elif self.check_orderbook_depth(self.broker.get_slippage_default_threshold(),self.config.get_order_size(),filled_order["price"]): - self.broker.logger.log_this(f"Orderbook depth not sufficient, waiting for cooldown and restarting trader",1,self.config.get_pair()) + elif self.check_orderbook_depth(self.broker.get_slippage_default_threshold(),self.status.get_order_size(),filled_order["price"]): + self.broker.logger.log_this(f"Orderbook depth not sufficient, waiting for cooldown and restarting trader",1,self.status.get_pair()) time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier()) self.pause = False self.restart = True @@ -833,104 +874,174 @@ class trader: self.pause = False self.restart = True self.status.save_to_file(is_backup=True) - self.broker.logger.log_this(self.trader_restart_errors[restart_trader],1,self.config.get_pair()) + self.broker.logger.log_this(self.trader_restart_errors[restart_trader],1,self.status.get_pair()) return restart_trader + + 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. + """ + + if amount<1: + return 0 + + orders_to_place = min(self.status.get_no_of_safety_orders()-self.status.get_so_amount(),amount) + if orders_to_place<1: + return 0 + + orders_placed = 0 + # if self.broker.get_exchange_name()!="binance": #Binance does not support sending multiple orders at once in Spot. + # self.broker.logger.log_this(f"Sending {orders_to_place} safety orders",2,self.status.get_pair()) + # orders_to_send = [] + # for i in range(orders_to_place): + # order_index = self.status.get_so_amount()+i+1 + # so_size = self.gib_so_size(self.status.get_order_size(),order_index,self.config.get_safety_order_scale()) + # if self.config.get_is_short(): + # orders_to_send.append({"symbol": self.status.get_pair(), + # "type": "limit", + # "side": "sell", + # "amount": so_size, + # "price": self.status.get_safety_price_table()[order_index]}) + # else: + # orders_to_send.append({"symbol": self.status.get_pair(), + # "type": "limit", + # "side": "buy", + # "amount": so_size/self.status.get_safety_price_table()[order_index], + # "price": self.status.get_safety_price_table()[order_index]}) + # sent_orders = self.broker.new_limit_orders(orders_to_send) + # orders_placed = len(sent_orders) + # self.status.set_so_amount(self.status.get_so_amount()+orders_placed) + # for item in sent_orders: + # self.status.add_safety_order(item) + # else: + for i in range(orders_to_place): + self.broker.logger.log_this(f"Sending a new safety order ({i+1}/{orders_to_place})",2,self.status.get_pair()) + 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.status.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.status.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]) - def new_so_routine(self, filled_order: dict, send_new_so: bool) -> int: + if new_order==1: + self.broker.logger.log_this("Not enough balance to send a new safety order",1,self.status.get_pair()) + self.status.set_no_of_safety_orders(self.status.get_so_amount()) #To avoid sending more safety orders, no_of_safety_order can be later modified manually. + return orders_placed + elif new_order is None: + self.broker.logger.log_this("new_limit_order returned None",1,self.status.get_pair()) + self.status.set_no_of_safety_orders(self.status.get_so_amount()) #To avoid sending more safety orders, no_of_safety_order can be later modified manually. + return orders_placed + elif new_order==self.broker.get_empty_order(): + self.broker.logger.log_this("new_limit_order returned an empty order",1,self.status.get_pair()) + self.status.set_no_of_safety_orders(self.status.get_so_amount()) #To avoid sending more safety orders, no_of_safety_order can be later modified manually. + return orders_placed + else: + 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): ''' - 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 + 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 ''' - #Let's do some type checking first + 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 - + self.broker.logger.log_this("Take profit order is None, can't send a new safety order",1,self.status.get_pair()) + 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 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()) + #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"]) - # 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 - 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()) + #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) + + #Cancel old TP order + if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())==1: + error_string = f"{self.status.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.status.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 - # 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()) + return 2 + + #Check if old TP order was partially filled + old_tp_order = self.broker.get_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) if old_tp_order["filled"]>0: - self.broker.logger.log_this(f"Old take profit order is partially filled, id {old_tp_order['id']}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Old take profit order is partially filled, id {old_tp_order['id']}",1,self.status.get_pair()) if self.broker.get_follow_order_history(): self.status.update_deal_order_history(old_tp_order) #self.status.set_base_bought(old_tp_order["remaining"]) # Partial profit calculation - #if not self.config.get_is_short(): - # current_deal_price = self.status.get_base_bought()/self.status.get_quote_spent() - # 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. - # + if not self.config.get_is_short(): + current_deal_price = self.status.get_quote_spent()/self.status.get_base_bought() + 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) 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]) - - #Cooldown - time.sleep(self.broker.get_wait_time()) + self.status.set_fees_paid_in_base(self.status.get_fees_paid_in_base() + self.parse_fees(old_tp_order)[0]) - # Send the new take profit order + #Cooldown + time.sleep(self.broker.get_wait_before_new_safety_order()) + + #Send new SO(s) + #Do not send new orders if the max amount is reached or surpassed. + #It can happen if the max amount of concurrent orders is modified through an API call. + max_orders = self.config.get_concurrent_safety_orders() if not self.status.get_is_boosted() else self.config.get_boosted_concurrent_safety_orders() + if len(self.status.get_safety_orders()) int: ''' Checks if short price exceeds old long price. If so, send a Telegram message ''' + price_exceeds = False if self.status.get_old_long()!={}: price_exceeds = self.status.get_price()>float(self.status.get_old_long()["tp_price"]) @@ -944,7 +1055,7 @@ class trader: self.warnings["speol_notified"] = True if not self.config.get_autoswitch(): message = f"{self.base}@{self.status.get_price()} ({str(self.broker.exchange)}), exceeds old long price of {self.status.get_old_long()['tp_price']}" - self.broker.logger.log_this(message,0,self.config.get_pair()) + self.broker.logger.log_this(message,0,self.status.get_pair()) return 0 @@ -965,9 +1076,9 @@ class trader: if self.config.get_is_short(): #Do not check for slippage in short traders (Pending to be implemented) return False - order_book = self.broker.get_order_book(self.config.get_pair(),no_retries=True) + order_book = self.broker.get_order_book(self.status.get_pair(),no_retries=True) if order_book=={}: - self.broker.logger.log_this("Can't fetch orderbook",1,self.config.get_pair()) + self.broker.logger.log_this("Can't fetch orderbook",1,self.status.get_pair()) return False suma = 0 try: @@ -987,7 +1098,7 @@ class trader: return True return False except Exception as e: - self.broker.logger.log_this(f"Exception in check_orderbook_depth: {e}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Exception in check_orderbook_depth: {e}",1,self.status.get_pair()) return False @@ -995,7 +1106,7 @@ class trader: ''' Main routine. It checks for closed orders and proceeds accordingly. ''' - + #It does not even try if it receives an empty list or the worker is paused if open_orders==[] or self.pause: self.update_status(False) @@ -1022,97 +1133,136 @@ class trader: return 1 #Extract ids from order list - open_orders_ids = [order["id"] for order in open_orders if order["symbol"]==self.config.get_pair()] + self.status.set_pause_reason("filtering open orders") + open_orders_list = [order for order in open_orders if order["symbol"]==self.status.get_pair()] + open_orders_ids = [order["id"] for order in open_orders_list] - #Checks if the orders are valid + self.status.set_pause_reason("check for tp_order is 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()) + self.broker.logger.log_this("Take profit order is None",1,self.status.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()) + self.broker.logger.log_this(f"Take profit order missing. Stopping trader. No order ID was provided.",1,self.status.get_pair()) + #Cancelling safety orders + for item in self.status.get_safety_orders(): + self.broker.cancel_order(item["id"],self.status.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True - self.broker.logger.log_this("Raising restart flag: take profit order missing, trader will be restarted",0,self.config.get_pair()) + self.broker.logger.log_this("Raising restart flag: take profit order missing, trader will be restarted",0,self.status.get_pair()) else: - self.broker.logger.log_this("Take profit order missing. Trader restart disabled.",2,self.config.get_pair()) + self.broker.logger.log_this("Take profit order missing. Trader restart disabled.",2,self.status.get_pair()) return 1 + self.status.set_pause_reason("check if tp_order is filled") #Checks if the take profit order is filled if self.status.get_take_profit_order()["id"] not in open_orders_ids: - tp_status = self.broker.get_order(self.status.get_take_profit_order()["id"],self.config.get_pair()) + # I hate Kucoin: + # Check if the order has a wrong id. If so, update the order. + for order in open_orders_list: + if order["amount"]==self.status.get_take_profit_order()["amount"] and order["price"]==self.status.get_take_profit_order()["price"] and order["side"]==self.status.get_take_profit_order()["side"]: + #Right order, wrong id. Update order + self.broker.logger.log_this(f"Updating take profit order for {self.status.get_pair()}") + self.status.set_take_profit_order(order) + self.update_status(True) + return 0 + + tp_status = self.broker.get_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) if tp_status["status"]=="closed": 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()) + 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.status.get_pair()) + #Cancelling safety orders + for item in self.status.get_safety_orders(): + self.broker.cancel_order(item["id"],self.status.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True - self.broker.logger.log_this("Take profit order closed but not filled, trader will be restarted.",0,self.config.get_pair()) + self.broker.logger.log_this("Take profit order closed but not filled, trader will be restarted.",0,self.status.get_pair()) else: - self.broker.logger.log_this("Take profit order closed but not filled, trader restart disabled.",1,self.config.get_pair()) + self.broker.logger.log_this("Take profit order closed but not filled, trader restart disabled.",1,self.status.get_pair()) return 1 elif tp_status["status"]=="canceled": - #TODO: Here, if the safety order is still open, we could resend the tp order. - if self.config.get_attempt_restart(): - self.broker.logger.log_this("Take profit order canceled. Restarting the trader.",1,self.config.get_pair()) - self.status.save_to_file(is_backup=True) - self.restart = True - else: - self.broker.logger.log_this("Take profit order canceled. Trader restart disabled.",1,self.config.get_pair()) - return 1 + #TODO: Here, if the safety order is still open, we could resend the tp order. + if self.config.get_attempt_restart(): + self.broker.logger.log_this("Take profit order canceled. Restarting the trader.",1,self.status.get_pair()) + self.status.save_to_file(is_backup=True) + self.restart = True + else: + self.broker.logger.log_this("Take profit order canceled. Trader restart disabled.",1,self.status.get_pair()) + return 1 elif tp_status["status"]=="": - self.broker.logger.log_this(f"Take profit order search returned empty order. Order ID: {tp_status['id']}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Take profit order search returned empty order. Order ID: {tp_status['id']}",1,self.status.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() - 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()0: #To make sure that the safety orders are actually filled (Kucoin sometimes sends incomplete order lists) + self.status.set_safety_orders_filled(self.status.get_safety_orders_filled()+len(filled_orders)) + 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.status.get_pair()) + if self.config.get_attempt_restart(): + self.status.save_to_file(is_backup=True) + self.restart = True return 1 - elif a==2: - self.broker.logger.log_this(f"Can't cancel old take profit order. new_so_routine returned {a}",1,self.config.get_pair()) + elif renew_outcome==2: + #Not enough funds? + self.broker.logger.log_this(f"Can't send new safety order. Not enough funds? renew_tp_and_so_routine returned 2",1,self.status.get_pair()) + #Set no_of_safety_orders to the same amount of orders open so the script does not try to send new safety orders + #This can be improved + self.status.set_no_of_safety_orders(self.status.get_so_amount()) + return 1 + elif renew_outcome==3: + self.broker.logger.log_this(f"Can't cancel old take profit order. renew_tp_and_so_routine returned 3",1,self.status.get_pair()) self.pause = False self.status.set_pause_reason("") if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True return 1 - elif a==3: - #self.pause = False - self.broker.logger.log_this(f"Error in trader: Can't send new take profit order. Restart will be attempted. new_so_routine returned {a}",0,self.config.get_pair()) + elif renew_outcome==4: + self.broker.logger.log_this(f"Error in trader: Can't send new take profit order. Restart will be attempted. renew_tp_and_so_routine returned 4",0,self.status.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True return 1 - #else: check if the order is partially filled. If so, add the amounts to amount_of_quote and amount_of_base and update the take profit order. + + #Should we send more safety orders without touching the TP order? + #Necessary check if we add to no_of_safety_orders or modify concurrent_safety_orders at runtime + max_concurrent_safety_orders = self.config.get_boosted_concurrent_safety_orders() if self.status.get_is_boosted() else self.config.get_concurrent_safety_orders() + condition_a = len(self.status.get_safety_orders())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.status.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()) @@ -1149,7 +1299,7 @@ class trader: if self.config.get_is_short() or self.config.get_tp_mode()==0: #Fixed take profit percentage tp_level = self.config.get_tp_level() elif self.config.get_tp_mode()==1: #Variable percentage - limit = self.config.get_no_of_safety_orders()/3 + limit = self.status.get_no_of_safety_orders()/3 if order_index<=1: tp_level = self.config.get_tp_level()+0.005 elif order_index<=limit: @@ -1165,7 +1315,7 @@ class trader: tp_level = self.config.get_tp_table()[-1] 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.config.get_no_of_safety_orders()) + 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] if order_index0: if self.status.get_base_bought()==0: - self.broker.logger.log_this("Amount of base equals 0, can't send take profit order",1,self.config.get_pair()) + self.broker.logger.log_this("Amount of base equals 0, can't send take profit order",1,self.status.get_pair()) return 1 if self.config.get_is_short(): self.status.set_take_profit_price(self.status.get_quote_spent()/self.status.get_base_bought()*(1-(self.get_tp_level(self.status.get_so_amount())-1))) - self.status.set_take_profit_order(self.broker.new_limit_order(self.config.get_pair(),self.status.get_base_bought(),"buy",self.status.get_take_profit_price())) + self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"buy",self.status.get_take_profit_price())) else: self.status.set_take_profit_price(self.status.get_quote_spent()/self.status.get_base_bought()*self.get_tp_level(self.status.get_so_amount())) - self.status.set_take_profit_order(self.broker.new_limit_order(self.config.get_pair(),self.status.get_base_bought(),"sell",self.status.get_take_profit_price())) + self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"sell",self.status.get_take_profit_price())) if self.status.get_take_profit_order()==1: #This means that there was a miscalculation of base currency amount, let's correct it. if self.config.get_is_short(): #If in short mode, we don't recalculate anything. return 1 @@ -1222,7 +1372,7 @@ class trader: return 0 tries-=1 time.sleep(self.broker.get_wait_time()) - self.broker.logger.log_this("Problems sending take profit order",1,self.config.get_pair()) + self.broker.logger.log_this("Problems sending take profit order",1,self.status.get_pair()) return 1 @@ -1234,50 +1384,28 @@ class trader: while retries>0: try: order_history = dumps(self.status.get_deal_order_history()) if write_deal_order_history else "" - dataset = (time.time(),self.config.get_pair(),amount,self.broker.get_exchange_name(),str(orderid),order_history) + dataset = (time.time(),self.status.get_pair(),amount,self.broker.get_exchange_name(),str(orderid),order_history) #Write profit to cache self.broker.write_profit_to_cache(dataset) return self.broker.write_profit_to_db(dataset) except Exception as e: - self.broker.logger.log_this(f"Exception while writing profit: {e}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Exception while writing profit: {e}",1,self.status.get_pair()) retries-=1 time.sleep(.1) #Shorter wait time since it's not an API call 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 ''' try: extra = f" and {round(base_profit,6)} {self.base}" if base_profit>0 else "" - message = f"{self.config.get_pair()} closed a {'short' if self.config.get_is_short() else 'long'} trade.\nProfit: {round(profit,6)} {self.quote}{extra}\nSafety orders triggered: {self.status.get_so_amount()-1}\nTake profit price: {order['price']} {self.quote}\nTrade size: {round(order['cost'],2)} {self.quote}\nDeal uptime: {self.seconds_to_time(self.status.get_deal_uptime())}\nOrder ID: {order['id']}\nExchange: {self.broker.get_exchange_name().capitalize()}\n" + message = f"{self.status.get_pair()} closed a {'short' if self.config.get_is_short() else 'long'} trade.\nProfit: {round(profit,6)} {self.quote}{extra}\nSafety orders triggered: {self.status.get_safety_orders_filled()}\nTake profit price: {order['price']} {self.quote}\nTrade size: {round(order['cost'],2)} {self.quote}\nDeal uptime: {self.seconds_to_time(self.status.get_deal_uptime())}\nOrder ID: {order['id']}\nExchange: {self.broker.get_exchange_name().capitalize()}\n" self.broker.logger.send_tg_message(message) return 0 except Exception as e: - self.broker.logger.log_this(f"Exception in telegram_bot_sendprofit: {e}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Exception in telegram_bot_sendprofit: {e}",1,self.status.get_pair()) return 1 @@ -1343,42 +1471,40 @@ class trader: #First let's check if the market exists market = self.broker.fetch_market(f"{self.base}/{new_quote}") if market is None: - self.broker.logger.log_this("Market might not exist",1,self.config.get_pair()) + self.broker.logger.log_this("Market might not exist",1,self.status.get_pair()) return 1 if "active" in market and not market["active"]: - self.broker.logger.log_this("Market is closed",1,self.config.get_pair()) + self.broker.logger.log_this("Market is closed",1,self.status.get_pair()) return 1 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()) + self.broker.logger.log_this("Take profit order is None",1,self.status.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()) + self.broker.logger.log_this("Replacing take profit order",2,self.status.get_pair()) self.status.set_take_profit_order(self.quote_currency_replace_order(self.status.get_take_profit_order(),new_quote)) 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(): - 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 + #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.status.get_pair()) + 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()) + self.broker.logger.log_this("Modifying config file",2,self.status.get_pair()) self.quote_currency_switch_configs(new_quote) #Updates status_dict - self.broker.logger.log_this("Updating status file",2,self.config.get_pair()) + self.broker.logger.log_this("Updating status file",2,self.status.get_pair()) self.status.set_status_file_path(f"status/{self.base}{self.quote}.status") self.update_status(True) #Done - self.broker.logger.log_this("Quote swap successful",2,self.config.get_pair()) + self.broker.logger.log_this("Quote swap successful",2,self.status.get_pair()) return 0 @@ -1387,8 +1513,8 @@ class trader: Cancels the order and returns the new updated order ''' #Cancels the old order - if self.broker.cancel_order(old_order["id"],self.config.get_pair())==1: - self.broker.logger.log_this(f"Can't cancel old order {old_order['id']}",1,self.config.get_pair()) + if self.broker.cancel_order(old_order["id"],self.status.get_pair())==1: + self.broker.logger.log_this(f"Can't cancel old order {old_order['id']}",1,self.status.get_pair()) return self.broker.get_empty_order() #Sends the new order @@ -1404,7 +1530,7 @@ class trader: self.broker.add_pair_to_config(f"{self.base}{new_quote}") if self.broker.rewrite_config_file()==1: #Error writing broker config file, undoing changes - self.broker.logger.log_this("Error writing new broker config file",1,self.config.get_pair()) + self.broker.logger.log_this("Error writing new broker config file",1,self.status.get_pair()) self.quote_currency_undo_changes(new_quote,self.quote,False) return 1 @@ -1412,6 +1538,7 @@ class trader: old_quote = self.quote self.quote = new_quote self.config.set_pair(f"{self.base}/{self.quote}") + self.status.set_pair(f"{self.base}/{self.quote}") self.profit_filename = f"profits/{self.base}{self.quote}.profits" self.log_filename = f"logs/{self.base}{self.quote}.log" @@ -1421,12 +1548,12 @@ class trader: with open(f"status/{self.base}{self.quote}.oldlong","w") as c: c.write(dumps(self.status.get_old_long(), indent=4)) except Exception as e: - self.broker.logger.log_this(f"Exception while writing new old_long file: {e}",1,self.config.get_pair()) + self.broker.logger.log_this(f"Exception while writing new old_long file: {e}",1,self.status.get_pair()) #Write the new config file self.config.set_config_file_path(f"configs/{self.base}{self.quote}.json") if self.config.save_to_file()==1: - self.broker.logger.log_this(f"Error while writing the new trader config file",1,self.config.get_pair()) + self.broker.logger.log_this(f"Error while writing the new trader config file",1,self.status.get_pair()) #Undoing changes self.quote_currency_undo_changes(new_quote,old_quote,True) return 1 @@ -1445,12 +1572,13 @@ class trader: self.broker.add_pair_to_config(f"{self.base}{self.quote}") self.config.set_pair(f"{self.base}/{self.quote}") + self.status.set_pair(f"{self.base}/{self.quote}") self.profit_filename = f"profits/{self.base}{self.quote}.profits" self.log_filename = f"logs/{self.base}{self.quote}.log" #Writing config file if write_broker_file and self.broker.rewrite_config_file()==1: - self.broker.logger.log_this("Error in quote_currency_undo_changed: error writing new broker config file",1,self.config.get_pair()) + self.broker.logger.log_this("Error in quote_currency_undo_changed: error writing new broker config file",1,self.status.get_pair()) #Done return 0 @@ -1488,8 +1616,9 @@ class trader: low_price = self.status.get_next_so_price() if self.status.get_next_so_price() is not None else 0 mid_price = self.status.get_price() if self.status.get_price() is not None else 0 high_price = self.status.get_take_profit_price() if self.status.get_take_profit_price() is not None else 0 + concurrent_so_amount = len(self.status.get_safety_orders()) - if low_price==self.low_price_cache and mid_price==self.mid_price_cache and high_price==self.high_price_cache: + if low_price==self.low_price_cache and mid_price==self.mid_price_cache and high_price==self.high_price_cache and concurrent_so_amount==self.concurrent_so_amount_cache: #Only modifies the uptime position = self.status.get_status_string().find("Uptime") new_uptime = self.seconds_to_time(self.status.get_deal_uptime()) @@ -1499,6 +1628,7 @@ class trader: self.low_price_cache = low_price self.mid_price_cache = mid_price self.high_price_cache = high_price + self.concurrent_so_amount_cache = concurrent_so_amount #Formatting low_boundary = '{:.20f}'.format(low_price)[:decimals].center(decimals) @@ -1559,9 +1689,9 @@ class trader: except Exception as e: print(e) - safety_order_string = f"{self.status.get_so_amount()-1}/{self.config.get_no_of_safety_orders()}".rjust(5) + safety_order_string = f"{self.status.get_safety_orders_filled()}/{self.get_color('cyan')}{concurrent_so_amount}{self.get_color('white')}/{self.status.get_no_of_safety_orders()}".rjust(27) prices = f"{low_boundary_color}{low_boundary}{self.get_color('white')}|{price_color}{mid_boundary}{self.get_color('white')}|{target_price_color}{high_boundary}{self.get_color('white')}|{pct_color}{pct_to_profit_str}%{self.get_color('white')}" - line1 = f"{p}{pair_color}{self.config.get_pair().center(13)}{self.get_color('white')}| {safety_order_string} |{prices}| Uptime: {self.seconds_to_time(self.status.get_deal_uptime())}" + line1 = f"{p}{pair_color}{self.status.get_pair().center(13)}{self.get_color('white')}| {safety_order_string} |{prices}| Uptime: {self.seconds_to_time(self.status.get_deal_uptime())}" if self.status.get_is_boosted(): line1 = f"{line1} | BOOSTED" if self.config.get_autoswitch(): @@ -1582,36 +1712,25 @@ 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 ''' #Load status dict if self.status.load_from_file()==1: - self.broker.logger.log_this(f"Error: Couldn't load status dict. Aborting",1,self.config.get_pair()) + self.broker.logger.log_this(f"Error: Couldn't load status dict. Aborting",1,self.status.get_pair()) self.quit = True return 1 self.status.set_pause_reason("Importing 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 - self.status.set_take_profit_order(self.broker.get_order(order_id,self.config.get_pair())) + order_id = self.status.get_take_profit_order()["id"] + self.status.set_take_profit_order(self.broker.get_order(order_id,self.status.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.broker.logger.log_this("Couldn't load take profit order (broker returned empty order). Aborting.",1,self.status.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()