diff --git a/changelog.txt b/changelog.txt index 871f2d2..178c5f0 100755 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,9 @@ +2025.03.01: +. StatusHandler initial implementation. +. Variable cleanup + 2025.02.27 -. config_handler: centralized configuration handling in an object, for easier future development (parameter validation, for example) +. ConfigHandler: centralized configuration handling in an object, for easier future development (parameter validation, for example) . Bugfixes everywhere. . Exchange_wrapper now validates every symbol against the market information from the exchange, both when adding and importing a trader. diff --git a/main.py b/main.py index 4535850..d9fd030 100644 --- a/main.py +++ b/main.py @@ -24,7 +24,7 @@ In case the permissions of the certificate changes, reset them this way: # ll /etc/letsencrypt/ ''' -version = "2025.02.27" +version = "2025.03.01" ''' Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors @@ -246,10 +246,10 @@ def restart_pair_no_json(base: str, quote: str) -> int: x.write_status_file(True) #Here, we could open a duster (if needed) for order in order_list: - if order["symbol"]==f"{base}/{quote}" and x.is_short and order["side"]=="sell": + if order["symbol"]==f"{base}/{quote}" and x.config.get_is_short() and order["side"]=="sell": broker.logger.log_this(f"Cancelling old sell orders",2,f"{base}/{quote}") broker.cancel_order(order["id"],order["symbol"]) - elif order["symbol"]==f"{base}/{quote}" and not x.is_short and order["side"]=="buy": + elif order["symbol"]==f"{base}/{quote}" and not x.config.get_is_short() and order["side"]=="buy": broker.logger.log_this(f"Cancelling old buy orders",2,f"{base}/{quote}") broker.cancel_order(order["id"],order["symbol"]) running_instances.remove(x) @@ -328,7 +328,7 @@ def main_loop(): curr = 0 top = 0 for x in running_instances: - if not x.is_short: + if not x.config.get_is_short(): curr += int(x.get_status_dict()["so_amount"]) # For the safety order occupancy percentage calculation top += int(x.config.get_no_of_safety_orders()) # It shows the percentage of safety orders not filled if not x.quit: #Why? Maybe to protect return_status() from weird errors if the trader errored out? @@ -337,8 +337,6 @@ def main_loop(): x.get_status_dict()["price"] = price_list[x.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,x.pair) - #if "status_string" in x.status_dict: - # screen_buffer.append(x.status_dict["status_string"]) worker_status[x.pair] = x.get_status_dict() #Clear the screen buffer @@ -346,12 +344,10 @@ def main_loop(): #Append worker data to screen buffer, shorts first. for x in running_instances: - if x.is_short and "status_string" in x.get_status_dict(): - #screen_buffer.append(x.status_dict["status_string"]) + if x.config.get_is_short() and "status_string" in x.get_status_dict(): screen_buffer.append(str(x)) for x in running_instances: - if not x.is_short and "status_string" in x.get_status_dict(): - #screen_buffer.append(x.status_dict["status_string"]) + if not x.config.get_is_short() and "status_string" in x.get_status_dict(): screen_buffer.append(str(x)) #Updates some global status variables prior to deletion of those @@ -369,8 +365,6 @@ def main_loop(): #Prints general info instance_uptime = int(time.time()) - instance_start_time - #long_traders = len([None for x in running_instances if not x.is_short]) - #short_traders = len([None for x in running_instances if x.is_short]) screen_buffer.append(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {len(running_instances)} traders online | Instance uptime: {seconds_to_time(instance_uptime)}")) if top==0: #To protect from division by zero when there are no traders active @@ -1455,13 +1449,8 @@ def unwrapped_switch_to_short(base,quote): broker.logger.log_this(f"Reinitializing trader",2,f"{base}/{quote}") for x in running_instances: if f"{base}/{quote}"==x.pair: - #Clearing some variables - #x.fees_paid_in_quote = 0 - #x.fees_paid_in_base = 0 - x.tp_order = x.broker.empty_order + x.status.set_take_profit_order(x.broker.empty_order) x.so = x.broker.empty_order - #x.safety_price_table.clear() #This clearing (probably) is the origin of the update_status error after switching to short. - #x.safety_order_index = 0 #Reloading config file x.config.load_from_file() @@ -1593,7 +1582,7 @@ def unwrapped_add_safety_orders(base,quote,amount): #x.no_of_safety_orders += int(amount) x.config.set_no_of_safety_orders(x.config.get_no_of_safety_orders()+int(amount)) broker.logger.log_this("Recalculating safety price table...",1,f"{base}/{quote}") - x.safety_price_table = x.calculate_safety_prices(x.start_price,x.config.get_no_of_safety_orders(),x.config.get_safety_order_deviance()) + x.status.set_safety_price_table(x.calculate_safety_prices(x.status.get_start_price(),x.config.get_no_of_safety_orders(),x.config.get_safety_order_deviance())) broker.logger.log_this(f"Done. Added {amount} safety orders",1,f"{base}/{quote}") x.update_status(True) x.pause = False @@ -1783,10 +1772,10 @@ def unwrapped_add_quote(base,quote,amount): for x in running_instances: if f"{base}/{quote}"==x.pair: - if x.is_short: + if x.config.get_is_short(): return jsonify({"Error": "Quote can't be added to short bots"}) x.pause = True - new_average_price = (x.total_amount_of_quote+float(amount))/(x.total_amount_of_base+(float(amount)/x.get_status_dict()["price"])) + new_average_price = (x.status.get_quote_spent()+float(amount))/(x.status.get_base_bought()+(float(amount)/x.get_status_dict()["price"])) broker.logger.log_this(f"Your new average buy price will be {new_average_price} {x.quote}",2,f"{base}/{quote}") broker.logger.log_this(f"Your new take profit price price will be {new_average_price*x.get_tp_level()} {x.quote}",2,f"{base}/{quote}") new_order = broker.new_market_order(x.pair,float(amount),"buy") @@ -1807,13 +1796,13 @@ def unwrapped_add_quote(base,quote,amount): elif returned_order["status"]=="closed": broker.logger.log_this("Order sent",2,f"{base}/{quote}") new_fees_in_base, new_fees_in_quote = x.parse_fees(returned_order) - x.fees_paid_in_base += new_fees_in_base - x.fees_paid_in_quote += new_fees_in_quote - x.total_amount_of_base = x.total_amount_of_base + returned_order["filled"] - new_fees_in_base - x.total_amount_of_quote += returned_order["cost"] + x.status.set_fees_paid_in_base(x.status.get_fees_paid_in_base() + new_fees_in_base) + x.status.set_fees_paid_in_quote(x.status.get_fees_paid_in_quote() + new_fees_in_quote) + x.status.set_base_bought(x.status.get_base_bought() + returned_order["filled"] - new_fees_in_base) + x.status.set_quote_spent(x.status.get_quote_spent()+returned_order["cost"]) broker.logger.log_this("Cancelling old take profit order and sending a new one",2,f"{base}/{quote}") attempts = 5 - while broker.cancel_order(x.tp_order["id"],x.pair)==1: + while broker.cancel_order(x.status.get_tp_order_id(),x.pair)==1: broker.logger.log_this("Can't cancel old take profit order, retrying...",2,f"{base}/{quote}") time.sleep(broker.get_wait_time()) attempts-=1 @@ -1821,8 +1810,8 @@ def unwrapped_add_quote(base,quote,amount): broker.logger.log_this("Can't cancel old take profit order, cancelling...",2,f"{base}/{quote}") x.pause = False return jsonify({"Error": "Can't cancel old take profit order."}) - x.take_profit_price = x.total_amount_of_quote/x.total_amount_of_base*x.get_tp_level() - x.tp_order = broker.new_limit_order(x.pair,x.total_amount_of_base,"sell",x.take_profit_price) + x.status.set_take_profit_price(x.status.get_quote_spent()/x.status.get_base_bought()*x.get_tp_level()) + x.status.set_take_profit_order(broker.new_limit_order(x.pair,x.status.get_base_bought(),"sell",x.status.get_take_profit_price())) x.update_status(True) break else: diff --git a/status_handler.py b/status_handler.py index 4cce8cd..3ccbb8b 100644 --- a/status_handler.py +++ b/status_handler.py @@ -1,19 +1,18 @@ import json +import time class StatusHandler: ''' Handles the status of the trader and the validation of the parameters ''' - def __init__(self, broker, pair, status_dict = None): + def __init__(self, broker, base, quote, status_dict = None): self.broker = broker self.default_status_dictionary = { - "pair": pair, - "tp_order_id": "", - "take_profit_order": {}, + "pair": f"{base}/{quote}", + "take_profit_order": broker.get_empty_order(), "take_profit_price": 0.0, - "so_order_id": "", - "safety_order": {}, + "safety_order": broker.get_empty_order(), "next_so_price": 0.0, "order_size": 0.0, "partial_profit": 0.0, @@ -43,33 +42,29 @@ class StatusHandler: "status_string": "", "deal_order_history": [] } - self.status_file_path = f"configs/{pair.split('/')[0]}{pair.split('/')[1]}.json" + self.status_file_path = f"status/{base}{quote}.status" self.status_dictionary = self.default_status_dictionary.copy() if status_dict is not None: self.status_dictionary = {**self.status_dictionary, **status_dict} - self.save_to_file() + self.save_to_file() - - def get_tp_order_id(self): - return self.status_dictionary["tp_order_id"] - def get_take_profit_order(self): return self.status_dictionary["take_profit_order"] def get_take_profit_price(self): return self.status_dictionary["take_profit_price"] - - def get_so_order_id(self): - return self.status_dictionary["so_order_id"] - + def get_safety_order(self): return self.status_dictionary["safety_order"] def get_next_so_price(self): return self.status_dictionary["next_so_price"] + def get_order_size(self): + return self.status_dictionary["order_size"] + def get_partial_profit(self): return self.status_dictionary["partial_profit"] @@ -149,7 +144,7 @@ class StatusHandler: self.status_dictionary["tp_order_id"] = order_id return 0 - def set_take_profit_order(self, order: dict): + def set_take_profit_order(self, order): self.status_dictionary["take_profit_order"] = order return 0 @@ -161,7 +156,7 @@ class StatusHandler: self.status_dictionary["so_order_id"] = order_id return 0 - def set_safety_order(self, order: dict): + def set_safety_order(self, order): self.status_dictionary["safety_order"] = order return 0 @@ -281,18 +276,27 @@ class StatusHandler: self.status_dictionary["deal_order_history"] = deal_history return 0 + def clear_deal_order_history(self): + self.status_dictionary["deal_order_history"] = [] + return 0 def update_deal_order_history(self, new_deal: dict): self.status_dictionary["deal_order_history"].append(new_deal) return 0 - def save_to_file(self, file_path = None): + def save_to_file(self, file_path = None, is_backup = False): if file_path is None: file_path = self.status_file_path + if is_backup: + try: + with open(time.strftime(f"{file_path}_%Y-%m-%d_%H:%M:%S.json"), "w") as f: + f.write(json.dumps(self.status_dictionary, indent=4)) + except Exception as e: + self.broker.logger.log_this(f"Error creating status backup file: {e}",1) try: with open(file_path, "w") as f: - json.dump(self.status_dictionary, f) + f.write(json.dumps(self.status_dictionary, indent=4)) return 0 except Exception as e: self.broker.logger.log_this(f"Error saving status to file: {file_path}: {e}",1) @@ -303,13 +307,12 @@ class StatusHandler: file_path = self.status_file_path try: with open(file_path, "r") as f: - self.set_status(json.load(f)) + self.status_dictionary = {**self.default_status_dictionary, **json.load(f)} return 0 except Exception as e: self.broker.logger.log_this(f"Error loading status from file: {file_path}: {e}",1) return 1 - def get_status(self): return self.status_dictionary diff --git a/trader.py b/trader.py index 7b5765e..5d31096 100755 --- a/trader.py +++ b/trader.py @@ -12,71 +12,33 @@ class trader: self.quit = False self.restart = False self.broker = broker - self.tp_order = self.broker.get_empty_order() - self.so = self.broker.get_empty_order() self.config = ConfigHandler(pair,broker) self.pair = self.config.get_pair() + self.base,self.quote = self.pair.split("/") + self.status = StatusHandler(broker, self.base, self.quote) self.market = self.broker.fetch_market(self.pair) self.market_load_time = int(time.time()) self.market_reload_period = 86400 #Market reload period in seconds - self.base,self.quote = self.pair.split("/") - self.is_short = self.config.get_is_short() - self.profit_table = self.config.get_tp_table() - self.max_short_safety_orders = self.config.get_max_short_safety_orders() - self.check_slippage = self.config.get_check_slippage() - self.is_boosted = False - self.start_time = int(time.time()) - self.total_amount_of_quote=0 - self.total_amount_of_base=1 - self.take_profit_price=0 - self.safety_price_table=[0] - self.fees_paid_in_base=0 - self.fees_paid_in_quote=0 - self.deal_start_time = 0 + self.status.set_start_time(int(time.time())) self.last_time_seen = time.time() - self.start_price = 0 - self.safety_order_index = 0 - self.status_dict = { - "pair": self.pair, - "quote_spent": 0, - "base_bought": 0, - "so_amount": 0, - "take_profit_price": 1, - "next_so_price": 1, - "tp_order_id": "", - "take_profit_order": {}, - "so_order_id": "", - "safety_order": {}, - "safety_price_table": [], - "pause_reason": "", - "deal_uptime": 0, #In seconds - "total_uptime": 0, #In seconds - "price": 0, - "deal_order_history": [], - } self.warnings = { "short_price_exceeds_old_long": False, "speol_notified": False } - if self.is_short: + if self.config.get_is_short(): #Check if there is an old_long file. If so, load it. try: with open(f"status/{self.base}{self.quote}.oldlong") as ol: - self.status_dict["old_long"] = json.load(ol) + self.status.set_old_long(json.load(ol)) except Exception as e: self.broker.logger.log_this(f"Exception: No old_long file. {e}",1,self.pair) self.profit_filename = f"profits/{self.base}{self.quote}.profits" self.log_filename = f"logs/{self.base}{self.quote}.log" - self.boosted_deals_range = self.config.get_boosted_deals_range() - self.boosted_time_range = self.config.get_boosted_time_range() - self.boosted_amount = self.config.get_boosted_amount() + self.deals_timestamps = self.broker.get_trades_timestamps(self.pair,self.config.get_boosted_time_range()) - self.deals_timestamps = self.broker.get_trades_timestamps(self.pair,self.boosted_time_range) - - self.stop_when_profit = False - self.status_dict["pause_reason"] = "Initialization" + self.status.set_pause_reason("Initialization") if is_import: self.load_imported_trader() return None @@ -101,11 +63,11 @@ class trader: def __str__(self): - return self.status_dict["status_string"] + return self.status.get_status_string() def get_status_dict(self): - return self.status_dict + return self.status.get_status() def set_market_load_time(self, period: float) -> int: @@ -122,7 +84,7 @@ class trader: Reloads the safety order. ''' - self.so = self.broker.get_order(self.status_dict["so_order_id"],self.pair) + self.status.set_safety_order(self.broker.get_order(self.status.get_safety_order()["id"],self.pair)) return 0 @@ -133,10 +95,10 @@ class trader: #Perhaps we should search for open buy orders from a crashed trader and cancel them? #Reset some variables - self.safety_order_index = 0 - self.status_dict["deal_order_history"].clear() - self.tp_order = self.broker.get_empty_order() - self.so = self.broker.get_empty_order() + 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()) #Reloads the market new_market_data = self.broker.fetch_market(self.pair) @@ -144,9 +106,9 @@ class trader: self.market = new_market_data self.pause = True - self.status_dict["pause_reason"] = "start_trader" + self.status.set_pause_reason("start_trader") - if self.is_short: + if self.config.get_is_short(): self.broker.logger.log_this("Calculating optimal order size...",2,self.pair) #Get minimum order size from exchange @@ -164,8 +126,8 @@ class trader: return 1 #Buy missing base sold because of rounding errors (rare) - if "old_long" in self.status_dict: - diff = self.status_dict["old_long"]["tp_amount"] - free_base + if self.status.get_old_long()!={}: + diff = self.status.get_old_long()["tp_amount"] - free_base if diff>min_base_size: diff = self.broker.amount_to_precision(self.pair,diff) self.broker.logger.log_this(f"Buying missing {diff} {self.base}",1,self.pair) @@ -179,7 +141,7 @@ class trader: #Calculate order size and amount of safety orders self.broker.logger.log_this("Calculating the order size...",2,self.pair) - order_size,no_of_safety_orders = self.calculate_order_size(free_base,min_base_size,self.max_short_safety_orders) + 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.pair) return 1 @@ -191,7 +153,7 @@ class trader: self.config.save_to_file() else: #Check order size - self.status_dict["pause_reason"] = "start_trader - checking order size" + self.status.set_pause_reason("start_trader - checking order size") self.broker.logger.log_this("Checking for order size",2,self.pair) minimum_order_size_allowed = self.broker.get_min_quote_size(self.pair) if minimum_order_size_allowed is not None and minimum_order_size_allowed>self.config.get_order_size(): @@ -205,21 +167,21 @@ class trader: return 1 #check slippage - if self.check_slippage: + if self.config.get_check_slippage(): self.broker.logger.log_this("Checking slippage...",2,self.pair) - self.status_dict["pause_reason"] = "start_trader - checking slippage" + self.status.set_pause_reason("start_trader - checking slippage") if self.check_orderbook_depth(self.broker.get_slippage_default_threshold(),self.config.get_order_size()): #Slippage threshold exceeded self.broker.logger.log_this("Slippage threshold exceeded",1,self.pair) return 3 - self.status_dict["pause_reason"] = "start_trader - after slippage" - self.status_dict["order_size"] = self.config.get_order_size() + self.status.set_pause_reason("start_trader - after slippage") + self.status.set_order_size(self.config.get_order_size()) #Sending initial order - self.status_dict["pause_reason"] = "start_trader - sending first order" + self.status.set_pause_reason("start_trader - sending first order") self.broker.logger.log_this("Sending first order...",2,self.pair) - action = "sell" if self.is_short else "buy" + action = "sell" if self.config.get_is_short() else "buy" first_order = self.broker.new_market_order(self.pair,self.config.get_order_size(),action) #self.broker.logger.log_this(f"First order id: {first_order}",1,self.pair) if first_order in [None,self.broker.get_empty_order()]: @@ -228,7 +190,7 @@ class trader: tries = self.broker.get_retries()*2 #This is really necessary, don't change it. Don't. DON'T. #Wait until the first order gets filled - self.status_dict["pause_reason"] = "start_trader - waiting for the first order to get filled" + self.status.set_pause_reason("start_trader - waiting for the first order to get filled") while True: #Wait a bit longer, to catch a bug: #Sometimes the amount of base taken into account by the trader is lower than the amount bought, @@ -258,27 +220,27 @@ class trader: return 2 #Save the order - self.status_dict["pause_reason"] = "start_trader - saving the order in deal_order_history" - self.status_dict["deal_order_history"].append(returned_order) + self.status.set_pause_reason("start_trader - saving the order in deal_order_history") + self.status.update_deal_order_history(returned_order) # Reset the fee count and sum fees from the first order - self.fees_paid_in_base, self.fees_paid_in_quote = self.parse_fees(returned_order) - self.broker.logger.log_this(f"Fees paid: {self.fees_paid_in_base} {self.base}, {self.fees_paid_in_quote} {self.quote}",2,self.pair) + self.status.set_fees_paid_in_base(self.parse_fees(returned_order)[0]) + self.status.set_fees_paid_in_quote(self.parse_fees(returned_order)[1]) + self.broker.logger.log_this(f"Fees paid: {self.status.get_fees_paid_in_base()} {self.base}, {self.status.get_fees_paid_in_quote()} {self.quote}",2,self.pair) self.broker.logger.log_this(f"Take profit order ID: {returned_order['id']}",2,self.pair) # Sum total amount of quote and base if returned_order["filled"]!=None: - self.total_amount_of_base = returned_order["filled"] - if not self.is_short: - #self.total_amount_of_base -= self.parse_fees(returned_order)[0] #The substraction is because some exchanges charges some fees in base - self.total_amount_of_base -= self.fees_paid_in_base - self.total_amount_of_quote = returned_order["cost"] + self.status.set_base_bought(returned_order["filled"]) + if not self.config.get_is_short(): + self.status.set_base_bought(self.status.get_base_bought()-self.status.get_fees_paid_in_base()) + self.status.set_quote_spent(returned_order["cost"]) else: self.broker.logger.log_this("Error starting bot. Aborting.",1,self.pair) return 1 # Send the take profit order - self.status_dict["pause_reason"] = "start_trader - sending tp order" + self.status.set_pause_reason("start_trader - sending tp order") self.broker.logger.log_this("Sending take profit order...",2,self.pair) if self.send_new_tp_order()==0: self.broker.logger.log_this("Take profit order sent",2,self.pair) @@ -287,29 +249,28 @@ class trader: return 1 # Generate the safety prices table - self.start_price = self.broker.price_to_precision(self.pair,self.total_amount_of_quote/self.total_amount_of_base) - self.safety_price_table = self.calculate_safety_prices(self.start_price,self.config.get_no_of_safety_orders(),self.config.get_safety_order_deviance()) + self.status.set_start_price(self.broker.price_to_precision(self.pair,self.status.get_quote_spent()/self.status.get_base_bought())) + self.status.set_safety_price_table(self.calculate_safety_prices(self.status.get_start_price(),self.config.get_no_of_safety_orders(),self.config.get_safety_order_deviance())) # Send the first safety order - self.status_dict["pause_reason"] = "start_trader - sending safety order" + self.status.set_pause_reason("start_trader - sending safety order") self.broker.logger.log_this("Sending safety order...",2,self.pair) - if self.send_new_safety_order(self.status_dict["order_size"])==0: + if self.send_new_safety_order(self.status.get_order_size())==0: self.broker.logger.log_this("Safety order sent",2,self.pair) else: self.broker.logger.log_this("Error sending safety order. Cancelling take profit order and aborting",1,self.pair) - self.broker.cancel_order(self.tp_order["id"],self.pair) + self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.pair) return 1 # Send cleanup order (if cleanup) - self.status_dict["pause_reason"] = "start_trader - doing cleanup (if needed)" - if self.config.get_cleanup() and not self.is_short: #Short traders do not need cleanup. + self.status.set_pause_reason("start_trader - doing cleanup (if needed)") + if self.config.get_cleanup() and not self.config.get_is_short(): #Short traders do not need cleanup. self.do_cleanup() - # Write variables to status_dict and reset deal_uptime - self.deal_start_time = int(time.time()) + self.status.set_deal_start_time(int(time.time())) self.update_status(True) self.pause = False - self.status_dict["pause_reason"] = "" + self.status.set_pause_reason("") return 0 @@ -321,62 +282,30 @@ class trader: :return: int ''' try: - if self.tp_order is not None: #Type checking - self.status_dict["tp_order_id"]=self.tp_order["id"] - self.status_dict["take_profit_order"]=self.tp_order - if self.so is not None: #Type checking - self.status_dict["so_order_id"]=self.so["id"] - self.status_dict["safety_order"]=self.so - if self.tp_order is not None and self.tp_order["price"] is not None: - self.status_dict["take_profit_price"]=self.tp_order["price"] - try: - self.status_dict["next_so_price"]=self.safety_price_table[self.safety_order_index] #List index out of range bug + self.status.set_next_so_price(self.status.get_safety_price_table()[self.status.get_so_amount()]) except Exception as e: - self.broker.logger.log_this(f"Is safety_price_table populated? Exception: {e} | Safety price table: {self.safety_price_table} | Safety order index: {self.safety_order_index}",1,self.pair) - - if self.so is not None and self.so["price"] is not None and self.so!=self.broker.get_empty_order(): - self.status_dict["next_so_price"]=self.so["price"] - self.status_dict["is_boosted"]=self.is_boosted - self.status_dict["is_short"]=self.is_short - self.status_dict["is_paused"]=self.pause - self.status_dict["quote_spent"]=self.total_amount_of_quote - self.status_dict["base_bought"]=self.total_amount_of_base - self.status_dict["so_amount"]=self.safety_order_index - self.status_dict["no_of_safety_orders"]=self.config.get_no_of_safety_orders() - self.status_dict["take_profit_price"]=self.take_profit_price - self.status_dict["safety_price_table"]=self.safety_price_table - self.status_dict["deal_uptime"]=int(time.time()) - self.deal_start_time - self.status_dict["total_uptime"]=int(time.time()) - self.start_time - self.status_dict["fees_paid_in_base"]=self.fees_paid_in_base - self.status_dict["fees_paid_in_quote"]=self.fees_paid_in_quote - self.status_dict["start_price"]=self.start_price - self.status_dict["tp_mode"]=self.config.get_tp_mode() - self.status_dict["profit_table"]=self.config.get_tp_table() - self.status_dict["start_time"]=self.start_time - self.status_dict["deal_start_time"]=self.deal_start_time - self.status_dict["stop_when_profit"]=self.stop_when_profit - self.status_dict["autoswitch"]=self.config.get_autoswitch() + self.broker.logger.log_this(f"Is safety_price_table populated? Exception: {e} | Safety price table: {self.status.get_safety_price_table()} | Safety order index: {self.status.get_so_amount()}",1,self.pair) + if self.status.get_safety_order() is not None and self.status.get_safety_order()["price"] is not None and self.status.get_safety_order()!=self.broker.get_empty_order(): + self.status.set_next_so_price(self.status.get_safety_order()["price"]) + + 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_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()) + self.status.set_profit_table(self.config.get_tp_table()) + self.status.set_autoswitch(self.config.get_autoswitch()) except Exception as e: self.broker.logger.log_this(f"Can't update status dictionary. Exception: {e}",1,self.pair) + return 1 if write_to_disk: - self.write_status_file() + self.status.save_to_file() return 0 - def write_status_file(self,is_backup:bool=False): - try: - json_object = json.dumps(self.status_dict, indent=4) - file_name = f"{self.base}{self.quote}.status" - if is_backup: - self.broker.logger.log_this("Backing up status file...",2,self.pair) - file_name = time.strftime(f"{self.base}{self.quote}_%Y-%m-%d_%H:%M:%S.backup_status") - with open(f"status/{file_name}", "w") as c: - c.write(json_object) - except Exception as e: - self.broker.logger.log_this(f"Can't write status file to disk. Exception: {e}",1,self.pair) - def dca_cost_calculator(self, order_size: float, amount_of_so: int, scalar: float) -> float: ''' Returns the maximum amount of currency that can be used by a trader, given the initial order size @@ -478,7 +407,7 @@ class trader: For smaller bots that might be overengineering it a bit anyway ''' - if self.is_short: #Short bots do not need cleanup + if self.config.get_is_short(): #Short bots do not need cleanup return 0 balance_to_clean = self.fetch_free_base() if balance_to_clean is None: @@ -486,11 +415,11 @@ class trader: 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.pair) - minimum_cleanup_size = self.so["amount"]*2 # type: ignore + minimum_cleanup_size = self.status.get_safety_order()["amount"]*2 # type: ignore 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.pair) self.broker.logger.log_this("Sending cleanup order...",2,self.pair) - cleanup_order = self.broker.new_limit_order(self.pair,balance_to_clean-minimum_cleanup_size,"sell",self.take_profit_price) + cleanup_order = self.broker.new_limit_order(self.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.pair) return 0 @@ -516,12 +445,12 @@ class trader: if optimal_order_size==0: self.broker.logger.log_this("Not enough base to switch. Order size would be too small",1,self.pair) self.pause = False - self.status_dict["pause_reason"] = "" + self.status.set_pause_reason("") return None,None if optimal_order_size0: self.broker.logger.log_this(f"Old safety order is partially filled, ID: {old_so_order['id']}",1,self.pair) - self.status_dict["deal_order_history"].append(old_so_order) - self.total_amount_of_base = self.total_amount_of_base + old_so_order["filled"] - self.parse_fees(old_so_order)[0] - self.total_amount_of_quote = self.total_amount_of_quote + old_so_order["cost"] + 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"]) if not self.broker.check_for_duplicate_profit_in_db(filled_order): - self.status_dict["pause_reason"] = "calculating profit" + self.status.set_pause_reason("calculating profit") # Calculate the profit - if self.is_short: - profit = self.total_amount_of_quote-filled_order["cost"]-self.fees_paid_in_quote-self.parse_fees(filled_order)[1] + if self.config.get_is_short(): + profit = self.status.get_quote_spent()-filled_order["cost"]-self.status.get_fees_paid_in_quote()-self.parse_fees(filled_order)[1] else: - profit = filled_order["cost"]-self.total_amount_of_quote-self.fees_paid_in_quote-self.parse_fees(filled_order)[1] - if "partial_profit" in self.status_dict: - profit+=self.status_dict["partial_profit"] + 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() #Checks if some base was left over. - base_profit = max(self.total_amount_of_base-filled_order["filled"],0) #To avoid negative numbers in base_profit + base_profit = max(self.status.get_base_bought()-filled_order["filled"],0) #To avoid negative numbers in base_profit # Write the profit to file and send telegram message 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_file(profit,filled_order["id"]) 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.total_amount_of_base}, base in the order: {filled_order['amount']}, base filled: {filled_order['filled']}, base 'profit': {base_profit}",1,self.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.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.pair) self.broker.logger.log_this(f"Fill price: {filled_order['price']} {self.quote}",2,self.pair) - self.broker.logger.log_this(f"Safety orders triggered: {self.safety_order_index-1}",2,self.pair) + self.broker.logger.log_this(f"Safety orders triggered: {self.status.get_so_amount()-1}",2,self.pair) - self.status_dict["pause_reason"] = "take_profit_routine - check time limit" + self.status.set_pause_reason("take_profit_routine - check time limit") #Checks if there is a time limit for the trader if self.config.get_programmed_stop() and time.time()>int(self.config.get_programmed_stop_time()): - self.stop_when_profit = True + self.status.set_stop_when_profit(True) - self.status_dict["pause_reason"] = "take_profit_routine - if stop_when_profit" - if self.stop_when_profit: #Signal to stop when trade is closed + 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.pair) self.quit = True return 1 # Clear variables and reload the config dictionary (Only reloading the config file is needed.) - self.status_dict["pause_reason"] = "take_profit_routine - var cleanup" + self.status.set_pause_reason("take_profit_routine - var cleanup") self.config.load_from_file() - if self.check_slippage: + if self.config.get_check_slippage(): self.broker.logger.log_this("Checking slippage...",2,self.pair) - price_to_compare = self.broker.get_top_bid_price(self.pair) if self.is_short else self.broker.get_top_ask_price(self.pair) + price_to_compare = self.broker.get_top_bid_price(self.pair) if self.config.get_is_short() else self.broker.get_top_ask_price(self.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.pair) time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier()) @@ -879,14 +808,14 @@ class trader: 3: "start_trader returned error #3: Slippage threshold exceeded. Trader will be restarted"} #Restarting the trader - self.status_dict["pause_reason"] = "take_profit_routine - restart_trader call" + self.status.set_pause_reason("take_profit_routine - restart_trader call") restart_trader = self.start_trader() - self.status_dict["pause_reason"] = "take_profit_routine - restart_trader call - start_trader() called" + self.status.set_pause_reason("take_profit_routine - restart_trader call - start_trader() called") #retries = self.broker.get_retries() if restart_trader in restart_errors.keys(): self.pause = False self.restart = True - self.write_status_file(is_backup=True) + self.status.save_to_file(is_backup=True) self.broker.logger.log_this(restart_errors[restart_trader],1,self.pair) return restart_trader @@ -900,67 +829,67 @@ class trader: ''' #Let's do some type checking first - if self.tp_order is None: + 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.pair) return 1 - if self.so is None: #I don't think this is necessary + 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.pair) return 1 self.pause = True - self.status_dict["pause_reason"] = "new_so_routine" + self.status.set_pause_reason("new_so_routine") # Save the order - self.status_dict["deal_order_history"].append(filled_order) + self.status.update_deal_order_history(filled_order) # Add the amount filled in the last safety order to the totals new_fees_base,new_fees_quote = self.parse_fees(filled_order) - self.fees_paid_in_quote += new_fees_quote - self.total_amount_of_base = self.total_amount_of_base + filled_order["filled"] - new_fees_base - self.total_amount_of_quote = self.total_amount_of_quote + filled_order["cost"] + self.status.set_fees_paid_in_quote(self.status.get_fees_paid_in_quote() + new_fees_quote) + self.status.set_base_bought(self.status.get_base_bought() + filled_order["filled"] - new_fees_base) + self.status.set_quote_spent(self.status.get_quote_spent() + filled_order["cost"]) #Cooldown time.sleep(self.broker.get_wait_before_new_safety_order()) - # Send the new safety order. If all expected safety orders are filled, it assigns an empty order to self.so + # 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.pair) - if self.send_new_safety_order(self.status_dict["order_size"])==1: + 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.pair) - self.status_dict["pause_reason"] = error_string + self.status.set_pause_reason(error_string) return 1 else: - self.so = self.broker.get_empty_order() - self.safety_order_index+=1 + 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.tp_order["id"],self.pair)==1: - error_string = f"{self.pair} | {self.tp_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.tp_order['id']}",1,self.pair) - self.status_dict["pause_reason"] = error_string + if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.pair)==1: + error_string = f"{self.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.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.tp_order["id"],self.pair) + old_tp_order = self.broker.get_order(self.status.get_take_profit_order()["id"],self.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.pair) - self.status_dict["deal_order_history"].append(old_tp_order) - #self.total_amount_of_base = old_tp_order["remaining"] + self.status.update_deal_order_history(old_tp_order) + #self.status.set_base_bought(old_tp_order["remaining"]) # Partial profit calculation - #if not self.is_short: - # current_deal_price = self.total_amount_of_base/self.total_amount_of_quote - # self.status_dict["partial_profit"] = old_tp_order["cost"]-(old_tp_order["filled"]*current_deal_price)-self.parse.fees(old_tp_order)[1] + #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. # - self.total_amount_of_base = self.total_amount_of_base - old_tp_order["filled"] - self.parse_fees(old_tp_order)[0] - self.total_amount_of_quote = self.total_amount_of_quote - old_tp_order["cost"]# + self.parse_fees(old_tp_order)[1] - self.fees_paid_in_quote += self.parse_fees(old_tp_order)[1] - #self.fees_paid_in_base += self.parse_fees(old_tp_order)[0] + 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()) @@ -969,13 +898,13 @@ class trader: if self.send_new_tp_order()==1: error_string = "Problems sending the new take profit order" self.broker.logger.log_this("Problems sending the new take profit order",1,self.pair) - self.status_dict["pause_reason"] = error_string + self.status.set_pause_reason(error_string) return 3 # Update the status_dict and that's it self.update_status(True) self.pause = False - self.status_dict["pause_reason"] = "" + self.status.set_pause_reason("") return 0 @@ -984,8 +913,8 @@ class trader: Checks if short price exceeds old long price. If so, send a Telegram message ''' price_exceeds = False - if "old_long" in self.status_dict: - price_exceeds = self.status_dict["price"]>float(self.status_dict["old_long"]["tp_price"]) + if self.status.get_old_long()!={}: + price_exceeds = self.status.get_price()>float(self.status.get_old_long()["tp_price"]) if price_exceeds: self.warnings["short_price_exceeds_old_long"] = True else: @@ -995,7 +924,7 @@ class trader: #Only notify one time AND if autoswitch is off self.warnings["speol_notified"] = True if not self.config.get_autoswitch(): - message = f"{self.base}@{self.status_dict['price']} ({str(self.broker.exchange)}), exceeds old long price of {self.status_dict['old_long']['tp_price']}" + 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.pair) return 0 @@ -1014,7 +943,7 @@ class trader: bool: True if the orderbook depth exceeds the threshold, False otherwise. ''' - if self.is_short: #Do not check for slippage in short traders (Pending to be implemented) + 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.pair,no_retries=True) @@ -1057,19 +986,19 @@ class trader: open_orders_ids = [order["id"] for order in open_orders if order["symbol"]==self.pair] #Checks if the orders are valid - if self.tp_order is None: + if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None",1,self.pair) return 1 - if self.so is None: + 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.pair) - self.so = self.broker.get_empty_order() + self.status.set_safety_order(self.broker.get_empty_order()) #return 1 - if self.tp_order["id"]=="": - self.broker.logger.log_this(f"Take profit order missing. Stopping bot. Order ID: {self.tp_order['id']}",1,self.pair) - self.broker.cancel_order(self.so["id"],self.pair) + if self.status.get_take_profit_order()["id"]=="": + self.broker.logger.log_this(f"Take profit order missing. Stopping bot. No order ID was provided.",1,self.pair) + self.broker.cancel_order(self.status.get_safety_order()["id"],self.pair) if self.config.get_attempt_restart(): - self.write_status_file(is_backup=True) + 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.pair) else: @@ -1077,16 +1006,16 @@ class trader: return 1 #Checks if the take profit order is filled - if self.tp_order["id"] not in open_orders_ids: - tp_status = self.broker.get_order(self.tp_order["id"],self.pair) + 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.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 bot. Order ID: {self.tp_order['id']}",1,self.pair) + self.broker.logger.log_this(f"Take profit order closed but not filled, 0 filled. Stopping bot. Order ID: {self.status.get_take_profit_order()['id']}",1,self.pair) #Cancelling safety order and stopping bot - self.broker.cancel_order(self.so["id"],self.pair) + self.broker.cancel_order(self.status.get_safety_order()["id"],self.pair) if self.config.get_attempt_restart(): - self.write_status_file(is_backup=True) + 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.pair) else: @@ -1096,7 +1025,7 @@ class trader: #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 bot.",1,self.pair) - self.write_status_file(is_backup=True) + 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.pair) @@ -1106,11 +1035,9 @@ class trader: return 1 # Check if safety order is filled - if self.so["id"] not in open_orders_ids and self.safety_order_index<=self.config.get_no_of_safety_orders(): - #so_status = self.so - #if self.so["id"]!="": - so_status = self.broker.get_order(self.so["id"],self.pair) - tp_order_status = self.broker.get_order(self.tp_order["id"],self.pair) + 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.pair) + tp_order_status = self.broker.get_order(self.status.get_take_profit_order()["id"],self.pair) #Now we check 2 things: #1. That the prior safety order status is indeed closed (or canceled) @@ -1118,60 +1045,60 @@ class trader: 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.is_short and self.safety_order_index==self.config.get_no_of_safety_orders() and self.config.get_autoswitch(): + 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.write_status_file(is_backup=True) + self.status.save_to_file(is_backup=True) self.restart = True return 0 - a = self.new_so_routine(so_status,self.safety_order_index=old_target: + #A more conservative approach would be old_target = self.status.get_old_long()["quote_spent"], just breaking even. + old_target = self.status.get_old_long()["tp_price"]*self.status.get_old_long()["tp_amount"] + base_left = self.status.get_old_long()["tp_amount"]-self.status.get_base_bought() + if (base_left*self.status.get_price())+self.status.get_quote_spent()>=old_target: #Sell all base (market), report the profits and restart the trader - self.status_dict["pause_reason"] = "automatic_switch" - self.switch_to_long(already_received_quote=self.status_dict["quote_spent"]) + self.status.set_pause_reason("automatic_switch") + self.switch_to_long(already_received_quote=self.status.get_quote_spent()) self.restart = True return 1 #Render status line(s) - self.status_dict["status_string"] = self.generate_status_strings() + self.status.set_status_string(self.generate_status_strings()) #Wrap up - self.status_dict["deal_uptime"]=int(time.time()) - self.deal_start_time - self.status_dict["total_uptime"]=int(time.time()) - self.start_time + 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.update_status(False) self.last_time_seen = int(time.time()) @@ -1184,7 +1111,7 @@ class trader: The last n deals must be within the last t seconds ''' - return len(self.deals_timestamps)>=self.boosted_deals_range and time.time()-self.boosted_time_range<=self.deals_timestamps[-self.boosted_deals_range] + return len(self.deals_timestamps)>=self.config.get_boosted_deals_range() and time.time()-self.config.get_boosted_time_range()<=self.deals_timestamps[-self.config.get_boosted_deals_range()] def get_tp_level(self, order_index: int = 0) -> float: @@ -1197,15 +1124,8 @@ class trader: ''' tp_level = 1 - boost_percentage = 0 - - #BOOST ROUTINE: If the trader closed certain amount of deals within the last t seconds, raise the take profit level by x% - self.is_boosted = False - if not self.is_short and self.check_boosted(): - self.is_boosted = True - boost_percentage = self.boosted_amount - - if self.is_short or self.config.get_tp_mode()==0: #Fixed take profit percentage + + 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 @@ -1228,7 +1148,13 @@ class trader: tp_level = profit_table[-1] if order_index str: @@ -1253,23 +1179,23 @@ class trader: ''' tries = self.broker.get_retries() while tries>0: - if self.total_amount_of_base==0: + 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.pair) return 1 - if self.is_short: - self.take_profit_price = self.total_amount_of_quote/self.total_amount_of_base*(1-(self.get_tp_level(self.safety_order_index)-1)) - self.tp_order = self.broker.new_limit_order(self.pair,self.total_amount_of_base,"buy",self.take_profit_price) + 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.pair,self.status.get_base_bought(),"buy",self.status.get_take_profit_price())) else: - self.take_profit_price = self.total_amount_of_quote/self.total_amount_of_base*self.get_tp_level(self.safety_order_index) - self.tp_order = self.broker.new_limit_order(self.pair,self.total_amount_of_base,"sell",self.take_profit_price) - if self.tp_order==1: #This means that there was a miscalculation of base currency amount, let's correct it. - if self.is_short: #If in short mode, we don't recalculate anything. + 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.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 adjusted = self.adjust_base() if adjusted is not None: - self.total_amount_of_base = adjusted - self.tp_order = None #Just to be able to iterate - if self.tp_order not in [None,self.broker.get_empty_order()]: + self.status.set_base_bought(adjusted) + self.status.set_take_profit_order(None) #Just to be able to iterate + if self.status.get_take_profit_order() not in [None,self.broker.get_empty_order()]: return 0 tries-=1 time.sleep(self.broker.get_wait_time()) @@ -1298,7 +1224,7 @@ class trader: retries = self.broker.get_retries() while retries>0: try: - order_history = json.dumps(self.status_dict["deal_order_history"]) if write_deal_order_history else "" + order_history = json.dumps(self.status.get_deal_order_history()) if write_deal_order_history else "" dataset = (time.time(),self.pair,amount,self.broker.get_exchange_name(),str(orderid),order_history) #Write profit to cache self.broker.write_profit_to_cache(dataset) @@ -1314,21 +1240,21 @@ class trader: ''' Sends a new safety order to the exchange ''' - so_size = self.gib_so_size(size,self.safety_order_index+1,self.config.get_safety_order_scale()) #safety_order_scale: safety order growth factor - if self.is_short: - new_order = self.broker.new_limit_order(self.pair,so_size,"sell",self.safety_price_table[self.safety_order_index+1]) + 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.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.pair,so_size/self.safety_price_table[self.safety_order_index+1],"buy",self.safety_price_table[self.safety_order_index+1]) + new_order = self.broker.new_limit_order(self.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.so = self.broker.get_empty_order() + 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.pair) #elif new_order in [None,self.broker.get_empty_order()] #MAYUBE THIS CONDITIONAL IS BETTER elif new_order is None: - self.so = None + self.status.set_safety_order(None) return 1 else: - self.so = new_order - self.safety_order_index+=1 + self.status.set_safety_order(new_order) + self.status.set_so_amount(self.status.get_so_amount()+1) return 0 @@ -1338,7 +1264,7 @@ class trader: ''' try: extra = f" and {round(base_profit,6)} {self.base}" if base_profit>0 else "" - message = f"{self.pair} closed a {'short' if self.is_short else 'long'} trade.\nProfit: {round(profit,6)} {self.quote}{extra}\nSafety orders triggered: {self.safety_order_index-1}\nTake profit price: {order['price']} {self.quote}\nTrade size: {round(order['cost'],2)} {self.quote}\nDeal uptime: {self.seconds_to_time(self.status_dict['deal_uptime'])}\nOrder ID: {order['id']}\nExchange: {self.broker.get_exchange_name().capitalize()}\n" + message = f"{self.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" self.broker.logger.send_tg_message(message) return 0 except Exception as e: @@ -1385,7 +1311,7 @@ class trader: else: so_deviance_table = [safety_order_deviance]*(no_of_safety_orders+2) - multiplier = -1 if self.is_short else 1 + multiplier = -1 if self.config.get_is_short() else 1 for y in range(1, no_of_safety_orders+2): #+2 instead of the expected +1 because of a bug when updating the status dict. It could be any value, if we add SOs the table is recalculated anyway safety_price_table.append(safety_price_table[-1]-multiplier*(safety_price_table[0]*so_deviance_table[y-1]/100)) @@ -1420,24 +1346,24 @@ class trader: if "active" in market and not market["active"]: self.broker.logger.log_this("Market is closed",1,self.pair) return 1 - if self.tp_order is None: + if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None",1,self.pair) return 1 - if self.so is None: + if self.status.get_safety_order() is None: self.broker.logger.log_this("Safety order is None",1,self.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.pair) - self.tp_order = self.quote_currency_replace_order(self.tp_order,new_quote) - if self.tp_order==self.broker.get_empty_order(): + 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.so!=self.broker.get_empty_order(): + if self.status.get_safety_order()!=self.broker.get_empty_order(): self.broker.logger.log_this("Replacing safety order",2,self.pair) - self.so = self.quote_currency_replace_order(self.so,new_quote) - if self.so==self.broker.get_empty_order(): + 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 #Calls switch_quote_currency_config @@ -1489,10 +1415,10 @@ class trader: self.pair = self.config.get_pair() #If there is an old_long file, also copy it - if self.is_short and "old_long" in self.status_dict: + if self.config.get_is_short() and self.status.get_old_long()!={}: try: with open(f"status/{self.base}{self.quote}.oldlong","w") as c: - c.write(json.dumps(self.status_dict["old_long"], indent=4)) + c.write(json.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.pair) @@ -1566,18 +1492,11 @@ class trader: mid_percentage = 10 high_percentage = 20 - safety_order_string = f"{self.status_dict['so_amount']-1}/{self.config.get_no_of_safety_orders()}".rjust(5) + safety_order_string = f"{self.status.get_so_amount()-1}/{self.config.get_no_of_safety_orders()}".rjust(5) - #Check if necessary - low_price = 0 - mid_price = 0 - high_price = 0 - if self.status_dict["next_so_price"] is not None: - low_price = self.status_dict["next_so_price"] - if self.status_dict["price"] is not None: - mid_price = self.status_dict["price"] - if self.status_dict["take_profit_price"] is not None: - high_price = self.status_dict["take_profit_price"] + 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 low_boundary = '{:.20f}'.format(low_price)[:decimals].center(decimals) mid_boundary = '{:.20f}'.format(mid_price)[:decimals].center(decimals) @@ -1585,9 +1504,9 @@ class trader: percentage_to_profit = 100 pct_to_profit_str = "XX.XX" - if self.status_dict not in [0,None] and self.status_dict["price"]!=0: - diff = abs(self.status_dict["take_profit_price"]-self.status_dict["price"]) - percentage_to_profit = diff/self.status_dict["price"]*100 + if self.status.get_price()!=0: + diff = abs(self.status.get_take_profit_price()-self.status.get_price()) + percentage_to_profit = diff/self.status.get_price()*100 #Formatting (on-screen percentage not longer than 4 digits) pct_to_profit_str = "{:.2f}".format(percentage_to_profit) @@ -1597,20 +1516,20 @@ class trader: pct_to_profit_str = pct_to_profit_str[:5] line3 = "" - if self.total_amount_of_base!=0: - line3 = draw_line(self.status_dict["price"],self.status_dict["next_so_price"],self.status_dict["take_profit_price"],self.total_amount_of_quote/self.total_amount_of_base) + if self.status.get_base_bought()!=0: + line3 = draw_line(self.status.get_price(),self.status.get_next_so_price(),self.status.get_take_profit_price(),self.status.get_quote_spent()/self.status.get_base_bought()) p = "*PAUSED*" if self.pause==True else "" low_boundary_color = red price_color = white target_price_color = green pair_color = cyan - if self.is_short: + if self.config.get_is_short(): price_color = white pair_color = yellow - if "old_long" in self.status_dict: - if self.status_dict["price"]>self.status_dict["old_long"]["tp_price"]: + if self.status.get_old_long()!={}: + if self.status.get_price()>self.status.get_old_long()["tp_price"]: price_color = bright_green - if self.status_dict["take_profit_price"]>self.status_dict["old_long"]["tp_price"]: + if self.status.get_take_profit_price()>self.status.get_old_long()["tp_price"]: target_price_color = bright_green #Set percentage's color @@ -1623,23 +1542,23 @@ class trader: pct_color = red multiplier = 0 - if self.is_short and "old_long" in self.status_dict: + if self.config.get_is_short() and self.status.get_old_long()!={}: try: #Logic to display switch price - old_target = self.status_dict["old_long"]["tp_price"]*self.status_dict["old_long"]["tp_amount"] - base_left = self.status_dict["old_long"]["tp_amount"]-self.status_dict["base_bought"] - minimum_switch_price = (old_target - self.status_dict["quote_spent"])/base_left - if old_target-self.status_dict["quote_spent"]>0 and base_left>0 and minimum_switch_price0 and base_left>0 and minimum_switch_price