217 lines
8.5 KiB
Python
217 lines
8.5 KiB
Python
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_profit<low_percentage:
|
|
pct_color = green
|
|
if percentage_to_profit>mid_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)) |