DCAv2/duster.py

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))