import time from os import path, remove from json import dumps, load from config_handler import ConfigHandler from status_handler import StatusHandler class trader: def __init__(self, broker, pair: str, is_import: bool = False): #Flags self.pause = True self.quit = False self.restart = False self.warnings = { "short_price_exceeds_old_long": False, "speol_notified": False } self.trader_restart_errors = {1: "start_trader returned error #1. Trader will be restarted", 2: "start_trader returned error #2: Initial order never got filled. Trader will be restarted", 3: "start_trader returned error #3: Slippage threshold exceeded. Trader will be restarted"} self.broker = broker self.config = ConfigHandler(pair,broker) base_quote = self.config.get_pair() self.base,self.quote = base_quote.split("/") self.status = StatusHandler(broker, self.base, self.quote) self.market = self.broker.fetch_market(base_quote) self.market_load_time = int(time.time()) self.market_reload_period = 86400 #Market reload period in seconds self.status.set_start_time(int(time.time())) self.last_time_seen = time.time() #Status string caches self.low_price_cache = None self.mid_price_cache = None self.high_price_cache = None self.concurrent_so_amount_cache = None if self.config.get_is_short(): #Check if there is an old_long file. If so, load it. try: with open(f"status/{self.base}{self.quote}.oldlong") as ol: self.status.set_old_long(load(ol)) except Exception as e: self.broker.logger.log_this(f"Exception: No old_long file. {e}",1,base_quote) self.profit_filename = f"profits/{self.base}{self.quote}.profits" self.log_filename = f"logs/{self.base}{self.quote}.log" self.deals_timestamps = self.broker.get_trades_timestamps(base_quote,self.config.get_boosted_time_range()) self.status.set_pause_reason("Initialization") if is_import: self.load_imported_trader() return None # An alternative would be to set up a variable like self.is_initalized to false and finish the initialization here. # Then, in the main loop, check if self.is_initalized is false. If it is, run start_trader. start_result = self.start_trader() if start_result==0: return None #Everything is OK elif start_result==1: #If initialization fails self.quit = True elif start_result==2: #Retries exceeded if self.config.get_force_restart_if_retries_exhausted(): self.pause = False self.restart = True else: self.quit = True elif start_result==3: #Not enough liquidity self.pause = False self.restart = True def __str__(self): return self.status.get_status_string() def get_color(self, color): ''' Returns white if color does not exist ''' colors = {"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"} return colors[color] if color in colors else "\033[0;37;40m" def get_status_dict(self): return self.status.get_status() def set_market_load_time(self, period: float) -> int: self.market_load_time = period return 0 def get_market_reload_period(self) -> float: return self.market_reload_period def start_trader(self) -> int: ''' Initializes the trader. ''' #Perhaps we should search for open buy orders from a crashed trader and cancel them? #Reset some variables self.status.set_so_amount(0) self.status.clear_deal_order_history() self.status.set_take_profit_order(self.broker.get_empty_order()) self.status.set_safety_orders([]) self.status.set_safety_orders_filled(0) self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) #Reloads the market new_market_data = self.broker.fetch_market(self.status.get_pair()) if new_market_data is not None: self.market = new_market_data self.pause = True self.status.set_pause_reason("start_trader") if self.status.get_is_short(): self.broker.logger.log_this("Calculating optimal order size...",2,self.status.get_pair()) #Get minimum order size from exchange self.broker.logger.log_this("Fetching minimum order size...",2,self.status.get_pair()) min_base_size = self.broker.get_min_base_size(self.status.get_pair()) if min_base_size is None: self.broker.logger.log_this("Can't fetch the minimum order size",1,self.status.get_pair()) return 1 #Fetch the amount of free base available on the exchange self.broker.logger.log_this("Fetching free base currency on the exchange...",2,self.status.get_pair()) free_base = self.fetch_free_base() if free_base is None: self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.status.get_pair()) return 1 #Buy missing base sold because of rounding errors (rare) if self.status.get_old_long()!={}: diff = self.broker.amount_to_precision(self.status.get_pair(), self.status.get_old_long()["tp_amount"] - free_base) if diff>min_base_size: self.broker.logger.log_this(f"Buying missing {diff} {self.base}",1,self.status.get_pair()) self.broker.new_market_order(self.status.get_pair(),diff,"buy",amount_in_base=True) time.sleep(self.broker.get_wait_time()*2) #Re-quering for the amount of base currency on the exchange free_base = self.fetch_free_base() if free_base is None: self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.status.get_pair()) return 1 #Calculate order size and amount of safety orders self.broker.logger.log_this("Calculating the order size...",2,self.status.get_pair()) order_size,no_of_safety_orders = self.calculate_order_size(free_base,min_base_size,self.config.get_max_short_safety_orders()) if order_size is None or no_of_safety_orders is None: self.broker.logger.log_this("Can't calculate optimal size",1,self.status.get_pair()) return 1 self.status.set_order_size(order_size) self.status.set_no_of_safety_orders(no_of_safety_orders) self.broker.logger.log_this(f"Order size: {self.broker.amount_to_precision(self.status.get_pair(),order_size)}. Amount of safety orders: {no_of_safety_orders}",2,self.status.get_pair()) #Write the changes to the config file self.config.save_to_file() else: #Check order size self.status.set_pause_reason("start_trader - checking order size") self.broker.logger.log_this("Checking for order size",2,self.status.get_pair()) minimum_order_size_allowed = self.broker.get_min_quote_size(self.status.get_pair()) if minimum_order_size_allowed is not None and minimum_order_size_allowed>self.config.get_order_size(): self.broker.logger.log_this(f"Order size too small. Minimum order size is {minimum_order_size_allowed} {self.quote}",1,self.status.get_pair()) if minimum_order_size_allowed int: ''' Updates the status dictionary :param write_to_disk: bool - If True, writes the status file to disk. :return: int ''' try: if self.status.get_safety_orders()!=[]: self.status.set_next_so_price(self.status.get_safety_orders()[0]["price"]) elif len(self.status.get_safety_price_table())>self.status.get_safety_orders_filled(): self.status.set_next_so_price(self.status.get_safety_price_table()[self.status.get_safety_orders_filled()]) else: self.status.set_next_so_price(0) self.status.set_is_paused(self.pause) self.status.set_is_short(self.config.get_is_short()) #self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) self.status.set_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()) self.status.set_liquidate_after_switch(self.config.get_liquidate_after_switch()) except Exception as e: self.broker.logger.log_this(f"Can't update status dictionary. Exception: {e}",1,self.status.get_pair()) return 1 if write_to_disk: self.status.save_to_file() return 0 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 ''' r = scalar * 100 if abs(r - 1.0) < 1e-6: return order_size * (amount_of_so + 1) return order_size * (r * (r**amount_of_so - 1) / (r - 1)) + order_size def base_add_calculation(self, base_currency_amount: float, max_so: int = 100): ''' Calculates how many safety orders you can add to the trader with a given amount of base currency. :param base_currency_amount: float :return: int ''' if not self.config.get_is_short(): # Only works for short traders. return 0 amount_accumulated = 0 so_count = 0 for i in range(self.status.get_so_amount()+1,max_so+1): amount_accumulated+= self.gib_so_size(self.status.get_order_size(),i,self.config.get_safety_order_scale()) if amount_accumulated >= base_currency_amount: return so_count so_count+=1 return so_count 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 trader, 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 ''' low, high = min_size, amount best = 0.0 while high - low > min_size / 10: mid = (low + high) / 2 cost = self.dca_cost_calculator(mid, amount_of_safety_orders, scalar) if cost <= amount: best = mid low = mid else: high = mid return best 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 (Should this be a flag?). #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.status.get_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 traders that might be overengineering it a bit anyway ''' if self.config.get_is_short(): #Short traders 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.status.get_pair()) return 1 #balance_to_clean /= 2 #Maybe it's a good idea, sort of DCAing the dust. min_base_size = self.broker.get_min_base_size(self.status.get_pair()) minimum_cleanup_size = self.status.get_safety_orders()[0]["amount"]*2 if balance_to_clean-minimum_cleanup_size >= min_base_size: self.broker.logger.log_this(f"Balance to clean: {balance_to_clean-minimum_cleanup_size} {self.base}",2,self.status.get_pair()) self.broker.logger.log_this("Sending cleanup order...",2,self.status.get_pair()) cleanup_order = self.broker.new_limit_order(self.status.get_pair(),balance_to_clean-minimum_cleanup_size,"sell",self.status.get_take_profit_price()) if cleanup_order not in [None,self.broker.get_empty_order()]: self.broker.logger.log_this("Cleanup successful",2,self.status.get_pair()) return 0 self.broker.logger.log_this("Problems with the cleanup order",1,self.status.get_pair()) return 1 self.broker.logger.log_this("No cleanup needed",2,self.status.get_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.get_safety_order_scale()) #safety_order_scale: safety order growth factor if optimal_order_size!=0: self.broker.logger.log_this(f"Optimal order size is {optimal_order_size}",2,self.status.get_pair()) break amount_of_so-=1 if optimal_order_size==0: self.broker.logger.log_this("Not enough base to switch. Order size would be too small",1,self.status.get_pair()) self.pause = False self.status.set_pause_reason("") return None,None if optimal_order_size int: ''' This method modifies the config file of a trader 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.config.get_is_short(): #Check if the trader is already in short mode return 1 #Let's do some type checking first if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None, can't switch to short",1,self.status.get_pair()) return 1 #Pauses trader self.pause = True self.status.set_pause_reason("switch_to_short") #Fetch the real amount of available base self.broker.logger.log_this(f"Fetching available {self.base}",2,self.status.get_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.status.get_pair()) if min_base_size is None: self.broker.logger.log_this("Error. Can't fetch market info from the exchange",1,self.status.get_pair()) self.pause = False self.status.set_pause_reason("") return 1 #Check if there is enough base if self.broker.amount_to_precision(self.status.get_pair(),free_base+self.status.get_take_profit_order()["amount"])<=min_base_size: self.broker.logger.log_this("Error. Not enough base currency",1,self.status.get_pair()) self.pause = False self.status.set_pause_reason("") return 1 #Calculate order size self.broker.logger.log_this("Calculating optimal order size",2,self.status.get_pair()) optimal_order_size,amount_of_so = self.calculate_order_size(free_base+self.status.get_take_profit_order()["amount"],min_base_size,amount_of_so=self.config.get_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.status.get_pair()) self.broker.logger.log_this(f"Amount of safety orders: {amount_of_so}",2,self.status.get_pair()) #Close old orders self.broker.logger.log_this("Switching trader mode to short",2,self.status.get_pair()) self.broker.logger.log_this("Closing orders...",2,self.status.get_pair()) if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())==1: self.broker.logger.log_this("Can't cancel the take profit order. Can't switch mode",1,self.status.get_pair()) self.pause = False self.status.set_pause_reason("") return 1 if self.status.get_take_profit_order()["id"]!="": self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) #Save the old take profit order info for later use self.broker.logger.log_this("Saving state in status_dict",2,self.status.get_pair()) self.status.set_old_long({"tp_price": self.status.get_take_profit_order()["price"], "tp_amount": self.status.get_take_profit_order()["amount"], "quote_spent": self.status.get_quote_spent(), "fees_paid_in_quote": self.status.get_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(dumps(self.status.get_old_long(),indent=4)) except Exception as e: self.broker.logger.log_this(f"Exception while saving old_long file: {e}",1,self.status.get_pair()) #Modify config file accordingly self.broker.logger.log_this("Modifying config file and saving a backup",2,self.status.get_pair()) try: self.config.save_to_file(f"configs/{self.base}{self.quote}.bak") self.config.set_is_short(True) self.config.save_to_file() self.broker.logger.log_this("Config file updated",2,self.status.get_pair()) except Exception as e: self.broker.logger.log_this(f"Error. Can't write the config file. Exception: {e}",1,self.status.get_pair()) #self.pause = False return 1 self.status.set_stop_when_profit(False) #self.config.set_is_short(True) self.broker.logger.log_this("Done configuring. Starting trader...",2,self.status.get_pair()) return 0 def switch_to_long(self, ignore_old_long: bool = False, already_received_quote: float = 0) -> int: ''' Takes a short trader and changes the mode to long. Only does it if the current trader was previously a long one. ''' if not self.config.get_is_short(): self.broker.logger.log_this("Trader already in long mode, nothing to do",1,self.status.get_pair()) return 1 self.broker.logger.log_this("Attempting to switch to long trader",0,self.status.get_pair()) #Check old_long data if not ignore_old_long and self.status.get_old_long()=={}: self.broker.logger.log_this("Can't find old long info on status_dict, searching for oldlong file",1,self.status.get_pair()) try: with open(f"status/{self.base}{self.quote}.oldlong") as f: self.status.set_old_long(load(f)) except Exception as e: #self.write_to_log(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.status.get_pair()} | Can't find old long file")) self.broker.logger.log_this(f"Can't file oldlong file. Exception: {e}",1,self.status.get_pair()) self.quit = True return 1 #Cancel open orders try: for order in self.status.get_safety_orders(): self.broker.cancel_order(order["id"],self.status.get_pair()) except Exception as e: self.broker.logger.log_this(f"Error in cancel_order while cancelling safety order. Exception: {e}",1,self.status.get_pair()) try: if self.status.get_take_profit_order() is not None: self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) else: self.broker.logger.log_this("Safety order is None",1,self.status.get_pair()) except Exception as e: self.broker.logger.log_this(f"Error in cancel_order while cancelling take profit order. Exception: {e}",1,self.status.get_pair()) #Sell all base currency self.liquidate_base(ignore_profits=ignore_old_long, already_received_quote=already_received_quote) if self.config.get_liquidate_after_switch(): self.quit = True return 1 #Rewrite config file (if it exists) if path.isfile(f"configs/{self.base}{self.quote}.bak") and path.isfile(f"configs/{self.base}{self.quote}.json"): with open(f"configs/{self.base}{self.quote}.bak") as c: old_config = load(c) with open(f"configs/{self.base}{self.quote}.json","w") as c: c.write(dumps(old_config, indent=4)) if self.config.load_from_file()==1: self.config.reset_to_default() else: self.broker.logger.log_this("Config/backup file does not exist",1,self.status.get_pair()) self.config.reset_to_default() self.config.save_to_file() #Remove old_long file (if it exists) if path.isfile(f"status/{self.base}{self.quote}.oldlong"): self.broker.logger.log_this("Removing old_long file...",2,self.status.get_pair()) remove(f"status/{self.base}{self.quote}.oldlong") #Set up a few variables self.status.set_fees_paid_in_quote(0) self.status.set_fees_paid_in_base(0) self.status.set_take_profit_order(self.broker.get_empty_order()) self.status.set_safety_orders([]) self.status.set_safety_price_table([]) self.status.set_so_amount(0) #Done. Ready for start_trader return 0 def liquidate_base(self, ignore_profits: bool = True, already_received_quote: float = 0) -> 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.status.get_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.status.get_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.status.get_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.status.get_pair()) return 1 #calculate profits if not ignore_profits: profit = already_received_quote + market_tp_order["cost"] - self.status.get_old_long()["quote_spent"] - self.status.get_old_long()["fees_paid_in_quote"] - fees_paid #Add profits to file and send telegram notifying profits 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.status.get_pair()) self.broker.logger.log_this(f"Sell price: {market_tp_order['price']} {self.quote}",0,self.status.get_pair()) self.broker.logger.log_this(f"Order ID: {market_tp_order['id']}",0,self.status.get_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 trader's orders (just in case) self.status.set_pause_reason("take_profit_routine - order handling") #start_trader will set this flag to False again once it starts #Add the timestamp to the deals cache and trims it self.deals_timestamps.append(time.time()) self.deals_timestamps = self.deals_timestamps[-self.config.get_boosted_deals_range():] #Let's do some type checking first if self.status.get_take_profit_order() is None: self.status.set_pause_reason(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.status.get_pair()} | TP order is None")) self.broker.logger.log_this("Error. Take profit order is None, trader will be restarted",0,self.status.get_pair()) self.status.save_to_file(is_backup=True) self.restart = True return 1 #Save the order in history. if self.broker.get_follow_order_history(): self.status.update_deal_order_history(filled_order) #Cancel all the safety orders ASAP for order in self.status.get_safety_orders(): self.broker.cancel_order(order["id"],self.status.get_pair()) #Check if some safety orders were filled for order in self.status.get_safety_orders(): closed_order = self.broker.get_order(order["id"],self.status.get_pair()) if closed_order["filled"]==0: #If this order wasn't filled, it is safe to assume that no order coming after this one was. break #Sum the filled amounts self.broker.logger.log_this(f"Old safety order is partially filled, ID: {closed_order['id']}, {closed_order['filled']}/{closed_order['amount']} {self.base} filled",1,self.status.get_pair()) self.status.set_base_bought(self.status.get_base_bought() + closed_order["filled"] - self.parse_fees(closed_order)[0]) self.status.set_quote_spent(self.status.get_quote_spent() + closed_order["cost"]) #Save the order if self.broker.get_follow_order_history(): self.status.update_deal_order_history(closed_order) if closed_order["remaining"]!=0: #If this order is not completely filled, it is safe to assume that no order coming after this one was partially filled. break #Now we can clear the safety order list self.status.set_safety_orders([]) if not self.broker.check_for_duplicate_profit_in_db(filled_order): self.status.set_pause_reason("calculating profit") # Calculate the profit 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.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.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_db(profit,filled_order["id"],self.broker.get_write_order_history()) else: #For logging purposes self.broker.logger.log_this(f"NEGATIVE PROFIT - Total amount of base: {self.status.get_base_bought()}, base in the order: {filled_order['amount']}, base filled: {filled_order['filled']}, base 'profit': {base_profit}",1,self.status.get_pair()) self.telegram_bot_sendprofit(profit,filled_order,base_profit=base_profit) # Print profit message on screen extra = ' and {:.4f}'.format(base_profit) + f" {self.base}" if base_profit>0 else "" self.broker.logger.log_this(f"Trader closed a deal. Profit: {'{:.4f}'.format(profit)} {self.quote}{extra}",2,self.status.get_pair()) self.broker.logger.log_this(f"Fill price: {filled_order['price']} {self.quote}",2,self.status.get_pair()) self.broker.logger.log_this(f"Safety orders triggered: {self.status.get_safety_orders_filled()}",2,self.status.get_pair()) self.status.set_pause_reason("take_profit_routine - check time limit") #Checks if there is a time limit for the trader if self.config.get_programmed_stop() and time.time()>int(self.config.get_programmed_stop_time()): self.status.set_stop_when_profit(True) 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.status.get_pair()) self.quit = True return 1 #Reloading config file (just in case any changes) self.config.load_from_file() self.status.set_pause_reason("Checking slippage") if self.config.get_check_slippage(): self.broker.logger.log_this("Checking slippage...",2,self.status.get_pair()) price_to_compare = self.broker.get_top_bid_price(self.status.get_pair()) if self.config.get_is_short() else self.broker.get_top_ask_price(self.status.get_pair()) if abs(filled_order["price"]-price_to_compare)/filled_order["price"]>self.broker.get_slippage_default_threshold(): self.broker.logger.log_this(f"Slippage threshold exceeded, waiting for cooldown and restarting trader",1,self.status.get_pair()) time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier()) #The trader is restarted by the instance instead of by itself to allow a couple of more seconds for the price to return to normal. #This could also be the default behavior. self.pause = False self.restart = True return 1 elif self.check_orderbook_depth(self.broker.get_slippage_default_threshold(),self.status.get_order_size(),filled_order["price"]): self.broker.logger.log_this(f"Orderbook depth not sufficient, waiting for cooldown and restarting trader",1,self.status.get_pair()) time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier()) self.pause = False self.restart = True return 1 #Restarting the trader self.status.set_pause_reason("take_profit_routine - restart_trader call") restart_trader = self.start_trader() self.status.set_pause_reason("take_profit_routine - restart_trader call - start_trader() called") #retries = self.broker.get_retries() if restart_trader in self.trader_restart_errors.keys(): self.pause = False self.restart = True self.status.save_to_file(is_backup=True) self.broker.logger.log_this(self.trader_restart_errors[restart_trader],1,self.status.get_pair()) return restart_trader def send_new_safety_order_batch(self, amount: int): """ Sends a new safety order batch to the broker :param amount: int - The amount of safety orders to send. :return: The amount of orders succesfully sent. None if an error occurs. """ if amount<1: return 0 orders_to_place = min(self.status.get_no_of_safety_orders()-self.status.get_so_amount(),amount) if orders_to_place<1: return 0 orders_placed = 0 # if self.broker.get_exchange_name()!="binance": #Binance does not support sending multiple orders at once in Spot. # self.broker.logger.log_this(f"Sending {orders_to_place} safety orders",2,self.status.get_pair()) # orders_to_send = [] # for i in range(orders_to_place): # order_index = self.status.get_so_amount()+i+1 # so_size = self.gib_so_size(self.status.get_order_size(),order_index,self.config.get_safety_order_scale()) # if self.config.get_is_short(): # orders_to_send.append({"symbol": self.status.get_pair(), # "type": "limit", # "side": "sell", # "amount": so_size, # "price": self.status.get_safety_price_table()[order_index]}) # else: # orders_to_send.append({"symbol": self.status.get_pair(), # "type": "limit", # "side": "buy", # "amount": so_size/self.status.get_safety_price_table()[order_index], # "price": self.status.get_safety_price_table()[order_index]}) # sent_orders = self.broker.new_limit_orders(orders_to_send) # orders_placed = len(sent_orders) # self.status.set_so_amount(self.status.get_so_amount()+orders_placed) # for item in sent_orders: # self.status.add_safety_order(item) # else: for i in range(orders_to_place): self.broker.logger.log_this(f"Sending a new safety order ({i+1}/{orders_to_place})",2,self.status.get_pair()) so_size = self.gib_so_size(self.status.get_order_size(),self.status.get_so_amount()+1,self.config.get_safety_order_scale()) if self.config.get_is_short(): new_order = self.broker.new_limit_order(self.status.get_pair(),so_size,"sell",self.status.get_safety_price_table()[self.status.get_so_amount()+1]) else: new_order = self.broker.new_limit_order(self.status.get_pair(),so_size/self.status.get_safety_price_table()[self.status.get_so_amount()+1],"buy",self.status.get_safety_price_table()[self.status.get_so_amount()+1]) if new_order==1: self.broker.logger.log_this("Not enough balance to send a new safety order",1,self.status.get_pair()) self.status.set_no_of_safety_orders(self.status.get_so_amount()) #To avoid sending more safety orders, no_of_safety_order can be later modified manually. return orders_placed elif new_order is None: self.broker.logger.log_this("new_limit_order returned None",1,self.status.get_pair()) self.status.set_no_of_safety_orders(self.status.get_so_amount()) #To avoid sending more safety orders, no_of_safety_order can be later modified manually. return orders_placed elif new_order==self.broker.get_empty_order(): self.broker.logger.log_this("new_limit_order returned an empty order",1,self.status.get_pair()) self.status.set_no_of_safety_orders(self.status.get_so_amount()) #To avoid sending more safety orders, no_of_safety_order can be later modified manually. return orders_placed else: orders_placed+=1 self.status.add_safety_order(new_order) self.status.set_so_amount(self.status.get_so_amount()+1) return orders_placed def renew_tp_and_so_routine(self, filled_safety_orders: list): ''' Modifies the current take profit order and sends a new safety order :return: 0 OK, 1 take profit order is None, 2 not enough funds, 3 can't cancel TP (filled?), 4 can't send new TP ''' safety_orders_to_remove_by_id = [] #Check if current TP order is valid if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None, can't send a new safety order",1,self.status.get_pair()) return 1 #Pause the trader self.pause = True self.status.set_pause_reason("renew_tp_and_so_routine") #Save the order if self.broker.get_follow_order_history(): for item in filled_safety_orders: self.status.update_deal_order_history(item) #Add the amount filled in the last safety orders to the totals for order in filled_safety_orders: safety_orders_to_remove_by_id.append(order["id"]) new_fees_base,new_fees_quote = self.parse_fees(order) self.status.set_fees_paid_in_quote(self.status.get_fees_paid_in_quote() + new_fees_quote) self.status.set_base_bought(self.status.get_base_bought() + order["filled"] - new_fees_base) self.status.set_quote_spent(self.status.get_quote_spent() + order["cost"]) #Remove the filled safety orders from the list if safety_orders_to_remove_by_id!=[]: new_order_list = [] #Remove filled orders from the list for order in self.status.get_safety_orders(): if order["id"] not in safety_orders_to_remove_by_id: new_order_list.append(order) self.status.set_safety_orders(new_order_list) #Cancel old TP order if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())==1: error_string = f"{self.status.get_pair()} | {self.status.get_take_profit_order()['id']} | Old TP order probably filled. Can't cancel. This trader should be restarted" self.broker.logger.log_this(f"Old take profit order is probably filled, can't cancel. This trader should be restarted. Order ID: {self.status.get_take_profit_order()['id']}",1,self.status.get_pair()) self.status.set_pause_reason(error_string) return 2 #Check if old TP order was partially filled old_tp_order = self.broker.get_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) if old_tp_order["filled"]>0: self.broker.logger.log_this(f"Old take profit order is partially filled, id {old_tp_order['id']}",1,self.status.get_pair()) if self.broker.get_follow_order_history(): self.status.update_deal_order_history(old_tp_order) #self.status.set_base_bought(old_tp_order["remaining"]) # Partial profit calculation if not self.config.get_is_short(): current_deal_price = self.status.get_base_bought()/self.status.get_quote_spent() self.status.set_partial_profit(self.status.get_partial_profit()+old_tp_order["cost"]-(old_tp_order["filled"]*current_deal_price)-self.parse_fees(old_tp_order)[1]) self.update_status(True) 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_before_new_safety_order()) #Send new SO(s) #Do not send new orders if the max amount is reached or surpassed. #It can happen if the max amount of concurrent orders is modified through an API call. max_orders = self.config.get_concurrent_safety_orders() if not self.status.get_is_boosted() else self.config.get_boosted_concurrent_safety_orders() if len(self.status.get_safety_orders()) int: ''' Checks if short price exceeds old long price. If so, send a Telegram message ''' price_exceeds = False if self.status.get_old_long()!={}: price_exceeds = self.status.get_price()>float(self.status.get_old_long()["tp_price"]) 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.get_autoswitch(): message = f"{self.base}@{self.status.get_price()} ({str(self.broker.exchange)}), exceeds old long price of {self.status.get_old_long()['tp_price']}" self.broker.logger.log_this(message,0,self.status.get_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 exceeds the slippage threshold. Returns True if it 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.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.status.get_pair(),no_retries=True) if order_book=={}: self.broker.logger.log_this("Can't fetch orderbook",1,self.status.get_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 top_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-top_price>top_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.status.get_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 #Check if short price exceeds old long price. If so, send a Telegram message if self.config.get_is_short() and self.status.get_old_long()!={} and self.config.get_check_old_long_price(): self.check_old_long_price() self.status.set_pause_reason("check for autoswitch") #If it's a short trader that used to be long AND autoswitch is enabled if self.config.get_is_short() and self.config.get_autoswitch() and self.status.get_old_long()!={}: #If selling the base currency left at the current market price plus the quote already received turns out to be more than the old long deal target, # it means that we already are in profit territory, switch back to long. #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.set_pause_reason("automatic_switch") self.switch_to_long(already_received_quote=self.status.get_quote_spent()) if not self.config.get_liquidate_after_switch(): self.restart = True return 1 #Extract ids from order list open_orders_ids = [order["id"] for order in open_orders if order["symbol"]==self.status.get_pair()] #Checks if the take profit order is valid if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None",1,self.status.get_pair()) return 1 if self.status.get_take_profit_order()["id"]=="": self.broker.logger.log_this(f"Take profit order missing. Stopping trader. No order ID was provided.",1,self.status.get_pair()) #Cancelling safety orders for item in self.status.get_safety_orders(): self.broker.cancel_order(item["id"],self.status.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True self.broker.logger.log_this("Raising restart flag: take profit order missing, trader will be restarted",0,self.status.get_pair()) else: self.broker.logger.log_this("Take profit order missing. Trader restart disabled.",2,self.status.get_pair()) return 1 #Checks if the take profit order is filled if self.status.get_take_profit_order()["id"] not in open_orders_ids: tp_status = self.broker.get_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) if tp_status["status"]=="closed": if tp_status["filled"]>0: return self.take_profit_routine(tp_status) self.broker.logger.log_this(f"Take profit order closed but not filled, 0 filled. Stopping trader. Order ID: {self.status.get_take_profit_order()['id']}",1,self.status.get_pair()) #Cancelling safety orders for item in self.status.get_safety_orders(): self.broker.cancel_order(item["id"],self.status.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True self.broker.logger.log_this("Take profit order closed but not filled, trader will be restarted.",0,self.status.get_pair()) else: self.broker.logger.log_this("Take profit order closed but not filled, trader restart disabled.",1,self.status.get_pair()) return 1 elif tp_status["status"]=="canceled": #TODO: Here, if the safety order is still open, we could resend the tp order. if self.config.get_attempt_restart(): self.broker.logger.log_this("Take profit order canceled. Restarting the trader.",1,self.status.get_pair()) self.status.save_to_file(is_backup=True) self.restart = True else: self.broker.logger.log_this("Take profit order canceled. Trader restart disabled.",1,self.status.get_pair()) return 1 elif tp_status["status"]=="": self.broker.logger.log_this(f"Take profit order search returned empty order. Order ID: {tp_status['id']}",1,self.status.get_pair()) return 1 # Check if any safety order is filled filled_ids = [] for order in self.status.get_safety_orders(): if order["id"] not in open_orders_ids: filled_ids.append(order["id"]) if filled_ids!=[]: closed_orders = self.broker.get_closed_orders(self.status.get_pair()) filled_orders = [item for item in closed_orders if item["id"] in filled_ids and item["status"]=="closed"] #maybe item["status"] in ["closed", "canceled", ""]? self.status.set_safety_orders_filled(self.status.get_safety_orders_filled()+len(filled_orders)) renew_outcome = self.renew_tp_and_so_routine(filled_orders) #0 OK, 1 take profit order is None, 2 not enough funds, 3 can't cancel TP (filled?), 4 can't send new TP if renew_outcome==1: self.broker.logger.log_this(f"Error in trader: TP order is None. Restart will be attempted. renew_tp_and_so_routine returned 1",0,self.status.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True return 1 elif renew_outcome==2: #Not enough funds? self.broker.logger.log_this(f"Can't send new safety order. Not enough funds? renew_tp_and_so_routine returned 2",1,self.status.get_pair()) #Set no_of_safety_orders to the same amount of orders open so the script does not try to send new safety orders #This can be improved self.status.set_no_of_safety_orders(self.status.get_so_amount()) return 1 elif renew_outcome==3: self.broker.logger.log_this(f"Can't cancel old take profit order. renew_tp_and_so_routine returned 3",1,self.status.get_pair()) self.pause = False self.status.set_pause_reason("") if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True return 1 elif renew_outcome==4: self.broker.logger.log_this(f"Error in trader: Can't send new take profit order. Restart will be attempted. renew_tp_and_so_routine returned 4",0,self.status.get_pair()) if self.config.get_attempt_restart(): self.status.save_to_file(is_backup=True) self.restart = True return 1 #Should we send more safety orders without touching the TP order? #Necessary check if we add to no_of_safety_orders or modify concurrent_safety_orders at runtime max_concurrent_safety_orders = self.config.get_boosted_concurrent_safety_orders() if self.status.get_is_boosted() else self.config.get_concurrent_safety_orders() condition_a = len(self.status.get_safety_orders())short) #Commented out because i'm not sure where this should go #if not self.config.get_is_short() and self.status.get_so_amount()==self.status.get_no_of_safety_orders() and self.config.get_autoswitch(): # self.switch_to_short() # self.status.save_to_file(is_backup=True) # self.restart = True # return 0 #Render status line(s) self.status.set_status_string(self.generate_status_strings()) #Wrap up 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()) return 0 def check_boosted(self): ''' Checks if the trader qualifies for boost: The last n deals must be within the last t seconds ''' 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: ''' Returns the correct take profit percentage, according to the strategy (config.get_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.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.status.get_no_of_safety_orders()/3 if order_index<=1: tp_level = self.config.get_tp_level()+0.005 elif order_index<=limit: tp_level = self.config.get_tp_level() elif limit<=order_index<=limit*2: tp_level = self.config.get_tp_level()-0.0025 else: tp_level = self.config.get_tp_level()-0.005 elif self.config.get_tp_mode()==2: if self.config.get_tp_table()!=[]: if len(self.config.get_tp_table())>=order_index: tp_level = self.config.get_tp_table()[order_index] #Custom percentage table tp_level = self.config.get_tp_table()[-1] tp_level = self.config.get_tp_level() elif self.config.get_tp_mode()==3: #Linear percentage table profit_table = self.linear_space(self.config.get_tp_level()+0.005,self.config.get_tp_level()-0.005,self.config.get_no_of_safety_orders()) tp_level = profit_table[-1] if order_index str: ''' Returns a D:HH:MM:SS representation of total_seconds ''' days = int(total_seconds // 86400) h, m, sec = int((total_seconds % 86400) // 3600), int((total_seconds % 3600) // 60), int(total_seconds % 60) return f"{days}:{h:02d}:{m:02d}:{sec:02d}" 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.status.get_pair()) return new_balance["free"][self.base] return None def send_new_tp_order(self) -> int: ''' Calculates the correct take profit price and sends the order to the exchange ''' tries = self.broker.get_retries() while tries>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.status.get_pair()) return 1 if self.config.get_is_short(): self.status.set_take_profit_price(self.status.get_quote_spent()/self.status.get_base_bought()*(1-(self.get_tp_level(self.status.get_so_amount())-1))) self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"buy",self.status.get_take_profit_price())) else: self.status.set_take_profit_price(self.status.get_quote_spent()/self.status.get_base_bought()*self.get_tp_level(self.status.get_so_amount())) self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"sell",self.status.get_take_profit_price())) if self.status.get_take_profit_order()==1: #This means that there was a miscalculation of base currency amount, let's correct it. if self.config.get_is_short(): #If in short mode, we don't recalculate anything. return 1 adjusted = self.adjust_base() if adjusted is not None: 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()) self.broker.logger.log_this("Problems sending take profit order",1,self.status.get_pair()) return 1 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 = 5 #Hardcoded because it's not an API call while retries>0: try: order_history = dumps(self.status.get_deal_order_history()) if write_deal_order_history else "" dataset = (time.time(),self.status.get_pair(),amount,self.broker.get_exchange_name(),str(orderid),order_history) #Write profit to cache self.broker.write_profit_to_cache(dataset) return self.broker.write_profit_to_db(dataset) except Exception as e: self.broker.logger.log_this(f"Exception while writing profit: {e}",1,self.status.get_pair()) retries-=1 time.sleep(.1) #Shorter wait time since it's not an API call return 1 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.status.get_pair()} closed a {'short' if self.config.get_is_short() else 'long'} trade.\nProfit: {round(profit,6)} {self.quote}{extra}\nSafety orders triggered: {self.status.get_safety_orders_filled()}\nTake profit price: {order['price']} {self.quote}\nTrade size: {round(order['cost'],2)} {self.quote}\nDeal uptime: {self.seconds_to_time(self.status.get_deal_uptime())}\nOrder ID: {order['id']}\nExchange: {self.broker.get_exchange_name().capitalize()}\n" self.broker.logger.send_tg_message(message) return 0 except Exception as e: self.broker.logger.log_this(f"Exception in telegram_bot_sendprofit: {e}",1,self.status.get_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 ''' return starting_order_size * (scaling_factor*100)**so_number 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.get_dynamic_so_deviance(): #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.. deviance_factor = safety_order_deviance*self.clip_value(self.config.get_bias(),-.99,.99) so_deviance_table = self.linear_space(safety_order_deviance+deviance_factor,safety_order_deviance-deviance_factor,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.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)) 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. ''' step = (stop - start) / (amount - 1) return [start + i * step for i in range(amount)] 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.status.get_pair()) return 1 if "active" in market and not market["active"]: self.broker.logger.log_this("Market is closed",1,self.status.get_pair()) return 1 if self.status.get_take_profit_order() is None: self.broker.logger.log_this("Take profit order is None",1,self.status.get_pair()) return 1 #Replace the current take profit order with a new one with new quote currency self.broker.logger.log_this("Replacing take profit order",2,self.status.get_pair()) self.status.set_take_profit_order(self.quote_currency_replace_order(self.status.get_take_profit_order(),new_quote)) if self.status.get_take_profit_order()==self.broker.get_empty_order(): return 1 #Replace the current safety orders (if any) with new ones with the new quote currency #This is WRONG: We need to build a list of the newly sent orders and assign them with self.status.set_safety_orders() new_order_list = [] for order in self.status.get_safety_orders(): self.broker.logger.log_this("Replacing safety order",2,self.status.get_pair()) new_order_list.append(self.quote_currency_replace_order(order,new_quote)) self.status.set_safety_orders(new_order_list) #Calls switch_quote_currency_config self.broker.logger.log_this("Modifying config file",2,self.status.get_pair()) self.quote_currency_switch_configs(new_quote) #Updates status_dict self.broker.logger.log_this("Updating status file",2,self.status.get_pair()) self.status.set_status_file_path(f"status/{self.base}{self.quote}.status") self.update_status(True) #Done self.broker.logger.log_this("Quote swap successful",2,self.status.get_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.status.get_pair())==1: self.broker.logger.log_this(f"Can't cancel old order {old_order['id']}",1,self.status.get_pair()) return self.broker.get_empty_order() #Sends the new order 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 trader 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.status.get_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.set_pair(f"{self.base}/{self.quote}") self.status.set_pair(f"{self.base}/{self.quote}") self.profit_filename = f"profits/{self.base}{self.quote}.profits" self.log_filename = f"logs/{self.base}{self.quote}.log" #If there is an old_long file, also copy it 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(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.status.get_pair()) #Write the new config file self.config.set_config_file_path(f"configs/{self.base}{self.quote}.json") if self.config.save_to_file()==1: self.broker.logger.log_this(f"Error while writing the new trader config file",1,self.status.get_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.set_pair(f"{self.base}/{self.quote}") self.status.set_pair(f"{self.base}/{self.quote}") self.profit_filename = f"profits/{self.base}{self.quote}.profits" self.log_filename = f"logs/{self.base}{self.quote}.log" #Writing config file if write_broker_file and self.broker.rewrite_config_file()==1: self.broker.logger.log_this("Error in quote_currency_undo_changed: error writing new broker config file",1,self.status.get_pair()) #Done return 0 def generate_status_strings(self) -> str: ''' Returns the status string properly formatted for screen output ''' decimals = 11 low_percentage = 1 mid_percentage = 10 high_percentage = 20 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 self.get_color("red") else: color = self.get_color("red") if price>=break_even else self.get_color("green") return f"{color}{'='*value}{self.get_color('white')}{'='*max(0,(80-value))}"[:100] low_price = self.status.get_next_so_price() if self.status.get_next_so_price() is not None else 0 mid_price = self.status.get_price() if self.status.get_price() is not None else 0 high_price = self.status.get_take_profit_price() if self.status.get_take_profit_price() is not None else 0 concurrent_so_amount = len(self.status.get_safety_orders()) if low_price==self.low_price_cache and mid_price==self.mid_price_cache and high_price==self.high_price_cache and concurrent_so_amount==self.concurrent_so_amount_cache: #Only modifies the uptime position = self.status.get_status_string().find("Uptime") new_uptime = self.seconds_to_time(self.status.get_deal_uptime()) status_string = self.status.get_status_string()[:position+8] + new_uptime + self.status.get_status_string()[position+8+len(new_uptime):] else: #Update caches self.low_price_cache = low_price self.mid_price_cache = mid_price self.high_price_cache = high_price self.concurrent_so_amount_cache = concurrent_so_amount #Formatting low_boundary = '{:.20f}'.format(low_price)[:decimals].center(decimals) 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 mid_price!=0: diff = abs(high_price-mid_price) percentage_to_profit = diff/mid_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.status.get_base_bought()!=0: line3 = draw_line(mid_price,low_price,high_price,self.status.get_quote_spent()/self.status.get_base_bought()) p = "*PAUSED*" if self.pause==True else "" low_boundary_color = self.get_color("red") price_color = self.get_color("white") target_price_color = self.get_color("green") pair_color = self.get_color("cyan") if self.config.get_is_short(): price_color = self.get_color("white") pair_color = self.get_color("yellow") if self.status.get_old_long()!={}: if mid_price>self.status.get_old_long()["tp_price"]: price_color = self.get_color("bright_green") if high_price>self.status.get_old_long()["tp_price"]: target_price_color = self.get_color("bright_green") #Set percentage's color pct_color = self.get_color("white") if percentage_to_profitmid_percentage: pct_color = self.get_color("yellow") if percentage_to_profit>high_percentage: pct_color = self.get_color("red") multiplier = 0 if self.config.get_is_short() and self.status.get_old_long()!={}: try: #Logic to display switch price 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() minimum_switch_price = (old_target - self.status.get_quote_spent())/base_left if old_target-self.status.get_quote_spent()>0 and base_left>0 and minimum_switch_price1: #Only displays the multiplier if autoswitch is enabled. line1 = f"{line1}{auto_color}x{multiplier}{self.get_color('white')}" if self.config.get_programmed_stop() and time.time()<=self.config.get_programmed_stop_time(): line1 = f"{line1} | PROGRAMMED LAST DEAL" if self.status.get_stop_when_profit(): line1 = f"{line1} | LAST DEAL" status_string = f"{self.get_color('white')}{line1}\n{line3}{self.get_color('white')}" return status_string def load_imported_trader(self) -> int: ''' Loads status dictionary, orders and sets up variables ''' #Load status dict if self.status.load_from_file()==1: self.broker.logger.log_this(f"Error: Couldn't load status dict. Aborting",1,self.status.get_pair()) self.quit = True return 1 self.status.set_pause_reason("Importing trader") #self.config.set_no_of_safety_orders(self.status.get_no_of_safety_orders()) #If this is not loaded from status_dict, it will ignore if safety orders were added at runtime #Refresh take profit order order_id = self.status.get_take_profit_order()["id"] self.status.set_take_profit_order(self.broker.get_order(order_id,self.status.get_pair())) if self.status.get_take_profit_order()==self.broker.get_empty_order(): self.broker.logger.log_this("Couldn't load take profit order (broker returned empty order). Aborting.",1,self.status.get_pair()) self.quit = True return 1 #MIGRATION: ONCE MIGRATION IS DONE, REMOVE THIS CODE # Copy the current safety order (if it exists) to the new safety order list # Set the concurrent safety orders setting to 1 (to avoid sending a big amount of orders at startup) # Save the modified config file #if self.status.status_dictionary["safety_order"]["id"]!="": # self.status.set_safety_orders([self.status.status_dictionary["safety_order"]]) #self.config.set_concurrent_safety_orders(1) #self.config.save_to_file() #self.status.set_safety_orders_filled(self.status.get_so_amount()-1) #Done self.pause = False self.status.set_pause_reason("") self.update_status(True) return 0