import csv import json import time import os class trader: def __init__(self, broker, config_dict: dict, is_import: bool = False): self.pause = True #Signals the trader to not process order info when an API call manhandles the trader #True by default, once the trader is started the start_bot method toggles it self.quit = False #If true, it doesn't restart the bot when profit is reached. self.restart = False self.broker = broker self.tp_order = self.broker.get_empty_order() self.so = self.broker.get_empty_order() self.config_dict = config_dict self.pair = self.config_dict["pair"] 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_dict["is_short"] self.profit_table = self.config_dict["tp_table"] self.max_short_safety_orders = 45 if "max_short_safety_orders" in config_dict: self.max_short_safety_orders = config_dict["max_short_safety_orders"] 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.last_time_seen = time.time() self.start_price = 0 self.safety_order_index = 0 self.status_dict = { "quote_spent": 0, "base_bought": 0, "so_amount": 0, #"max_so_amount": config_dict["no_of_safety_orders"], "take_profit_price": 1, "next_so_price": 1, #"acc_profit": 0, "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 "stop_time" in self.config_dict and int(self.config_dict["stop_time"]) int: self.market_load_time = period return 0 def get_market_reload_period(self) -> float: return self.market_reload_period def reload_safety_order(self) -> int: ''' Reloads the safety order. ''' self.so = self.broker.get_order(self.status_dict["so_order_id"],self.pair) return 0 def start_bot(self) -> int: ''' Initializes the 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() #Reloads the market new_market_data = self.broker.fetch_market(self.pair) if new_market_data is not None: self.market = new_market_data self.pause = True self.status_dict["pause_reason"] = "start_bot" if self.is_short: self.broker.logger.log_this("Calculating optimal order size...",2,self.pair) #Get minimum order size from exchange self.broker.logger.log_this("Fetching minimum order size...",2,self.pair) min_base_size = self.broker.get_min_base_size(self.pair) if min_base_size is None: self.broker.logger.log_this("Can't fetch the minimum order size",1,self.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.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.pair) 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 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) self.broker.new_market_order(self.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.pair) return 1 #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) 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 self.config_dict["order_size"] = order_size self.config_dict["no_of_safety_orders"] = no_of_safety_orders self.broker.logger.log_this(f"Order size: {self.broker.amount_to_precision(self.pair,order_size)}. Amount of safety orders: {no_of_safety_orders}",2,self.pair) #Write the changes to the config file with open(f"configs/{self.base}{self.quote}.json","w") as g: g.write(json.dumps(self.config_dict, indent=4)) else: #Check order size self.status_dict["pause_reason"] = "start_bot - 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_dict["order_size"]: self.broker.logger.log_this(f"Order size too small. Minimum order size is {minimum_order_size_allowed} {self.quote}",1,self.pair) if minimum_order_size_allowed dict: ''' Reloads the config dictionary from disk :return: dict ''' config_filename = f"configs/{self.base}{self.quote}.json" with open(config_filename,"r") as y: config_dict = json.load(y) if self.config_dict["autoswitch"]: config_dict["autoswitch"] = True return config_dict def update_status(self, write_to_disk: bool) -> int: ''' Updates the status dictionary :param write_to_disk: bool - If True, writes the status file to disk. :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 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_short"]=self.is_short 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_dict["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_dict["tp_mode"] self.status_dict["profit_table"]=self.config_dict["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 except Exception as e: self.broker.logger.log_this(f"Can't update status dictionary. Exception: {e}",1,self.pair) if write_to_disk: self.write_status_file() #try: # if write_to_disk: # json_object = json.dumps(self.status_dict, indent=4) # with open(f"status/{self.base}{self.quote}.status", "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) 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 :param order_size: float :param amount_of_so: int :param scalar: float :return: float ''' total = order_size for i in range(1,amount_of_so+1): total+=self.gib_so_size(order_size,i,scalar) return total def return_optimal_order_size(self, amount: float, min_size: float, amount_of_safety_orders: int, scalar: float) -> float: ''' Calculates the optimal order size for a short bot, according to the amount passed as a parameter. Due to performance issues, the step size that is used is 1/10th of the minimum order size. :param amount: float :param min_size: float :param amount_of_safety_orders: int :param scalar: float :return: float ''' total_size = float(min_size) #Calculate optimal step size self.broker.logger.log_this("Calculating optimal step size...",2,self.pair) #step = self.get_step_size() #if step is None: # step = min_size #if step==0: # step = min_size #self.broker.logger.log_this(f"Step size is {step}",2,self.pair) divisor = 10 while divisor>0: #step = self.broker.amount_to_precision(self.pair,min_size/divisor) step = min_size/divisor if step!=0: #When using amount_to_precision, this comes handy. break divisor-=1 #if step==0: # step = self.broker.amount_to_precision(self.pair,min_size) previous_size = 0 while True: #This loop should have a safeguard self.broker.logger.log_this(f"Calculating optimal order size ...",2,self.pair) total_cost = self.dca_cost_calculator(total_size,amount_of_safety_orders,scalar) if total_cost>=amount: return previous_size previous_size = total_size total_size+=step def parse_fees(self, order: dict) -> tuple: ''' Returns the fees paid ordered in "base,quote" Note: CCXT does not detail the fees paid if the exchange is Binance. ''' basefee = 0 quotefee = 0 #Uncomment if you want to guesstimate Binance's fees. #if self.broker.get_exchange_name()=="binance": # #Fees of buy orders are charged in base currency, fees of sell orders are charged in quote currency. # 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.pair) # fee_rate = 0.001 # # if order["side"]=="buy": # basefee = order["filled"]*float(fee_rate) # elif order["side"]=="sell": # quotefee = order["cost"]*float(fee_rate) # return basefee,quotefee for x in order["fees"]: if x["currency"]==self.base: basefee+=float(x["cost"]) if x["currency"]==self.quote: quotefee+=float(x["cost"]) return basefee,quotefee def do_cleanup(self) -> int: ''' Checks for any remaining base currency balance on the exchange If it finds some and that amount is enough, it sends a sell order at the take profit price It was implemented because some deals close with a little amount of base remaining and it tends to pile up overtime A more elegant solution would be to take note of the amount and the price at the moment of the deal closing that lead to that small amount of change to appear, to make possible to calculate an optimal sell price of the remaining assets instead of brute forcing it this way. For smaller bots that might be overengineering it a bit anyway ''' if self.is_short: #Short bots do not need cleanup return 0 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.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.pair) minimum_cleanup_size = self.so["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) if cleanup_order not in [None,self.broker.get_empty_order()]: self.broker.logger.log_this("Cleanup successful",2,self.pair) return 0 self.broker.logger.log_this("Problems with the cleanup order",1,self.pair) return 1 self.broker.logger.log_this("No cleanup needed",2,self.pair) return 0 def calculate_order_size(self, free_base: float, min_base_size: float, amount_of_so: int = 30) -> tuple: ''' Calculates the optimal order size and the amount of safety orders from the free_base and minimum base size ''' optimal_order_size = 0 minimum_amount_of_safety_orders = 1 #This variable could be a config knob 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_dict["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.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.pair) self.pause = False self.status_dict["pause_reason"] = "" return None,None if optimal_order_size int: ''' This method modifies the config file of a pair to convert it to short. It automagically sets the order size according to the funds available. If not enough funds, it returns 1 ''' if self.is_short: #Check if bot is already a short bot return 1 #Let's do some type checking first if self.tp_order is None: self.broker.logger.log_this("Take profit order is None, can't switch to short",1,self.pair) return 1 if self.so is None: self.broker.logger.log_this("Safety order is None, can't switch to short",1,self.pair) return 1 #Pauses bot self.pause = True self.status_dict["pause_reason"] = "switch_to_short" #Read the config file self.broker.logger.log_this("Reading config file",2,self.pair) try: with open(f"configs/{self.base}{self.quote}.json","r") as f: old_config = json.load(f) except Exception as e: self.broker.logger.log_this(f"Error. Can't read the config file. Can't switch mode. Exception: {e}",1,self.pair) self.pause = False self.status_dict["pause_reason"] = "" return 1 #Calculate if there is enough base and, if so, calculate the optimal order size #Fetch the real amount of available base self.broker.logger.log_this(f"Fetching available {self.base}",2,self.pair) free_base = self.fetch_free_base() if free_base is None: return 1 #Fetch the minimal order size min_base_size = self.broker.get_min_base_size(self.pair) if min_base_size is None: self.broker.logger.log_this("Error. Can't fetch market info from the exchange",1,self.pair) self.pause = False self.status_dict["pause_reason"] = "" return 1 #Check if there is enough base if self.broker.amount_to_precision(self.pair,free_base+self.tp_order["amount"])<=min_base_size: self.broker.logger.log_this("Error. Not enough base currency",1,self.pair) self.pause = False self.status_dict["pause_reason"] = "" return 1 #Calculate order size self.broker.logger.log_this("Calculating optimal order size",2,self.pair) optimal_order_size,amount_of_so = self.calculate_order_size(free_base+self.tp_order["amount"],min_base_size,amount_of_so=self.max_short_safety_orders) if optimal_order_size is None or amount_of_so is None: return 1 self.broker.logger.log_this(f"New order size: {optimal_order_size}",2,self.pair) self.broker.logger.log_this(f"Amount of safety orders: {amount_of_so}",2,self.pair) #Close old orders self.broker.logger.log_this("Switching trader mode to short",2,self.pair) self.broker.logger.log_this("Closing orders...",2,self.pair) if self.broker.cancel_order(self.tp_order["id"],self.pair)==1: self.broker.logger.log_this("Can't cancel the take profit order. Can't switch mode",1,self.pair) self.pause = False self.status_dict["pause_reason"] = "" return 1 if self.so["id"]!="": self.broker.cancel_order(self.so["id"],self.pair) #Save the old take profit order info for later and saves it to a different file in case of trader crash self.broker.logger.log_this("Saving state in status_dict",2,self.pair) self.status_dict["old_long"] = {"tp_price": self.tp_order["price"], "tp_amount": self.tp_order["amount"], "quote_spent": self.total_amount_of_quote, "fees_paid_in_quote": self.fees_paid_in_quote, "datetime": time.strftime("[%Y/%m/%d %H:%M:%S]") } try: with open(f"status/{self.base}{self.quote}.oldlong","w") as s: s.write(json.dumps(self.status_dict["old_long"],indent=4)) except Exception as e: self.broker.logger.log_this(f"Exception while saving old_long file: {e}",1,self.pair) #Modify config file accordingly self.broker.logger.log_this("Modifying config file and saving a backup",2,self.pair) try: with open(f"configs/{self.base}{self.quote}.bak","w") as c: c.write(json.dumps(old_config, indent=4)) old_config["is_short"] = True old_config["order_size"] = optimal_order_size #Now calculated on-the-fly at the start of every deal old_config["no_of_safety_orders"] = amount_of_so #4. Write the config file with open(f"configs/{self.base}{self.quote}.json","w") as c: c.write(json.dumps(old_config, indent=4)) self.broker.logger.log_this("Config file updated",2,self.pair) except Exception as e: self.broker.logger.log_this(f"Error. Can't write the config file. Exception: {e}",1,self.pair) #self.pause = False return 1 self.stop_when_profit = False self.is_short = True self.broker.logger.log_this("Done configuring. Starting bot...",2,self.pair) return 0 def switch_to_long(self, from_take_profit: bool = False, ignore_old_long: bool = False) -> int: ''' Takes a short bot and changes the mode to long. Only does it if the current bot was previously a long one. ''' #Check if it's not already long if not self.is_short: self.broker.logger.log_this("Can't switch a long trader to long, there's nothing to do",1,self.pair) return 1 #Check if the orders are OK if self.tp_order is None: self.broker.logger.log_this("Take profit order is None, can't switch to short",1,self.pair) return 1 if self.so is None: self.broker.logger.log_this("Safety order is None, can't switch to short",1,self.pair) return 1 #Send Telegram message self.broker.logger.log_this("Attempting to switch to long bot",0,self.pair) if not ignore_old_long and "old_long" not in self.status_dict: self.broker.logger.log_this("Can't find old long info on status_dict, searching for oldlong file",1,self.pair) try: with open(f"status/{self.base}{self.quote}.oldlong") as f: self.status_dict["old_long"] = json.load(f) except Exception as e: #self.write_to_log(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.pair} | Can't find old long file")) self.broker.logger.log_this(f"Can't file oldlong file. Exception: {e}",1,self.pair) return 1 #Cancel open orders if not from_take_profit: #I think this exception handler is al pedo. try: self.broker.cancel_order(self.so["id"],self.pair) except Exception as e: self.broker.logger.log_this(f"Error in cancel_order while cancelling safety order. Exception: {e}",1,self.pair) #Also this one. try: self.broker.cancel_order(self.tp_order["id"],self.pair) except Exception as e: self.broker.logger.log_this(f"Error in cancel_order while cancelling take profit order. Exception: {e}",1,self.pair) #Liquidate base self.liquidate_base(ignore_profits=ignore_old_long) #switch config files and set self.is_short to False try: with open(f"configs/{self.base}{self.quote}.bak") as c: old_config = json.load(c) with open(f"configs/{self.base}{self.quote}.json","w") as c: c.write(json.dumps(old_config, indent=4)) self.is_short = False except Exception as e: self.broker.logger.log_this(f"Exception in switch_to_long while switching config files: {e}",1,self.pair) return 1 #Remove old_long file (if it exists) if os.path.isfile(f"status/{self.base}{self.quote}.oldlong"): self.broker.logger.log_this("Removing old_long file...",2,self.pair) os.remove(f"status/{self.base}{self.quote}.oldlong") #Set up a few variables self.fees_paid_in_quote = 0 self.fees_paid_in_base = 0 self.tp_order = self.broker.get_empty_order() self.so = self.broker.get_empty_order() self.safety_price_table = [0] self.safety_order_index = 0 #Disabling autoswitch #self.config_dict["autoswitch"] = False #Done. Ready for start_bot return 0 def liquidate_base(self, ignore_profits: bool = True) -> int: ''' Fetches the amount of free base on the exchange, sells the entirety of the amount and calculates profits Sends the Telegram message and writes the profits to disk ''' #Find out the amount of free base free_base = self.fetch_free_base() if free_base is None: self.broker.logger.log_this("Can't fetch free base",1,self.pair) return 1 #send market order selling the total amount of base in the last take profit short order order = self.broker.new_market_order(self.pair,free_base,"sell") tries = self.broker.get_retries() while True: time.sleep(self.broker.get_wait_time()) market_tp_order = self.broker.get_order(order["id"],self.pair) if market_tp_order["status"]=="closed": _, fees_paid = self.parse_fees(market_tp_order) break tries-=1 if tries==0: self.broker.logger.log_this("Liquidation order not filling. Skipping base liquidation",1,self.pair) return 1 #calculate profits if not ignore_profits: profit = market_tp_order["cost"] - self.status_dict["old_long"]["quote_spent"] - self.status_dict["old_long"]["fees_paid_in_quote"] - fees_paid #self.status_dict["acc_profit"] += profit #Add profits to file and send telegram notifying profits self.profit_to_file(profit,market_tp_order["id"]) #This is not used anymore, but it's still here since it does not take almost any disk space and/or CPU time. self.profit_to_db(profit,market_tp_order["id"],self.broker.get_write_order_history()) self.broker.logger.log_this(f"Switch successful. Profit: {round(profit,2)} {self.quote}",0,self.pair) self.broker.logger.log_this(f"Sell price: {market_tp_order['price']} {self.quote}",0,self.pair) self.broker.logger.log_this(f"Order ID: {market_tp_order['id']}",0,self.pair) return 0 def take_profit_routine(self, filled_order: dict) -> int: ''' When profit is reached, this method is called to handle the profit calculations, the closing of orders, the reporting and the restart of the trader. ''' self.pause = True #To stop the main thread to iterate through this bot's orders (just in case) self.status_dict["pause_reason"] = "take_profit_routine - order handling" #start_bot will set this flag to False again once it starts #Let's do some type checking first if self.tp_order is None: error_string = time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.pair} | TP order is None") self.status_dict["pause_reason"] = error_string self.broker.logger.log_this("Error. Take profit order is None, pair will be restarted",0,self.pair) self.write_status_file(True) self.restart = True return 1 if self.so is None: error_string = time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.pair} | Safety order is None") self.status_dict["pause_reason"] = error_string self.broker.logger.log_this("Error. Safety order is None",1,self.pair) self.so = self.broker.get_empty_order() #Save the order self.status_dict["deal_order_history"].append(filled_order) # Cancel the current safety order (first check if there is something to cancel) already_counted = False if self.so["id"]=="": self.broker.logger.log_this("There is no safety order to cancel",2,self.pair) elif self.broker.cancel_order(self.so["id"],self.pair)==1: self.broker.logger.log_this("Old safety order probably filled. Can't cancel.",1,self.pair) closed_order = self.broker.get_order(self.so["id"],self.pair) if closed_order!=self.broker.get_empty_order() and closed_order["status"]=="closed": self.total_amount_of_base = self.total_amount_of_base + closed_order["filled"] self.total_amount_of_quote = self.total_amount_of_quote + closed_order["cost"] #Save the order self.status_dict["deal_order_history"].append(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 #Suggestion: Would it be feasible to send a market sell order for the amount of base bought on the old safety order? #Or is it better to leave this amount of change to the cleanup routine? if not self.is_short and self.so["id"]!="" and not already_counted: old_so_order = self.broker.get_order(self.so["id"],self.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.pair) self.status_dict["deal_order_history"].append(old_so_order) #Uncomment the next two lines if you do not want to ignore the partial fill #self.total_amount_of_base = self.total_amount_of_base + old_so_order["filled"] #self.total_amount_of_quote = self.total_amount_of_quote + old_so_order["cost"] #Hypothetical market order code below: #DO NOT UPDATE total_amount_of_base and total_amount_of_quote #If there is not enough base for an order, we'll accumulate it and let the cleanup routine deal with that. # floor_amount = self.broker.get_min_base_size(self.pair) # if floor_amount is not None and old_so_order["filled"]>floor_amount: # self.broker.logger.log_this(f"Sending sell order for partially filled funds: {old_so_order["filled"]} {self.base}",1,self.pair) # loose_change_order = broker.new_market_order(self.pair,old_so_order["filled"],"sell",amount_in_base=True) # retries = self.broker.get_retries() # while retries>0: # time.sleep(self.broker.get_wait_time()) # order_to_fill = self.broker.get_order(loose_change_order["id"],self.pair) # if order_to_fill["status"]=="closed" and order_to_fill["filled"]>0: # profit_with_fees = order_to_fill["filled"]-old_so_order["cost"]-self.parse_fees(order_to_fill)[1] # self.broker.logger.log_this(f"Trader closed a loose change deal. Profit: {profit_with_fees} {self.quote}",0,self.pair) # if profit_with_fees>0: # self.profit_to_file(profit_with_fees,order_to_fill["id"]) # retries-=1 if not self.broker.check_for_duplicate_profit_in_db(filled_order): self.status_dict["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] 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"] #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 # 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.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) #Check if it needs to switch the trader mode self.status_dict["pause_reason"] = "check for autoswitch" #If it's a short bot that used to be long AND autoswitch is enabled if self.is_short and self.config_dict["autoswitch"] and "old_long" in self.status_dict and filled_order["average"]>self.status_dict["old_long"]["tp_price"]: #Checking the price again to prevent being fooled by anomalous spikes #It could be done above instead of using filled_order["average"], bit this way eases the API load a little bit, #since every time (except the last one) it won't be bigger anyway. if float(self.broker.get_ticker_price(self.pair))>self.status_dict["old_long"]["tp_price"]: #Sell all base (market), report the profits and restart the trader self.status_dict["pause_reason"] = "automatic_switch" if self.switch_to_long(from_take_profit=True)==1: self.write_status_file(True) self.restart = True self.broker.logger.log_this("Error, switch_to_long returned 1, pair will be restarted",0,self.pair) return 1 self.status_dict["pause_reason"] = "take_profit_routine - check time limit" #Checks if there is a time limit for the trader if "stop_time" in self.config_dict and time.time()>int(self.config_dict["stop_time"]): self.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.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 config_dict #self.pause = False self.status_dict["pause_reason"] = "take_profit_routine - var cleanup" self.fees_paid_in_quote = 0 self.fees_paid_in_base = 0 #self.tp_order = self.broker.get_empty_order() #self.so = self.broker.get_empty_order() self.safety_price_table.clear() #self.safety_order_index = 0 self.config_dict = self.reload_config_dict() 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) 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 pair",1,self.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_dict["order_size"],filled_order["price"]): self.broker.logger.log_this(f"Orderbook depth not sufficient, waiting for cooldown and restarting pair",1,self.pair) time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier()) self.pause = False self.restart = True return 1 #Restarting the trader self.status_dict["pause_reason"] = "take_profit_routine - restart_bot call" restart_bot = self.start_bot() self.status_dict["pause_reason"] = "take_profit_routine - restart_bot call - start_bot() called" #retries = self.broker.get_retries() if restart_bot==0: return 0 elif restart_bot==1: self.pause = False #self.write_status_file(True) self.restart = True self.broker.logger.log_this("Error in trader, start_bot returned 1. Trader will be restarted",1,self.pair) return 1 elif restart_bot==2: self.pause = False #self.write_status_file(True) self.restart = True self.broker.logger.log_this("Error in trader, start_bot returned 2 (Initial order never got filled). Trader will be restarted",1,self.pair) return 2 elif restart_bot==3: self.pause = False #self.write_status_file(True) self.restart = True self.broker.logger.log_this("Error in trader, start_bot returned 3 (Slippage exceeded). Trader will be restarted",1,self.pair) return 3 else: self.broker.logger.log_this(f"Error restarting trader, trader will be removed. Error code {restart_bot}",0,self.pair) #self.write_status_file(True) self.quit = True return 1 def sum_filled_amounts(self, order: dict) -> int: ''' Adds the amount filled and the cost of an order to the totals. ''' # Add up the last order fees new_fees_base = 0 #For type checking compliance new_fees_quote = 0 # Update the total_amount_of_quote and total_amount_of_base variables if order["id"]!="": #Necessary check when adding SOs new_fees_base,new_fees_quote = self.parse_fees(order) self.fees_paid_in_quote += new_fees_quote self.total_amount_of_base = self.total_amount_of_base + order["filled"] - new_fees_base self.total_amount_of_quote = self.total_amount_of_quote + order["cost"] # Done return 0 def new_so_routine(self, filled_order: dict, send_new_so: bool) -> int: ''' Handles all the bureaucracy prior and after sending a new safety order :param filled_order: dict :param send_new_so: bool :return: 0 OK, 1 not enough funds, if can't cancel old TP, can't send new TP ''' #Let's do some type checking first if self.tp_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: 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" # Save the order self.status_dict["deal_order_history"].append(filled_order) # Cancel the 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 return 2 # Add the amount filled in the last safety order to the totals self.sum_filled_amounts(filled_order) #Cooldown time.sleep(self.broker.get_wait_time()) if self.broker.get_exchange_name()=="gateio": #Extend the wait because Gate.io sucks time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier()) # Send the new safety order. If all expected safety orders are filled, it assigns an empty order to self.so 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: 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.sum_filled_amounts(filled_order) #self.so = self.broker.get_empty_order() return 1 else: self.so = self.broker.get_empty_order() self.safety_order_index+=1 # 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) 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"] # 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] # 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] #Cooldown time.sleep(self.broker.get_wait_time()) # Send the new take profit order if self.send_new_tp_order()==1: error_string = "Problems sending the new take profit order" self.broker.logger.log_this("Problems sending the new take profit order",1,self.pair) self.status_dict["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"] = "" return 0 def check_old_long_price(self) -> int: ''' 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 price_exceeds: self.warnings["short_price_exceeds_old_long"] = True else: self.warnings["short_price_exceeds_old_long"] = False self.warnings["speol_notified"] = False if not self.warnings["speol_notified"] and price_exceeds: #Only notify one time AND if autoswitch is off self.warnings["speol_notified"] = True if not self.config_dict["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']}" self.broker.logger.log_this(message,0,self.pair) return 0 def check_orderbook_depth(self, threshold: float, order_size: float, old_price: float = 0, size_in_quote = True) -> bool: ''' Checks if the orderbook depth exceed the slippage threshold. Returns True if threshold is exceeded, False otherwise. Parameters: threshold (float): The threshold for the orderbook depth. order_size (float): The size of the order to check.. old_price (float): The old price of the order. size_in_quote (bool): If True, the order size is in quote currency. If False, the order size is in base currency. Returns: 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) return False order_book = self.broker.get_order_book(self.pair,no_retries=True) if order_book=={}: self.broker.logger.log_this("Can't fetch orderbook",1,self.pair) return False suma = 0 try: mid_price = old_price if old_price!=0 else (order_book["asks"][0][0]+order_book["bids"][0][0])/2 if size_in_quote: amount = (order_size/mid_price)*2 #Extra margin to avoid surprises. else: amount = order_size*2 first_price = order_book["asks"][0][0] for x in order_book["asks"]: suma += x[1] if suma>=amount: last_price = x[0] break if last_price-first_price>first_price*threshold: #Threshold exceeded return True return False except Exception as e: self.broker.logger.log_this(f"Exception in check_orderbook_depth: {e}",1,self.pair) return False def check_status(self,open_orders: list) -> int: #Should I change the order? Check the SO first? ''' 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) return 0 #Checks if the orders are valid if self.tp_order is None: self.broker.logger.log_this("Take profit order is None",1,self.pair) return 1 if self.so is None: #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() 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.config_dict["attempt_restart"]: self.write_status_file(True) self.restart = True self.broker.logger.log_this("Raising restart flag: take profit order missing, trader will be restarted",0,self.pair) else: self.broker.logger.log_this("Take profit order missing. Trader restart disabled.",2,self.pair) return 1 #Checks if the take profit order is filled if self.tp_order["id"] not in open_orders: tp_status = self.broker.get_order(self.tp_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) #Cancelling safety order and stopping bot self.broker.cancel_order(self.so["id"],self.pair) if self.config_dict["attempt_restart"]: self.write_status_file(True) self.restart = True self.broker.logger.log_this("Take profit order closed but not filled, trader will be restarted.",0,self.pair) else: self.broker.logger.log_this("Take profit order closed but not filled, trader restart disabled.",1,self.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_dict["attempt_restart"]: self.broker.logger.log_this("Take profit order canceled. Restarting the bot.",1,self.pair) self.write_status_file(True) self.restart = True else: self.broker.logger.log_this("Take profit order canceled. Trader restart disabled.",1,self.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.pair) return 1 # Check if safety order is filled if self.so["id"] not in open_orders and self.safety_order_index<=self.config_dict["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) #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.is_short and self.safety_order_index==self.config_dict["no_of_safety_orders"] and self.config_dict["autoswitch"]: self.switch_to_short() self.write_status_file(True) self.restart = True return 0 a = self.new_so_routine(so_status,self.safety_order_index float: ''' Returns the correct take profit percentage, according to the strategy (config_dict["tp_mode"]): 0. Fixed percentage 1. Variable percentage (+0.5% to -0.5% of the fixed percentage) 2. Custom percentage table 3. Linear percentage table ''' tp_level = 1 if self.is_short or self.config_dict["tp_mode"]==0: #Fixed take profit percentage tp_level = self.config_dict["tp_level"] elif self.config_dict["tp_mode"]==1: #Variable percentage limit = self.config_dict["no_of_safety_orders"]/3 if order_index<=1: tp_level = self.config_dict["tp_level"]+0.005 elif order_index<=limit: tp_level = self.config_dict["tp_level"] elif limit<=order_index<=limit*2: tp_level = self.config_dict["tp_level"]-0.0025 else: tp_level = self.config_dict["tp_level"]-0.005 elif self.config_dict["tp_mode"]==2: if ["tp_table"] in self.config_dict: if len(self.config_dict["tp_table"])>=order_index: tp_level = self.config_dict["tp_table"][order_index] #Custom percentage table tp_level = self.config_dict["tp_table"][-1] tp_level = self.config_dict["tp_level"] elif self.config_dict["tp_mode"]==3: #Linear percentage table profit_table = self.linear_space(self.config_dict["tp_level"]+0.005,self.config_dict["tp_level"]-0.005,self.config_dict["no_of_safety_orders"]) tp_level = profit_table[-1] if order_index str: ''' Returns a D:HH:MM:SS representation of total_seconds ''' return f"{int(total_seconds / 86400)}:" + '%02d:%02d:%02d' % (int(total_seconds % 86400 / 3600), int(total_seconds % 3600 / 60), int(total_seconds % 60)) def adjust_base(self): time.sleep(self.broker.get_wait_time()) new_balance = self.broker.get_coins_balance() if bool(new_balance): self.broker.logger.log_this(f"Adjusting base amount to {new_balance['free'][self.base]}, total balance: {new_balance['total'][self.base]}",1,self.pair) return new_balance["free"][self.base] return None def send_new_tp_order(self, multiplier: float = 1) -> int: ''' Calculates the correct take profit price and sends the order to the exchange ''' tries = self.broker.get_retries() while tries>0: if self.total_amount_of_base==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,multiplier)-1)) self.tp_order = self.broker.new_limit_order(self.pair,self.total_amount_of_base,"buy",self.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,multiplier) 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. 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()]: return 0 tries-=1 time.sleep(self.broker.get_wait_time()) self.broker.logger.log_this("Problems sending take profit order",1,self.pair) return 1 def profit_to_file(self, amount: float, orderid: str) -> int: ''' Saves the profit to the corresponding profit file DEPRECATED. Use profit_to_db instead. ''' try: with open(self.profit_filename,"a") as profit_file: profit_writer = csv.writer(profit_file, delimiter=",") profit_writer.writerow([time.strftime("%Y-%m-%d"), amount, orderid]) except Exception as e: self.broker.logger.log_this(f"Exception in profit_to_file: {e}",1,self.pair) return 0 def profit_to_db(self, amount: float, orderid: str, write_deal_order_history: bool = False) -> int: ''' Saves the profit to the db in the format (pair,timestamp,profit,exchange_name,order_id,order_history) ''' 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 "" dataset = (time.time(),self.pair,amount,self.broker.get_exchange_name(),str(orderid),order_history) 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.pair) retries-=1 time.sleep(self.broker.get_wait_time()) 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.safety_order_index+1,self.config_dict["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]) 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]) if new_order==1: self.so = 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 return 1 else: self.so = new_order self.safety_order_index+=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.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" 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.pair) return 1 def gib_so_size(self, starting_order_size: float, so_number: int, scaling_factor: float) -> float: ''' Returns the correct safety order size depending on the number Scaling factor example: 5% = 0.0105 ''' order_size = starting_order_size for _ in range(so_number): order_size = order_size*scaling_factor*100 return order_size def clip_value(self,value,lower_limit,upper_limit): ''' Clips a value to a given range ''' if valueupper_limit: return upper_limit return value def calculate_safety_prices(self, start_price: float, no_of_safety_orders: int, safety_order_deviance: float) -> list: ''' Generates a table of safety order's prices ''' safety_price_table = [start_price] if self.config_dict["dynamic_so_deviance"]:# and no_of_safety_orders>=30: #if self.config_dict["dynamic_so_deviance"] and not self.is_short: #bias should be a real number between -1 and 1 (1>n>-1, NOT 1=>n>=-1) #If bias -> 1, more space between the first orders, if -> -1, more space between the last orders, if 0, no change.. if "bias" in self.config_dict: deviance_factor = safety_order_deviance*self.clip_value(self.config_dict["bias"],-.99,.99) so_deviance_table = self.linear_space(safety_order_deviance+deviance_factor,safety_order_deviance-deviance_factor,no_of_safety_orders) else: #Old way of calculating deviance so_deviance_table = self.linear_space(safety_order_deviance-self.config_dict["dsd_range"],safety_order_deviance+self.config_dict["dsd_range"],no_of_safety_orders) so_deviance_table.extend([so_deviance_table[-1]]*2) #This extra entries are needed in the next for loop else: so_deviance_table = [safety_order_deviance]*(no_of_safety_orders+2) multiplier = -1 if self.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)) return safety_price_table def linear_space(self, start: float, stop: float, amount: int) -> list: ''' Numpy's linspace local implementation. Implemented here because: - This is the only piece of code needed from Numpy - Only executed when calculating the safety order table, so there's no need for outstanding performance. ''' result = [start] if amount in [0,1]: return result step = (start-stop)/(amount-1) for _ in range(1,amount): result.append(result[-1]-step) return result def switch_quote_currency(self, new_quote: str) -> int: ''' Replaces the open orders with new updated orders, updates the config file and the status. ''' #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.pair) return 1 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: self.broker.logger.log_this("Take profit order is None",1,self.pair) return 1 if self.so 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(): 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(): 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(): return 1 #Calls switch_quote_currency_config self.broker.logger.log_this("Modifying config file",2,self.pair) self.quote_currency_switch_configs(new_quote) #Updates status_dict self.broker.logger.log_this("Updating status file",2,self.pair) self.update_status(True) #Done self.broker.logger.log_this("Quote swap successful",2,self.pair) return 0 def quote_currency_replace_order(self, old_order: dict, new_quote: str) -> dict: ''' Cancels the order and returns the new updated order ''' #Cancels the old order if self.broker.cancel_order(old_order["id"],self.pair)==1: self.broker.logger.log_this(f"Can't cancel old order {old_order['id']}",1,self.pair) return self.broker.get_empty_order() #Sends the new order return self.broker.new_limit_order(f"{self.base}/{new_quote}",old_order["amount"],old_order["side"],old_order["price"]) def quote_currency_switch_configs(self, new_quote: str) -> int: ''' Updates the broker config file, changes all the variables and writes the new pair config file ''' #Change broker config file self.broker.remove_pair_from_config(f"{self.base}{self.quote}") 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.pair) self.quote_currency_undo_changes(new_quote,self.quote,False) return 1 #Change pair-related variables old_quote = self.quote self.quote = new_quote self.config_dict["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" self.pair = self.config_dict["pair"] #If there is an old_long file, also copy it if self.is_short and "old_long" in self.status_dict: try: with open(f"status/{self.base}{self.quote}.oldlong","w") as c: c.write(json.dumps(self.status_dict["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) #Write the new config file try: with open(f"configs/{self.base}{self.quote}.json","w") as c: c.write(json.dumps(self.config_dict, indent=4)) except Exception as e: self.broker.logger.log_this(f"Exception while writing new trader config file: {e}",1,self.pair) #Undoing changes self.quote_currency_undo_changes(new_quote,old_quote,True) return 1 #Done return 0 def quote_currency_undo_changes(self, new_quote: str, old_quote: str, write_broker_file: bool = False) -> int: ''' Revert changes made by switch_quote_currency() ''' #Switching variables self.quote = old_quote self.broker.remove_pair_from_config(f"{self.base}{new_quote}") self.broker.add_pair_to_config(f"{self.base}{self.quote}") self.config_dict["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" self.pair = self.config_dict["pair"] #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.pair) #Done return 0 def generate_status_strings(self) -> str: ''' Returns the status string properly formatted for screen output ''' yellow = "\033[0;33;40m" green = "\033[0;32;40m" red = "\033[0;31;40m" blue = "\033[0;34;40m" cyan = "\033[0;36;40m" bright_white = "\033[0;97;40m" bright_green = "\033[0;92;40m" white = "\033[0;37;40m" def draw_line(price,min_value,max_value,break_even): ''' It draws the progress bar according to the inputs: * If the price is bigger or equal to the break even price, the line's color is green * If it's lower, the line's color is red * All the way to the left, new safety order * All the way to the right, profit! ''' try: value = int(((price-min_value)/(max_value-min_value))*80) except Exception as e: self.broker.logger.log_this(f"{e}") value = 1 if min_value=break_even else red else: color = red if price>=break_even else green return f"{color}{'='*value}{white}{'='*max(0,(80-value))}"[:100] decimals = 11 low_percentage = 1 mid_percentage = 10 high_percentage = 20 safety_order_string = f"{self.status_dict['so_amount']-1}/{self.config_dict['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_boundary = '{:.20f}'.format(low_price)[:decimals].center(decimals) mid_boundary = '{:.20f}'.format(mid_price)[:decimals].center(decimals) high_boundary = '{:.20f}'.format(high_price)[:decimals].center(decimals) 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 #Formatting (on-screen percentage not longer than 4 digits) pct_to_profit_str = "{:.2f}".format(percentage_to_profit) if len(pct_to_profit_str)==4: pct_to_profit_str = f" {pct_to_profit_str}" elif len(pct_to_profit_str)==6: 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) p = "*PAUSED*" if self.pause==True else "" price_color = white target_price_color = green pair_color = cyan if self.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"]: price_color = bright_green if self.status_dict["take_profit_price"]>self.status_dict["old_long"]["tp_price"]: target_price_color = bright_green #Set percentage's color pct_color = white if percentage_to_profitmid_percentage: pct_color = yellow if percentage_to_profit>high_percentage: pct_color = red prices = f"{red}{low_boundary}{white}|{price_color}{mid_boundary}{white}|{target_price_color}{high_boundary}{white}|{pct_color}{pct_to_profit_str}%{white}" line1 = f"{p}{pair_color}{self.pair.center(13)}{white}| {safety_order_string} |{prices}| Uptime: {self.seconds_to_time(self.status_dict['deal_uptime'])}" if self.config_dict["autoswitch"]: line1 = f"{line1} | AUTO" if self.is_short and "old_long" in self.status_dict: try: percentage_to_switch = (self.status_dict["old_long"]["tp_price"]-self.status_dict["price"])*100/self.status_dict["price"] #line1 = f"{line1} {round(percentage_to_switch,2)}%" multiplier = int(percentage_to_switch/100)+1 if multiplier>1: line1 = f"{line1}x{multiplier}" except ZeroDivisionError as e: print(e) if "stop_time" in self.config_dict and time.time()<=int(self.config_dict["stop_time"]): line1 = f"{line1} | PROGRAMMED LAST DEAL" if self.stop_when_profit==True: line1 = f"{line1} | LAST DEAL" return f"{white}{line1}\n{line3}{white}" def load_imported_trader(self) -> int: ''' Loads status dictionary, orders and sets up variables ''' #Load status dict try: with open(f"status/{self.base}{self.quote}.status") as sd: self.status_dict = json.load(sd) except Exception as e: self.broker.logger.log_this(f"Exception: Couldn't load status dict. Aborting {e}",1,self.pair) self.quit = True return 1 self.status_dict["pause_reason"] = "Importing trader" #Load variables self.total_amount_of_quote = self.status_dict["quote_spent"] self.total_amount_of_base = self.status_dict["base_bought"] self.safety_order_index = self.status_dict["so_amount"] self.config_dict["no_of_safety_orders"] = self.status_dict["no_of_safety_orders"] #If this is not loaded from status_dict, it will ignore if safety orders were added at runtime self.take_profit_price = self.status_dict["take_profit_price"] self.safety_price_table = self.status_dict["safety_price_table"] self.fees_paid_in_base = self.status_dict["fees_paid_in_base"] self.fees_paid_in_quote = self.status_dict["fees_paid_in_quote"] self.start_price = self.status_dict["start_price"] self.start_time = self.status_dict["start_time"] self.deal_start_time = self.status_dict["deal_start_time"] self.stop_when_profit = self.status_dict["stop_when_profit"] if "deal_order_history" not in self.status_dict: #No longer needed? self.status_dict["deal_order_history"] = [] #Load take profit order self.tp_order = self.broker.get_order(self.status_dict["tp_order_id"],self.pair) if self.tp_order==self.broker.get_empty_order(): self.broker.logger.log_this("Couldn't load take profit order (broker returned empty order). Aborting.",1,self.pair) self.quit = True return 1 #Load safety order self.so = self.broker.get_order(self.status_dict["so_order_id"],self.pair) if self.so==self.broker.get_empty_order() and self.safety_order_index