1823 lines
93 KiB
Python
Executable File
1823 lines
93 KiB
Python
Executable File
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_trader 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.check_slippage = True
|
|
if "check_slippage" in self.config_dict:
|
|
self.check_slippage = self.config_dict["check_slippage"]
|
|
self.is_boosted = False
|
|
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(time.time()):
|
|
self.config_dict.pop("stop_time",None)
|
|
#Write config file changes
|
|
self.broker.rewrite_config_file()
|
|
if self.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_dict["old_long"] = json.load(ol)
|
|
except Exception as e:
|
|
self.broker.logger.log_this(f"Exception: No old_long file. {e}",1,self.pair)
|
|
self.profit_filename = f"profits/{self.base}{self.quote}.profits"
|
|
self.log_filename = f"logs/{self.base}{self.quote}.log"
|
|
|
|
self.boosted_deals_range = 4
|
|
self.boosted_time_range = 3600
|
|
self.boosted_amount = .01
|
|
if "boosted_deals_range" in self.config_dict:
|
|
self.boosted_deals_range = self.config_dict["boosted_deals_range"]
|
|
if "boosted_time_range" in self.config_dict:
|
|
self.boosted_time_range = self.config_dict["boosted_time_range"]
|
|
if "boosted_amount" in self.config_dict:
|
|
self.boosted_amount = self.config_dict["boosted_amount"]
|
|
self.deals_timestamps = self.broker.get_trades_timestamps(self.pair,self.boosted_time_range)
|
|
|
|
self.stop_when_profit = False
|
|
self.status_dict["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
|
|
self.quit = True
|
|
elif start_result==3: #Not enough liquidity
|
|
self.pause = False
|
|
self.restart = True
|
|
|
|
|
|
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 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_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.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_trader"
|
|
|
|
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_trader - 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<self.config_dict["order_size"]*2:
|
|
#int(n)+1 is treated here as a simplified ceil function, since minimum_order_size_allowed will always be positive.
|
|
self.broker.logger.log_this(f"Due to exchange limits, trader initial order size will be {float(int(minimum_order_size_allowed)+1)} {self.quote}",1,self.pair)
|
|
self.config_dict["order_size"] = float(int(minimum_order_size_allowed)+1)
|
|
else:
|
|
self.broker.logger.log_this("Limit difference is more than 2x the configured order size. Please adjust the order size in the trader config file and restart the trader.",1,self.pair)
|
|
return 1
|
|
|
|
#check slippage
|
|
if self.check_slippage:
|
|
self.broker.logger.log_this("Checking slippage...",2,self.pair)
|
|
self.status_dict["pause_reason"] = "start_trader - checking slippage"
|
|
if self.check_orderbook_depth(self.broker.get_slippage_default_threshold(),self.config_dict["order_size"]):
|
|
#Slippage threshold exceeded
|
|
self.broker.logger.log_this("Slippage threshold exceeded",1,self.pair)
|
|
return 3
|
|
|
|
self.status_dict["pause_reason"] = "start_trader - after slippage"
|
|
self.status_dict["order_size"] = self.config_dict["order_size"]
|
|
|
|
#Sending initial order
|
|
self.status_dict["pause_reason"] = "start_trader - sending first order"
|
|
self.broker.logger.log_this("Sending first order...",2,self.pair)
|
|
action = "sell" if self.is_short else "buy"
|
|
first_order = self.broker.new_market_order(self.pair,self.config_dict["order_size"],action)
|
|
#self.broker.logger.log_this(f"First order id: {first_order}",1,self.pair)
|
|
if first_order in [None,self.broker.get_empty_order()]:
|
|
self.broker.logger.log_this(f"Error sending the first order. Market order returned {first_order}",1,self.pair)
|
|
return 1
|
|
tries = self.broker.get_retries()*2 #This is really necessary, don't change it. Don't. DON'T.
|
|
|
|
#Wait until the first order gets filled
|
|
self.status_dict["pause_reason"] = "start_trader - waiting for the first order to get filled"
|
|
while True:
|
|
#Wait a bit longer, to catch a bug:
|
|
#Sometimes the amount of base taken into account by the trader is lower than the amount bought,
|
|
# which ends up misrepresenting the trade cost per unit of base, which causes the take profit price to skyrocket.
|
|
# Maybe is the first market order getting "closed" before is fully filled?
|
|
# Or is there an error later in the trader?
|
|
time.sleep(self.broker.get_wait_time())
|
|
returned_order = self.broker.get_order(first_order["id"],self.pair)
|
|
if returned_order==self.broker.get_empty_order():
|
|
self.broker.logger.log_this("Problems with the initial order",1,self.pair)
|
|
return 1
|
|
elif returned_order["status"]=="closed":
|
|
break
|
|
elif returned_order["status"]=="expired":
|
|
self.broker.logger.log_this(f"First order expired. Id: {returned_order['id']}",1,self.pair)
|
|
return 1
|
|
else:
|
|
tries-=1
|
|
self.broker.logger.log_this("Waiting for initial order to get filled...",2,self.pair)
|
|
self.broker.logger.log_this(f"Order ID: {returned_order['id']}",2,self.pair)
|
|
if tries==0:
|
|
self.broker.logger.log_this("Restart retries exhausted.",0,self.pair)
|
|
self.broker.cancel_order(returned_order["id"],self.pair)
|
|
#self.restart = True #This restart is tricky, it can end up in an endless loop of retries
|
|
#By this point, both the take_profit_routine initialization AND the subsequent restart attempt failed.
|
|
#Since it only reaches this point very unfrequently, let we'll the trader get stuck in a pause state.
|
|
return 2
|
|
|
|
#Save the order
|
|
self.status_dict["pause_reason"] = "start_trader - saving the order in deal_order_history"
|
|
self.status_dict["deal_order_history"].append(returned_order)
|
|
|
|
# Reset the fee count and sum fees from the first order
|
|
self.fees_paid_in_base, self.fees_paid_in_quote = self.parse_fees(returned_order)
|
|
self.broker.logger.log_this(f"Fees paid: {self.fees_paid_in_base} {self.base}, {self.fees_paid_in_quote} {self.quote}",2,self.pair)
|
|
self.broker.logger.log_this(f"Take profit order ID: {returned_order['id']}",2,self.pair)
|
|
|
|
# Sum total amount of quote and base
|
|
if returned_order["filled"]!=None:
|
|
self.total_amount_of_base = returned_order["filled"]
|
|
if not self.is_short:
|
|
#self.total_amount_of_base -= self.parse_fees(returned_order)[0] #The substraction is because some exchanges charges some fees in base
|
|
self.total_amount_of_base -= self.fees_paid_in_base
|
|
self.total_amount_of_quote = returned_order["cost"]
|
|
else:
|
|
self.broker.logger.log_this("Error starting bot. Aborting.",1,self.pair)
|
|
return 1
|
|
|
|
# Send the take profit order
|
|
self.status_dict["pause_reason"] = "start_trader - sending tp order"
|
|
self.broker.logger.log_this("Sending take profit order...",2,self.pair)
|
|
if self.send_new_tp_order()==0:
|
|
self.broker.logger.log_this("Take profit order sent",2,self.pair)
|
|
else:
|
|
self.broker.logger.log_this("Error sending take profit order. Aborting.",1,self.pair)
|
|
return 1
|
|
|
|
# Generate the safety prices table
|
|
#self.start_price = returned_order["average"]
|
|
self.start_price = self.broker.price_to_precision(self.pair,self.total_amount_of_quote/self.total_amount_of_base)
|
|
self.safety_price_table = self.calculate_safety_prices(self.start_price,self.config_dict["no_of_safety_orders"],self.config_dict["safety_order_deviance"])
|
|
|
|
# Send the first safety order
|
|
self.status_dict["pause_reason"] = "start_trader - sending safety order"
|
|
self.broker.logger.log_this("Sending safety order...",2,self.pair)
|
|
if self.send_new_safety_order(self.status_dict["order_size"])==0:
|
|
self.broker.logger.log_this("Safety order sent",2,self.pair)
|
|
else:
|
|
self.broker.logger.log_this("Error sending safety order. Cancelling take profit order and aborting",1,self.pair)
|
|
self.broker.cancel_order(self.tp_order["id"],self.pair)
|
|
return 1
|
|
|
|
# Send cleanup order (if cleanup)
|
|
self.status_dict["pause_reason"] = "start_trader - doing cleanup (if needed)"
|
|
if self.config_dict["cleanup"] and not self.is_short: #Short traders do not need cleanup.
|
|
self.do_cleanup()
|
|
|
|
# Write variables to status_dict and reset deal_uptime
|
|
self.deal_start_time = int(time.time())
|
|
self.update_status(True)
|
|
self.pause = False
|
|
self.status_dict["pause_reason"] = ""
|
|
return 0
|
|
|
|
|
|
|
|
def reload_config_dict(self) -> 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_boosted"]=self.is_boosted
|
|
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 (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.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<min_base_size: #Sometimes amount_to_precision rounds to a value less than the minimum
|
|
self.broker.logger.log_this("Optimal order size is smaller than the minimum order size",1,self.pair)
|
|
self.pause = False
|
|
self.status_dict["pause_reason"] = ""
|
|
return None,None
|
|
return optimal_order_size,amount_of_so
|
|
|
|
|
|
def fetch_free_base(self,currency: str = ""):
|
|
'''
|
|
Returns the amount of free currency on the exchange
|
|
'''
|
|
|
|
if currency=="":
|
|
currency = self.base
|
|
balance = self.broker.get_coins_balance()
|
|
if balance==[]:
|
|
self.broker.logger.log_this("Can't fetch free base from the exchange",1,self.pair)
|
|
return None
|
|
if currency in balance["free"]:
|
|
return float(balance["free"][currency])
|
|
self.broker.logger.log_this("Currency not present in balance",1,self.pair)
|
|
return 0
|
|
|
|
|
|
def switch_to_short(self) -> 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, ignore_old_long: bool = False, already_received_quote: float = 0) -> 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
|
|
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)
|
|
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, already_received_quote=already_received_quote)
|
|
|
|
#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_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.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 = already_received_quote + 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_trader will set this flag to False again once it starts
|
|
|
|
#Add the timestamp to the deals cache
|
|
self.deals_timestamps.append(time.time())
|
|
|
|
#Let's do some type checking first
|
|
if self.tp_order is None:
|
|
self.status_dict["pause_reason"] = time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.pair} | TP order is None")
|
|
self.broker.logger.log_this("Error. Take profit order is None, pair will be restarted",0,self.pair)
|
|
self.write_status_file(is_backup=True)
|
|
self.restart = True
|
|
return 1
|
|
if self.so is None:
|
|
self.status_dict["pause_reason"] = time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.pair} | Safety order is None")
|
|
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
|
|
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)
|
|
self.total_amount_of_base = self.total_amount_of_base + old_so_order["filled"] - self.parse_fees(old_so_order)[0]
|
|
self.total_amount_of_quote = self.total_amount_of_quote + old_so_order["cost"]
|
|
|
|
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)
|
|
|
|
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 (Only reloading the config file is needed.)
|
|
self.status_dict["pause_reason"] = "take_profit_routine - var cleanup"
|
|
self.config_dict = self.reload_config_dict()
|
|
|
|
if self.check_slippage:
|
|
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 trader",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 trader",1,self.pair)
|
|
time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier())
|
|
self.pause = False
|
|
self.restart = True
|
|
return 1
|
|
|
|
#Possible restart errors
|
|
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"}
|
|
|
|
#Restarting the trader
|
|
self.status_dict["pause_reason"] = "take_profit_routine - restart_trader call"
|
|
restart_trader = self.start_trader()
|
|
self.status_dict["pause_reason"] = "take_profit_routine - restart_trader call - start_trader() called"
|
|
#retries = self.broker.get_retries()
|
|
if restart_trader in restart_errors.keys():
|
|
self.pause = False
|
|
self.restart = True
|
|
self.write_status_file(is_backup=True)
|
|
self.broker.logger.log_this(restart_errors[restart_trader],1,self.pair)
|
|
return restart_trader
|
|
|
|
|
|
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_before_new_safety_order())
|
|
|
|
# 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(is_backup=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(is_backup=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(is_backup=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(is_backup=True)
|
|
self.restart = True
|
|
return 0
|
|
a = self.new_so_routine(so_status,self.safety_order_index<self.config_dict["no_of_safety_orders"])
|
|
#0 OK, 1 not enough funds, 2 can't cancel old TP, 3 can't send new TP
|
|
if a==1:
|
|
self.broker.logger.log_this(f"Can't send new safety order. Not enough funds? new_so_routine returned {a}",1,self.pair)
|
|
#If there are not enough funds do not even try to send more safety orders
|
|
#This way of doing it seems more practical than setting up yet another flag
|
|
self.config_dict["no_of_safety_orders"] = self.safety_order_index
|
|
return 1
|
|
elif a==2:
|
|
self.broker.logger.log_this(f"Can't cancel old take profit order. new_so_routine returned {a}",1,self.pair)
|
|
self.pause = False
|
|
self.status_dict["pause_reason"] = ""
|
|
if self.config_dict["attempt_restart"]:
|
|
self.write_status_file(is_backup=True)
|
|
self.restart = True
|
|
return 1
|
|
elif a==3:
|
|
#self.pause = False
|
|
self.broker.logger.log_this(f"Error in trader: Can't send new take profit order. Restart will be attempted. new_so_routine returned {a}",0,self.pair)
|
|
if self.config_dict["attempt_restart"]:
|
|
self.write_status_file(is_backup=True)
|
|
self.restart = True
|
|
return 1
|
|
|
|
#Check if short price exceeds old long price. If so, send a Telegram message
|
|
if self.is_short and "old_long" in self.status_dict and self.config_dict["check_old_long_price"]:
|
|
self.check_old_long_price()
|
|
|
|
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 "autoswitch" in self.config_dict and self.config_dict["autoswitch"] and "old_long" in self.status_dict:
|
|
#If selling the base currency left at the current market price plus the quote already received turns out to be more than the amount of the old_long,
|
|
# it means that we already are in profit territory, switch back to long.
|
|
old_target = self.status_dict["old_long"]["tp_price"]*self.status_dict["old_long"]["tp_amount"]
|
|
base_left = self.status_dict["old_long"]["tp_amount"]-self.status_dict["base_bought"]
|
|
if (base_left*self.status_dict["price"])+self.status_dict["quote_spent"]>=old_target:
|
|
#Sell all base (market), report the profits and restart the trader
|
|
self.status_dict["pause_reason"] = "automatic_switch"
|
|
self.switch_to_long(already_received_quote=self.status_dict["quote_spent"])
|
|
self.restart = True
|
|
return 1
|
|
|
|
#Render status line(s)
|
|
self.status_dict["status_string"] = self.generate_status_strings()
|
|
|
|
#Wrap up
|
|
self.status_dict["deal_uptime"]=int(time.time()) - self.deal_start_time
|
|
self.status_dict["total_uptime"]=int(time.time()) - self.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.boosted_deals_range and time.time()-self.boosted_time_range<=self.deals_timestamps[-self.boosted_deals_range]
|
|
|
|
|
|
def get_tp_level(self, order_index: int = 0) -> 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
|
|
boost_percentage = 0
|
|
|
|
#BOOST ROUTINE: If the trader closed certain amount of deals within the last t seconds, raise the take profit level by x%
|
|
self.is_boosted = False
|
|
if not self.is_short and self.check_boosted():
|
|
self.is_boosted = True
|
|
boost_percentage = self.boosted_amount
|
|
|
|
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<len(profit_table): #If more safety orders were added, instead of recalculating the whole table
|
|
tp_level = profit_table[order_index] #it just returns the last value. Otherwise, the percentage gets very small.
|
|
return tp_level+boost_percentage
|
|
|
|
|
|
def seconds_to_time(self, total_seconds: float) -> 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) -> 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)-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)
|
|
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 value<lower_limit:
|
|
return lower_limit
|
|
|
|
if value>upper_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<max_value:
|
|
color = green if price>=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_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"{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.is_boosted:
|
|
line1 = f"{line1} | BOOSTED"
|
|
if self.config_dict["autoswitch"]:
|
|
line1 = f"{line1} | AUTO"
|
|
if self.is_short and "old_long" in self.status_dict:
|
|
try:
|
|
#When adding a trader, this line always throws an exception since status_dict["price"] is not yet populated
|
|
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 "is_boosted" in self.status_dict:
|
|
self.is_boosted = self.status_dict["is_boosted"]
|
|
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<self.config_dict["no_of_safety_orders"]:
|
|
#The second condition is important: it signals that the empty order returned was because of an error, not because the trader ran out of funds in the past.
|
|
#When the trader runs out of funds, safety_order_index=config_dict["no_of_safety_orders"]
|
|
self.broker.logger.log_this("Couldn't load safety order. Aborting.",2,self.pair)
|
|
self.quit = True
|
|
return 1
|
|
|
|
#Done
|
|
self.pause = False
|
|
self.status_dict["pause_reason"] = ""
|
|
self.update_status(True)
|
|
return 0
|
|
|
|
|
|
'''
|
|
class imported_trader(trader):
|
|
def __init__(self,broker,config_file,status_file):
|
|
self.broker = broker
|
|
self.config_dict = config_file
|
|
self.status_dict = status_file
|
|
self.restart = False
|
|
self.last_time_seen = time.time()
|
|
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
|
|
if self.load_values()==1:
|
|
self.broker.logger.log_this("Error loading values",2,self.pair)
|
|
self.quit = True
|
|
self.warnings = {
|
|
"short_price_exceeds_old_long": False,
|
|
"speol_notified": False
|
|
}
|
|
self.update_status(True)
|
|
|
|
def load_values(self):
|
|
self.is_short = self.config_dict["is_short"]
|
|
self.base,self.quote = self.pair.split("/")
|
|
self.tp_order = self.broker.get_order(self.status_dict["tp_order_id"],self.pair)
|
|
if self.status_dict["so_order_id"]=="":
|
|
self.so = self.broker.get_empty_order()
|
|
else:
|
|
self.so = self.broker.get_order(self.status_dict["so_order_id"],self.pair)
|
|
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:
|
|
self.status_dict["deal_order_history"] = []
|
|
|
|
#I need to revise this two conditionals
|
|
if self.so==self.broker.get_empty_order() and self.safety_order_index<self.config_dict["no_of_safety_orders"]:
|
|
self.broker.logger.log_this("Couldn't load safety order. Aborting.",2,self.pair)
|
|
self.quit = True
|
|
return 1
|
|
if self.tp_order==self.broker.get_empty_order():
|
|
self.broker.logger.log_this("Couldn't load take profit order. Aborting.",2,self.pair)
|
|
self.quit = True
|
|
return 1
|
|
|
|
self.profit_filename = f"profits/{self.base}{self.quote}.profits"
|
|
self.log_filename = f"logs/{self.base}{self.quote}.log"
|
|
if self.is_short and "old_long" not in self.status_dict:
|
|
#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_dict["old_long"] = json.load(ol)
|
|
except Exception as e:
|
|
self.broker.logger.log_this(f"Exception in load_values while trying to load the old_long file: {e}",1,self.pair)
|
|
|
|
self.quit = False
|
|
self.pause = False
|
|
self.status_dict["pause_reason"] = ""
|
|
return 0
|
|
''' |