import time import json import csv class duster: def __init__(self,broker,status_info,current_price,importing=False): ''' Initializes duster. broker: broker object from exchange_wrapper status_info: old status_dict dictionary of the trader or old duster_status from an old duster importing: Boolean value, if True, the status_info dictionary is already formated as a duster. If not, it's formatted as an old status_dict dictionary from a trader. ''' self.broker = broker if not importing: order = self.broker.get_order(status_info["tp_order_id"],status_info["market"]["symbol"]) self.duster_status = {"duster_id": status_info["tp_order_id"], "pair": status_info["market"]["symbol"], "amount_spent": status_info["quote_spent"], "current_price": current_price, "deal_close_price": order["price"], "volume_on_close": order["price"]*order["amount"], "start_time": time.time() } else: self.duster_status = status_info self.quit = False self.pause = False #Maybe useful in the future def __str__(self): ''' BASE/QUOTE | order_id followed | current_price | deal_close_price | total_volume_on_close | pct_to_profit | uptime ''' yellow = "\033[0;33;40m" green = "\033[0;32;40m" red = "\033[0;31;40m" cyan = "\033[0;36;40m" white = "\033[0;37;40m" decimals = 11 low_percentage = 1 mid_percentage = 10 high_percentage = 20 #safety_order_string = f"{x.status_dict['so_amount']-1}/{x.config_dict['no_of_safety_orders']}".rjust(5) #Check if necessary mid_price = 0 high_price = 0 if self.duster_status["price"] is not None: mid_price = self.duster_status["current_price"] if self.duster_status["deal_close_price"] is not None: high_price = self.duster_status["deal_close_price"] 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.duster_status not in [0,None] and self.duster_status["current_price"]!=0: diff = abs(self.duster_status["deal_close_price"]-self.duster_status["current_price"]) percentage_to_profit = diff/self.duster_status["current_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] p = "*PAUSED*" if self.pause==True else "" price_color = white pair_color = cyan #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"{price_color}{mid_boundary}{white}|{green}{high_boundary}{white}" percentage = f"{pct_color}{pct_to_profit_str}%{white}" return f"{p}{pair_color}{self.duster_status['pair'].center(13)}{white}| {self.duster_status['id']} |{prices}| {self.duster_status['volume_on_close']} | {percentage} | Uptime: {self.seconds_to_time(time.time()-self.duster_status['start_time'])}" def set_current_price(self,price: float): ''' Writes the current price on the status dictionary ''' try: self.duster_status["current_price"] = float(price) except Exception as e: self.broker.logger.log_this(f"Error in duster. Set_current_price received a wrong value. {e}",1,self.duster_status["pair"]) return 0 def get_duster_status(self): ''' Returns the status dictionary ''' return self.duster_status def save_duster_status(self): ''' Save duster status in a file. ''' try: with open(f"status/{self.duster_status['id']}.duster_status","w") as status_file: status_file.write(json.dumps(self.duster_status, indent=4)) return 0 except Exception as e: self.broker.logger.log_this(f"Exception while writing duster status file: {e}",1,self.duster_status["pair"]) return 1 def load_duster_status(self): ''' Loads duster status to the status dictionary. ''' try: with open(f"status/{self.duster_status['id']}.duster_status") as status_file: self.duster_status = json.load(status_file) return 0 except Exception as e: self.broker.logger.log_this(f"Exception while reading duster status file: {e}",1,self.duster_status["pair"]) return 1 def check_duster_status(self,open_orders): ''' Checks duster status ''' if open_orders==[] or self.pause: return 0 if self.duster_status["id"] not in open_orders: order = self.broker.get_order(self.duster_status["id"],self.duster_status["pair"]) if order["status"]=="": self.broker.logger.log_this(f"In check_duster_status, get_order returned an empty order",1,self.duster_status["pair"]) return 1 elif order["status"]=="closed": self.report_profit(order) self.quit = True return 0 def report_profit(self,closed_order): ''' Saves profit in the profit file and sends the telegram notification. ''' _, fees_paid = self.parse_fees(closed_order) profit = closed_order["cost"]-self.duster_status["amount_spent"]-fees_paid #Write to file profit_filename = f"profits/{self.duster_status['pair'].remove('/')}.profit" try: with open(profit_filename,"a") as profit_file: profit_writer = csv.writer(profit_file, delimiter=",") profit_writer.writerow([time.strftime("%Y-%m-%d"), profit, closed_order["id"]]) except Exception as e: self.broker.logger.log_this(f"Exception in profit_to_file: {e}",1,self.duster_status["pair"]) #Send notification self.broker.logger.log_this(f"Duster closed a deal. Profit: {'{:.4f}'.format(profit)} {self.duster_status['pair'].split('/')[1]}",0,self.duster_status["pair"]) return 0 def parse_fees(self,order): ''' Returns the fees paid ordered in "base,quote" ''' basefee = 0 quotefee = 0 base,quote = self.duster_status["pair"].split("/") if self.broker.get_exchange_name()=="binance": #CCXT still to this day does not take Binance fees into account. try: fee_rate = self.broker.fetch_market["maker"] if order["type"]=="limit" else self.broker.fetch_market["taker"] except Exception as e: self.broker.logger.log_this(f"Exception fetching market information: {e}. Using default fee rate of 0.1%",1,f"{base}{quote}") fee_rate = 0.001 if order["side"]=="buy": basefee = order["filled"]*float(fee_rate) elif order["side"]=="sell": #To be implemented #Maybe: #quotefee = order["cost"]*float(fee_rate) pass return basefee,quotefee for x in order["fees"]: if x["currency"]==base: basefee+=float(x["cost"]) if x["currency"]==quote: quotefee+=float(x["cost"]) return basefee,quotefee def seconds_to_time(self,total_seconds): ''' Takes an int or float as an input and it returns a D:HH:MM:SS formatted string. ''' return f"{int(total_seconds/86400)}:" + '%02d:%02d:%02d' % (int((total_seconds % 86400) / 3600), int((total_seconds % 3600) / 60), int(total_seconds % 60))