Compare commits

...

23 Commits

Author SHA1 Message Date
Nicolás Sánchez 4a1f1c844d 2025.12.01 2025-12-01 10:17:23 -03:00
Nicolás Sánchez 536866364c 2025.11.11 2025-11-18 20:46:49 -03:00
Nicolás Sánchez 7c33dd231d 2025.11.08 2025-11-08 10:42:29 -03:00
Nicolás Sánchez 96d1cf6d78 2025.10.24 2025-10-24 20:17:46 -03:00
Nicolás Sánchez b69b0d2f15 2025.10.12 2025-10-12 19:01:13 -03:00
Nicolás Sánchez 18506dbaf3 2025.10.11 2025-10-11 22:48:26 -03:00
Nicolás Sánchez ca43b3dad5 2025.10.10 2025-10-11 09:58:58 -03:00
Nicolás Sánchez d06bfd9d10 2025.10.09 2025-10-09 17:51:55 -03:00
Nicolás Sánchez e354ea4d55 2025.10.07 2025-10-07 17:36:39 -03:00
Nicolás Sánchez 8f3b0eb186 2025.10.04 2025-10-04 21:40:03 -03:00
Nicolás Sánchez 2823dff56a 2025.10.03 2025-10-03 15:44:59 -03:00
Nicolás Sánchez c42a505e49 2025.10.01 2025-10-01 15:07:17 -03:00
Nicolás Sánchez 0576f93477 2025.09.27 2025-09-27 20:12:29 -03:00
Nicolás Sánchez 65c406a03d 205.09.25 2025-09-25 18:07:58 -03:00
Nicolás Sánchez 9e2a1dc7a1 2025.09.24 2025-09-24 16:04:43 -03:00
Nicolás Sánchez 171738fa4d 2025.09.21 2025-09-21 18:23:14 -03:00
Nicolás Sánchez 09f9aa313c 2025.09.20 2025-09-20 19:18:32 -03:00
Nicolás Sánchez 451e1a63aa 2025.09.19 2025-09-19 17:47:15 -03:00
Nicolás Sánchez 733f6efbff 2025.09.18 2025-09-19 10:29:32 -03:00
Nicolás Sánchez 0d753aa3cd 2025.09.15 2025-09-15 18:55:59 -03:00
Nicolás Sánchez 73eff21dbb 2025.09.12 2025-09-13 08:16:42 -03:00
Nicolás Sánchez 37661d91eb version number 2025-09-12 09:51:12 -03:00
Nicolás Sánchez 885797db01 2025.09.11 2025-09-11 20:36:40 -03:00
8 changed files with 463 additions and 306 deletions

View File

@ -1,3 +1,95 @@
2025.12.01:
. Modified log output of new_market_order.
. Modified Kucoin's case in min_amount_of_base.
2025.11.11:
. deals_cache and log_list cache are now 20 items long.
. Less log spam.
2025.11.08:
. broker.set_default_order_size() now saves the config file to disk after changing the value.
. Variable renaming and other small stuff.
2025.10.24:
. Toggling liquidate_after_switch now writes the config file to disk so the setting persists between trades.
. Manually switching to long now sets double_check_price to false.
. Added a few comments to switch_to_long.
2025.10.12:
. do_cleanup relocated after generating the safety orders' prices.
2025.10.11:
. Minor simplification in do_cleanup.
. Removed a couple of (no longer needed?) pauses.
2025.10.10:
. New endpoint: /refresh_log_cache.
. Fixed an error in /add_so endpoint that incremented the config setting but not the status setting.
2025.10.09:
. Cleanup is done as soon as the trader starts, rather than after sending the take profit and safety orders.
2025.10.07:
. In short traders, if there are too few safety orders (less than 67% of the max amount), safety_order_deviance is increased from 2% to 3%.
2025.10.04:
. Fixed error while logging orders in new_simulated_market_order.
. renew_tp_and_so_routine now send the take profit order first, and then the safety orders.
2025.10.03:
. New broker config option: log_orders. If set to True, the orders will be logged in orders.log under logs directory.
. New API endpoint: /toggle_log_orders.
2025.10.01:
. Fixed base fees not being taken into account.
2025.09.27:
. Added notes in every entry of deal_order_history.
. Minor refactor in renew_tp_and_so_routine.
. Added another cooldown before sending a take profit order (To give the exchange a bit more time to reflect correctly the amount of base present in the account)
. Updated cleanup routine to leave some change in the account.
2025.09.25:
. Added a pause after getting filled orders in check_status.
. Added an extra logging line in take_profit_routine.
2025.09.24:
. Added a new config option: wait_after_initial_market_order. If specifies in seconds the amount of wait time after sending the initial market order.
It should help the exchanges to report correctly the recently filled market order.
. Removed the "PAUSED" notice in the screen output that was unused.
2025.09.21:
. Fixed a bug that caused short traders to have an incorrect order size.
2025.09.20:
. Fixed bug that caused short traders to initialize using the same workflow as a long one.
2025.09.19:
. Added pageSize parameter to the open order requests when querying Kucoin.
2025.09.18:
. do_cleanup now uses get_min_quote_size.
. Added an extra price check to switch_to_long.
. Removed old check_old_long_price method.
2025.09.14:
. Refactored full order list fetching.
. Minor refactor of restart_pair_no_json.
. Pausing the trader is now done via set_pause() method.
. Reverted modification of wait time after initial market order.
. wait_time now present in broker config file.
. Minor refactorings.
2025.09.13:
. Increased wait time after initial market order.
2025.09.12:
. No retries when sending a cleanup order.
. Removed redundant try...except blocks in switch_to_long.
2025.09.11:
. Fixed bug in start_trader that called amount_to_precision with very low amounts and spammed logs.
2025.09.10:
. Deal order history now stores only the id of each order instead of the full order object.

View File

@ -252,6 +252,7 @@ class ConfigHandler:
# self.broker.logger.log_this(f"liquidate_after_switch must be a boolean",1,self.get_pair())
# return 1
self.config_dictionary["liquidate_after_switch"] = liquidate_after_switch
self.save_to_file()
return 0
def set_tp_mode(self, tp_mode: int):

View File

@ -14,17 +14,19 @@ class Broker:
self.broker_config = broker_config
self.exchange = exchange
self.last_price = 0
self.wait_time = .5 #Default wait time for API breathing room
self.empty_order = {"id": "", "status": "", "filled": 0, "remaining": 0, "price": 0, "cost": 0, "fees": [], "symbol": ""}
#Default values
self.cooldown_multiplier = self.broker_config["cooldown_multiplier"] if "cooldown_multiplier" in self.broker_config else 2
self.wait_before_new_safety_order = self.broker_config["wait_before_new_safety_order"] if "wait_before_new_safety_order" in self.broker_config else 1
self.retries = self.broker_config["retries"] if "retries" in self.broker_config else 5
self.slippage_default_threshold = self.broker_config["slippage_default_threshold"] if "slippage_default_threshold" in self.broker_config else .03
self.follow_order_history = self.broker_config["follow_order_history"] if "follow_order_history" in self.broker_config else False
self.write_order_history = self.broker_config["write_order_history"] if "write_order_history" in self.broker_config else False
self.wait_time = self.broker_config.get("wait_time",.5)
self.cooldown_multiplier = self.broker_config.get("cooldown_multiplier",2)
self.wait_after_initial_market_order = self.broker_config.get("wait_after_initial_market_order",1)
self.wait_before_new_safety_order = self.broker_config.get("wait_before_new_safety_order",1)
self.retries = self.broker_config.get("retries",5)
self.slippage_default_threshold = self.broker_config.get("slippage_default_threshold",.03)
self.follow_order_history = self.broker_config.get("follow_order_history",False)
self.write_order_history = self.broker_config.get("write_order_history", False)
self.logger = Logger(self.broker_config)
self.log_orders = self.broker_config.get("log_orders",False)
#Initialize database
self.profits_database_filename = "profits/profits_database.db"
@ -49,7 +51,7 @@ class Broker:
self.markets = self.exchange.load_markets()
#Populates deals cache
self.deals_cache_length = 10
self.deals_cache_length = 20
self.deals_list = self.preload_deals(amount_to_preload=self.deals_cache_length)
@ -80,6 +82,16 @@ class Broker:
def get_deals_cache(self):
return self.deals_list
def get_log_orders(self):
return self.log_orders
def set_log_orders(self,log_orders:bool):
self.log_orders = log_orders
return 0
def get_symbol(self,pair):
if "/" in pair:
return pair
@ -172,7 +184,7 @@ class Broker:
query = f"SELECT * FROM profits_table WHERE pair = ? ORDER BY timestamp DESC LIMIT 1;"
with self._cur() as cur:
cur.execute(query, (order['symbol'],))
cur.execute(query, (order["symbol"],))
result = cur.fetchone()
if result is None:
return False
@ -192,6 +204,13 @@ class Broker:
self.cooldown_multiplier = value
return 0
def get_wait_after_initial_market_order(self):
return self.wait_after_initial_market_order
def set_wait_after_initial_market_order(self, value:float):
self.wait_after_initial_market_order = value
return 0
def get_wait_before_new_safety_order(self):
return self.wait_before_new_safety_order
@ -205,6 +224,7 @@ class Broker:
def set_default_order_size(self,size):
try:
self.broker_config["default_order_size"] = float(size)
self.rewrite_config_file()
except Exception as e:
self.logger.log_this(f"Exception in set_default_order_size: {e}",1)
return 1
@ -458,7 +478,7 @@ class Broker:
try:
return orderbook["bids"][0][0]
except Exception as e:
self.logger.log_this(f"Exception getting top mid price: {e}",1,symbol)
self.logger.log_this(f"Exception getting top bid price: {e}",1,symbol)
return self.get_ticker_price(symbol)
@ -503,28 +523,6 @@ class Broker:
return []
def fetch_full_orders(self,pairs=None) -> list:
'''
Returns a list of all orders on the exchange
:param pairs: list of pairs to get orders for
:return: list of orders
'''
if pairs is None:
pairs = []
try:
orders = []
if self.get_exchange_name()=="binance":
orders = self.get_opened_orders_binance(pairs)
else:
orders = self.get_opened_orders()
return [] if orders is None else orders
except Exception as e:
self.logger.log_this(f"Exception in fetch_full_orders: {e}",2)
return []
def fetch_open_orders(self,pairs=None) -> list:
'''
Returns a list of all open orders on the exchange
@ -536,9 +534,18 @@ class Broker:
if pairs is None:
pairs = []
try:
if self.get_exchange_name()in ["binance","kucoin"]:
return self.get_opened_orders_binance(pairs)
return self.get_opened_orders()
if self.get_exchange_name()=="binance":
if self.broker_config.get("unified_order_query"):
return self.exchange.fetch_open_orders()
result = []
for pair in pairs:
a = self.exchange.fetch_open_orders(pair)
result.extend(iter(a))
return result
elif self.get_exchange_name()=="kucoin":
return self.exchange.fetch_open_orders(params={"pageSize": "500"})
else:
return self.exchange.fetch_open_orders()
except Exception as e:
self.logger.log_this(f"Exception in fetch_open_orders: {e}",2)
return []
@ -559,31 +566,10 @@ class Broker:
return self.get_closed_orders_binance(pairs)
return self.get_closed_orders()
except Exception as e:
self.logger.log_this(f"Exception in fetch_open_orders: {e}",2)
self.logger.log_this(f"Exception in fetch_closed_orders: {e}",2)
return []
def get_opened_orders(self,no_retries=False): #It should return a list of all opened orders
'''
Returns a list of all the open orders on the exchange
:param pairs: list of pairs
:return: list of all the open orders on the exchange
'''
retries = self.retries
while retries>0:
try:
return self.exchange.fetch_open_orders()
except Exception as e:
self.logger.log_this(f"Exception in get_opened_orders: {e}",1)
if no_retries:
break
time.sleep(self.wait_time)
retries-=1
return []
def get_closed_orders(self,pair=None,no_retries=False): #It should return a list of all opened orders
'''
Returns a list of all the open orders on the exchange
@ -605,27 +591,6 @@ class Broker:
return []
def get_opened_orders_binance(self,pairs):
'''
Returns a list of all the open orders on the exchange
:param pairs: list of pairs
:return: list of all the open orders on the exchange
'''
try:
if "unified_order_query" in self.broker_config and self.broker_config["unified_order_query"] is True:
return self.exchange.fetch_open_orders()
result = []
for pair in pairs:
a = self.exchange.fetch_open_orders(pair)
result.extend(iter(a))
return result
except Exception as e:
self.logger.log_this(f"Exception in get_opened_orders_binance: {e}",1)
return []
def get_closed_orders_binance(self,pairs):
'''
Returns a list of all the closed orders on the exchange
@ -635,7 +600,7 @@ class Broker:
'''
try:
if "unified_order_query" in self.broker_config and self.broker_config["unified_order_query"] is True:
if self.broker_config.get("unified_order_query"):
return self.exchange.fetch_closed_orders()
result = []
for pair in pairs:
@ -699,7 +664,7 @@ class Broker:
return amount
def new_simulated_market_order(self,symbol,size,side,amount_in_base=False,no_retries=False):
def new_simulated_market_order(self,symbol,size,side,amount_in_base=False,no_retries=False,log=""):
'''
TODO: Emulating Market Orders With Limit Orders
@ -729,7 +694,9 @@ class Broker:
while retries>0:
try:
if self.get_exchange_name()=="gateio" and side=="buy" and not amount_in_base:
new_order = self.exchange.create_market_buy_order_with_cost(symbol, size)
new_order = self.exchange.create_market_buy_order_with_cost(symbol, size)
if self.log_orders:
self.logger.log_order(f"New simulated market order: Symbol: {symbol} - Side: {side} - Size: {size} - ID: {new_order['id']} - Origin: {log}")
else:
order_book = self.get_order_book(symbol)
if order_book=={}:
@ -743,6 +710,8 @@ class Broker:
price = self.find_minimum_viable_price(order_book,base_amount,side)
#Maybe check for slippage here instead of within the trader itself? idk
new_order = self.exchange.create_order(symbol,"limit",side,base_amount,price)
if self.log_orders:
self.logger.log_order(f"New simulated market order: Symbol: {symbol} - Side: {side} - Size: {size} - Price: {price} - ID: {new_order['id']} - Origin: {log}")
time.sleep(self.wait_time)
return self.get_order(new_order["id"],symbol)
except Exception as e:
@ -789,7 +758,7 @@ class Broker:
return None
def new_market_order(self,symbol,size,side,amount_in_base=False,no_retries=False): #It should send a new market order to the exchange
def new_market_order(self,symbol,size,side,amount_in_base=False,no_retries=False, log=""): #It should send a new market order to the exchange
'''
Sends a new market order to the exchange.
@ -814,12 +783,13 @@ class Broker:
amount = self.amount_to_precision(symbol,size) #Market sell orders are always nominated in base currency
order_to_send = self.exchange.create_order(symbol,"market",side,amount)
if self.log_orders:
self.logger.log_order(f"New market order: Symbol: {symbol} - Side: {side} - Size: {size} - ID: {order_to_send['id']} - Origin: {log}")
time.sleep(self.wait_time)
# Wait a bit more when dealing with Kucoin
return self.get_order(order_to_send["id"],symbol)
except Exception as e:
self.logger.log_this(f"Exception in new_market_order: {e}",1,symbol)
self.logger.log_this(f"Exception in new_market_order: {e} - Side: {side} - Size: {size}",1,symbol)
if no_retries:
break
time.sleep(self.wait_time)
@ -901,7 +871,7 @@ class Broker:
# return returned_orders
def new_limit_order(self,symbol,size,side,price,no_retries=False):
def new_limit_order(self,symbol,size,side,price,no_retries=False,log=""):
'''
Sends a new limit order.
@ -917,6 +887,8 @@ class Broker:
try:
order_to_send = self.exchange.create_order(symbol,"limit",side,self.amount_to_precision(symbol,size),price)
time.sleep(self.wait_time)
if self.log_orders:
self.logger.log_order(f"New limit order: Symbol: {symbol} - Side: {side} - Size: {size} - Price: {price} - ID: {order_to_send['id']} - Notes: {log}")
return self.get_order(order_to_send["id"],symbol)
except Exception as e:
self.logger.log_this(f"Exception in new_limit_order - Side: {side} - Size: {size} - {self.amount_to_precision(symbol,size)} - Exception: {e}",1,symbol)
@ -1016,9 +988,9 @@ class Broker:
if self.get_exchange_name() in ["okex","bybit"]:
return float(market["limits"]["amount"]["min"])
elif self.get_exchange_name() in ["kucoin"]:
return (float(market["limits"]["cost"]["min"])+.1)/self.get_ticker_price(pair)
return max(float(market["limits"]["amount"]["min"]),(float(market["limits"]["cost"]["min"])+.25)/self.get_ticker_price(pair))
elif self.get_exchange_name() in ["gateio"]:
return (float(market["limits"]["cost"]["min"])+.25)/self.get_ticker_price(pair)
return (float(market["limits"]["cost"]["min"])+.1)/self.get_ticker_price(pair)
elif self.get_exchange_name()=="binance":
for line in market["info"]["filters"]:
if line["filterType"] == "NOTIONAL":
@ -1080,7 +1052,7 @@ class Logger:
self.broker_config = broker_config
self.exchange_name = self.broker_config["exchange"]
self.tg_credentials = credentials.get_credentials("telegram")
self.log_list_max_length = 10
self.log_list_max_length = 20 # log cache
self.log_list = collections.deque(maxlen=self.log_list_max_length)
self.preload_logs()
@ -1096,6 +1068,16 @@ class Logger:
return 1
def refresh_logs(self):
try:
self.log_list.clear()
self.preload_logs()
return 0
except Exception as e:
print(e)
return 1
def set_log_list_max_length(self, amount):
self.log_list_max_length = amount
return self.log_list_max_length
@ -1132,6 +1114,9 @@ class Logger:
self.log_this(f"Error in send_tg_message: {e}",1)
return 1
def log_order(self,message):
with open(f"logs/orders.log","a") as log_file:
log_file.write(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {message}\n"))
def log_this(self,message,level=2,pair=None):
'''
@ -1153,8 +1138,6 @@ class Logger:
#Write to log file
with open(f"logs/{self.exchange_name}.log","a") as log_file:
log_file.write(text+"\n")
log_file.close()
#Append to log list
self.log_list.append(text)
except Exception as e:

117
main.py
View File

@ -18,7 +18,7 @@ import exchange_wrapper
import trader
version = "2025.09.10"
version = "2025.12.01"
'''
Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
@ -41,7 +41,7 @@ executor = None
#Shutdown handler
def shutdown_handler(signum, _):
broker.logger.log_this(f"Received signal {signum}, shutting down as gracefully as possible...", 2)
broker.logger.log_this(f"Received signal {signum}, shutting down.", 2)
if executor:
executor.shutdown(wait=True, timeout=5)
os_exit(0)
@ -246,20 +246,26 @@ def restart_pair_no_json(base: str, quote: str) -> int:
try:
symbol = f"{base}/{quote}"
order_list = broker.fetch_full_orders(tickers)
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.pause = True
instance.set_pause(True, "Restarting trader")
#Backing up old status file
instance.status.save_to_file(is_backup=True)
#Here, we could open a duster (if needed)
for order in order_list:
if order["symbol"]==symbol and instance.config.get_is_short() and order["side"]=="sell":
broker.logger.log_this(f"Cancelling old sell orders",2,symbol)
broker.cancel_order(order["id"],order["symbol"])
elif order["symbol"]==symbol and not instance.config.get_is_short() and order["side"]=="buy":
broker.logger.log_this(f"Cancelling old buy orders",2,symbol)
broker.cancel_order(order["id"],order["symbol"])
broker.logger.log_this(f"Cancelling old take profit order",2,symbol)
try:
old_tp_order = instance.status.get_take_profit_order()
broker.cancel_order(old_tp_order["id"],old_tp_order["symbol"])
except Exception as e:
broker.logger.log_this(f"Error canceling old take profit order: {e}",2,symbol)
broker.logger.log_this(f"Cancelling old take safety orders",2,symbol)
for item in instance.status.get_safety_orders():
try:
broker.cancel_order(item["id"],item["symbol"])
except Exception as e:
broker.logger.log_this(f"Error canceling old safety order: {e}",2,symbol)
try:
running_traders.remove(instance)
except ValueError:
@ -1232,6 +1238,20 @@ def switch_quote_currency():
return jsonify({'Error': 'Halp'})
@base_api.route("/toggle_log_orders", methods=['POST'])
def toggle_log_orders():
'''
POST request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_toggle_log_orders()
@base_api.route("/toggle_restart", methods=['POST'])
def toggle_restart():
'''
@ -1288,6 +1308,21 @@ def get_log_list():
return unwrapped_get_log_list()
@base_api.route("/refresh_log_cache", methods=['POST'])
def refresh_log_cache():
'''
POST request
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
return unwrapped_refresh_log_cache()
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/get_balance", methods=['GET'])
def get_balance():
'''
@ -1606,8 +1641,8 @@ def unwrapped_switch_to_long(base,quote,calculate_profits):
return jsonify({"Error": "Pair not running"})
for instance in running_traders:
if f"{base}/{quote}"==instance.status.get_pair():
instance.pause = True
if instance.switch_to_long(ignore_old_long=ignore_old_long)==1:
instance.set_pause(True, "Switching to long mode")
if instance.switch_to_long(ignore_old_long=ignore_old_long,double_check_price=False)==1:
return jsonify({"Error": "Error in switch_to_long()"})
if instance.start_trader()==1:
instance.quit = True
@ -1774,14 +1809,13 @@ def unwrapped_add_safety_orders(base,quote,amount):
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.pause = True
#x.no_of_safety_orders += int(amount)
instance.config.set_no_of_safety_orders(instance.config.get_no_of_safety_orders()+int(amount))
instance.set_pause(True, "Adding safety orders")
instance.status.set_no_of_safety_orders(instance.status.get_no_of_safety_orders()+int(amount))
broker.logger.log_this("Recalculating safety price table...",1,symbol)
instance.status.set_safety_price_table(instance.calculate_safety_prices(instance.status.get_start_price(),instance.config.get_no_of_safety_orders(),instance.config.get_safety_order_deviance()))
broker.logger.log_this(f"Done. Added {amount} safety orders",1,symbol)
instance.update_status(True)
instance.pause = False
instance.set_pause(False)
return jsonify({"Success": f"Done. Added {amount} safety orders"})
return jsonify({"Error": "Pair not found"})
except Exception as e:
@ -2097,24 +2131,24 @@ def unwrapped_add_quote(base,quote,amount):
if f"{base}/{quote}"==instance.status.get_pair():
if instance.config.get_is_short():
return jsonify({"Error": "Quote can't be added to short traders"})
instance.pause = True
instance.set_pause(True, "Adding quote")
new_average_price = (instance.status.get_quote_spent()+float(amount))/(instance.status.get_base_bought()+(float(amount)/instance.status.get_price()))
broker.logger.log_this(f"Your new average buy price will be {new_average_price} {quote}",2,instance.status.get_pair())
broker.logger.log_this(f"Your new take profit price price will be {new_average_price*instance.get_tp_level()} {quote}",2,instance.status.get_pair())
new_order = broker.new_market_order(instance.status.get_pair(),float(amount),"buy")
if new_order is None:
broker.logger.log_this("Error: Market order returned None",2,instance.status.get_pair())
instance.pause = False
instance.set_pause(False)
return jsonify({"Error": "Market order returned None"})
while True:
time.sleep(broker.get_wait_time())
returned_order = broker.get_order(new_order["id"],instance.status.get_pair())
if returned_order==broker.empty_order:
broker.logger.log_this("Problems sending the order",2,instance.status.get_pair())
instance.pause = False
instance.set_pause(False)
return jsonify({"Error": "Problems sending the order"})
elif returned_order["status"]=="expired":
instance.pause = False
instance.set_pause(False)
return jsonify({"Error": "New order expired"})
elif returned_order["status"]=="closed":
broker.logger.log_this("Order sent",2,instance.status.get_pair())
@ -2131,7 +2165,7 @@ def unwrapped_add_quote(base,quote,amount):
attempts-=1
if attempts==0:
broker.logger.log_this("Can't cancel old take profit order, cancelling...",2,instance.status.get_pair())
instance.pause = False
instance.set_pause(False)
return jsonify({"Error": "Can't cancel old take profit order."})
instance.status.set_take_profit_price(instance.status.get_quote_spent()/instance.status.get_base_bought()*instance.get_tp_level())
instance.status.set_take_profit_order(broker.new_limit_order(instance.status.get_pair(),instance.status.get_base_bought(),"sell",instance.status.get_take_profit_price()))
@ -2141,7 +2175,7 @@ def unwrapped_add_quote(base,quote,amount):
broker.logger.log_this("Waiting for initial order to get filled",2,instance.status.get_pair())
broker.logger.log_this(f"{returned_order}",2,instance.status.get_pair())
time.sleep(broker.get_wait_time())
instance.pause = False
instance.set_pause(False)
broker.logger.log_this("Done",2,instance.status.get_pair())
return jsonify({"Success": "Quote added successfully"})
return jsonify({"Error": "Something horrible happened :S"})
@ -2246,6 +2280,7 @@ def unwrapped_toggle_autoswitch(base,quote):
broker.logger.log_this(f"Exception while toggling autoswitch: {e}",1,symbol)
return jsonify({"Error": "Halp"})
def unwrapped_toggle_liquidate_after_switch(base,quote):
'''
Signals a trader to enable or disable quitting after switching from short to long.
@ -2324,14 +2359,14 @@ def unwrapped_switch_quote_currency(base,quote,new_quote):
for trader in running_traders:
if symbol==trader.status.get_pair():
#Pause the trader
trader.pause = True
trader.set_pause(True, "Switching quote currency")
#Call x.switch_quote_currency
if trader.switch_quote_currency(new_quote)==1:
return jsonify({"Error": "Swap failed. Check log files for details."})
#Resume the trader
trader.pause = False
trader.set_pause(False)
return jsonify({"Success": "Mission successful"})
return jsonify({"Error": "Trader not found"})
except Exception as e:
@ -2354,6 +2389,20 @@ def unwrapped_toggle_restart():
return jsonify({"Success": "attempt_to_restart disabled"})
def unwrapped_toggle_log_orders():
'''
Toggles on or off the logging of orders.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
broker.set_log_orders(not broker.get_log_orders())
if broker.get_log_orders():
return jsonify({"Success": "log_orders enabled"})
return jsonify({"Success": "log_orders disabled"})
def unwrapped_toggle_telegram():
'''
Switches on or off the Telegram notifications
@ -2425,6 +2474,22 @@ def unwrapped_get_log_list():
return jsonify({"Logs": broker.logger.get_log_list()})
def unwrapped_refresh_log_cache():
'''
Reloads the log file cache.
Parameters:
None
Returns:
jsonify: A jsonified dictionary containing the last n entries from the log file.
'''
if broker.logger.refresh_logs()==0:
return jsonify({"Success": "Logs refreshed"})
else:
return jsonify({"Error": "Error while refreshing logs"})
def unwrapped_get_deals_cache():
'''
Retrieves the last n entries from the broker's logger.

View File

@ -418,11 +418,11 @@ class StatusHandler:
self.status_dictionary["deal_order_history"] = []
return 0
def update_deal_order_history(self, new_deal: dict):
def update_deal_order_history(self, new_deal: dict, note: str = ""):
# if not isinstance(new_deal, dict):
# self.broker.logger.log_this(f"value provided is not a dict",1,self.get_pair())
id = new_deal["id"] if "id" in new_deal else None
self.status_dictionary["deal_order_history"].append(id)
self.status_dictionary["deal_order_history"].append(f"{note} - {id}")
return 0
def save_to_file(self, file_path = None, is_backup = False):

370
trader.py
View File

@ -11,14 +11,16 @@ class trader:
self.pause = True
self.quit = False
self.restart = False
self.warnings = {
"short_price_exceeds_old_long": False,
"speol_notified": False
}
self.trader_restart_errors = {1: "start_trader returned error #1. Trader will be restarted",
2: "start_trader returned error #2: Initial order never got filled. Trader will be restarted",
3: "start_trader returned error #3: Slippage threshold exceeded. Trader will be restarted"}
#Status string caches
self.low_price_cache = None
self.mid_price_cache = None
self.high_price_cache = None
self.concurrent_so_amount_cache = None
self.broker = broker
self.config = ConfigHandler(pair,broker)
base_quote = self.config.get_pair()
@ -30,17 +32,18 @@ class trader:
self.status.set_start_time(int(time.time()))
self.last_time_seen = time.time()
#Status string caches
self.low_price_cache = None
self.mid_price_cache = None
self.high_price_cache = None
self.concurrent_so_amount_cache = None
self.base_diff_check = False
self.min_base_difference = .1 #Percentage difference between base in the tp order and its adjusted amount that triggers a recheck.
#The exchanges sometimes take a few seconds to update the balance after an order is closed.
self.base_check_interval = 60 #In seconds
self.base_check_time = 0
self.base_amount_missing = 0
if self.config.get_is_short():
#Check if there is an old_long file. If so, load it.
try:
with open(f"status/{self.base}{self.quote}.oldlong") as ol:
self.status.set_old_long(load(ol))
with open(f"status/{self.base}{self.quote}.oldlong") as old_long_file_handler:
self.status.set_old_long(load(old_long_file_handler))
except Exception as e:
self.broker.logger.log_this(f"Exception: No old_long file. {e}",1,base_quote)
@ -67,12 +70,12 @@ class trader:
self.quit = True
elif start_result==2: #Retries exceeded
if self.config.get_force_restart_if_retries_exhausted():
self.pause = False
self.set_pause(False)
self.restart = True
else:
self.quit = True
elif start_result==3: #Not enough liquidity
self.pause = False
self.set_pause(False)
self.restart = True
@ -127,10 +130,9 @@ class trader:
if new_market_data is not None:
self.market = new_market_data
self.pause = True
self.status.set_pause_reason("start_trader")
self.set_pause(True, "start_trader")
if self.status.get_is_short():
if self.config.get_is_short():
self.broker.logger.log_this("Calculating optimal order size...",2,self.status.get_pair())
#Get minimum order size from exchange
@ -147,14 +149,15 @@ class trader:
self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.status.get_pair())
return 1
#Buy missing base sold because of rounding errors (rare)
#Buy missing base sold because of errors (unaccounted filled orders, crashes, etc)
if self.status.get_old_long()!={}:
diff = self.broker.amount_to_precision(self.status.get_pair(), self.status.get_old_long()["tp_amount"] - free_base)
if diff>min_base_size:
self.broker.logger.log_this(f"Buying missing {diff} {self.base}",1,self.status.get_pair())
self.broker.new_market_order(self.status.get_pair(),diff,"buy",amount_in_base=True)
#diff = self.broker.amount_to_precision(self.status.get_pair(), self.status.get_old_long()["tp_amount"] - free_base)
if self.status.get_old_long()["tp_amount"]-free_base>min_base_size:
amount_to_buy = self.broker.amount_to_precision(self.status.get_pair(),self.status.get_old_long()["tp_amount"]-free_base)
self.broker.logger.log_this(f"Buying missing {amount_to_buy} {self.base}",1,self.status.get_pair())
self.broker.new_market_order(self.status.get_pair(),amount_to_buy,"buy",amount_in_base=True,log="start_trader-missing_base")
time.sleep(self.broker.get_wait_time()*2)
#Re-quering for the amount of base currency on the exchange
#Re-querying for the amount of base currency on the exchange
free_base = self.fetch_free_base()
if free_base is None:
self.broker.logger.log_this("Can't fetch the amount of base at the exchange",1,self.status.get_pair())
@ -168,6 +171,8 @@ class trader:
return 1
self.status.set_order_size(order_size)
self.status.set_no_of_safety_orders(no_of_safety_orders)
if no_of_safety_orders<self.config.get_max_short_safety_orders()*.67:
self.config.set_safety_order_deviance(3)
self.broker.logger.log_this(f"Order size: {self.broker.amount_to_precision(self.status.get_pair(),order_size)}. Amount of safety orders: {no_of_safety_orders}",2,self.status.get_pair())
#Write the changes to the config file
@ -195,15 +200,19 @@ class trader:
#Slippage threshold exceeded
self.broker.logger.log_this("Slippage threshold exceeded",1,self.status.get_pair())
return 3
self.status.set_order_size(self.config.get_order_size())
self.status.set_pause_reason("start_trader - after slippage")
self.status.set_order_size(self.config.get_order_size())
#Sending initial order
#
# Here, if the amount of the initial order is already available in the account, don't send a market order; just pull the current price and simulate that the order was sent and filled.
# Cleanup probably would have to be disabled for this to make sense.
#
self.status.set_pause_reason("start_trader - sending first order")
self.broker.logger.log_this("Sending first order...",2,self.status.get_pair())
action = "sell" if self.config.get_is_short() else "buy"
first_order = self.broker.new_market_order(self.status.get_pair(),self.status.get_order_size(),action)
first_order = self.broker.new_market_order(self.status.get_pair(),self.status.get_order_size(),action,log="start_trader")
#self.broker.logger.log_this(f"First order id: {first_order}",1,self.status.get_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.status.get_pair())
@ -216,7 +225,7 @@ class trader:
#Wait a bit longer, sometimes a recently filled market order is not updated quickly enough.
# When that happens, 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.
time.sleep(self.broker.get_wait_time()*2)
time.sleep(self.broker.get_wait_after_initial_market_order())
returned_order = self.broker.get_order(first_order["id"],self.status.get_pair())
if returned_order==self.broker.get_empty_order():
self.broker.logger.log_this("Problems with the initial order",1,self.status.get_pair())
@ -241,7 +250,7 @@ class trader:
#Save the order
if self.broker.follow_order_history:
self.status.set_pause_reason("start_trader - saving the order in deal_order_history")
self.status.update_deal_order_history(returned_order)
self.status.update_deal_order_history(returned_order, "init")
# Reset the fee count and sum fees from the first order
self.status.set_fees_paid_in_base(self.parse_fees(returned_order)[0])
@ -272,6 +281,11 @@ class trader:
self.status.set_start_price(self.broker.price_to_precision(self.status.get_pair(),self.status.get_quote_spent()/self.status.get_base_bought()))
self.status.set_safety_price_table(self.calculate_safety_prices(self.status.get_start_price(),self.status.get_no_of_safety_orders(),self.config.get_safety_order_deviance()))
# Send cleanup order (if cleanup)
self.status.set_pause_reason("start_trader - doing cleanup (if needed)")
if self.config.get_cleanup() and not self.config.get_is_short(): #Short traders do not need cleanup.
self.do_cleanup()
# Send the initial batch of safety orders
self.status.set_pause_reason("start_trader - sending safety orders")
self.broker.logger.log_this("Sending safety orders...",2,self.status.get_pair())
@ -285,15 +299,9 @@ class trader:
self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())
return 1
# Send cleanup order (if cleanup)
self.status.set_pause_reason("start_trader - doing cleanup (if needed)")
if self.config.get_cleanup() and not self.config.get_is_short(): #Short traders do not need cleanup.
self.do_cleanup()
self.status.set_deal_start_time(int(time.time()))
self.update_status(True)
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
return 0
@ -437,18 +445,15 @@ class trader:
if self.config.get_is_short(): #Short traders do not need cleanup
return 0
balance_to_clean = self.fetch_free_base()
if balance_to_clean is None:
balance_in_account = self.fetch_free_base()
if balance_in_account is None:
self.broker.logger.log_this("Can't fetch free base",1,self.status.get_pair())
return 1
balance_to_clean /= 2 #Maybe it's a good idea, sort of DCAing the dust.
min_base_size = self.broker.get_min_base_size(self.status.get_pair())
#minimum_cleanup_size = self.status.get_safety_orders()[0]["amount"]*2
if balance_to_clean >= min_base_size:
self.broker.logger.log_this(f"Balance to clean: {balance_to_clean} {self.base}",2,self.status.get_pair())
if balance_in_account*self.status.get_start_price()>self.broker.get_min_quote_size(self.status.get_pair()):
self.broker.logger.log_this(f"Balance to clean: {balance_in_account} {self.base}",2,self.status.get_pair())
self.broker.logger.log_this("Sending cleanup order...",2,self.status.get_pair())
cleanup_order = self.broker.new_limit_order(self.status.get_pair(),balance_to_clean,"sell",self.status.get_take_profit_price())
cleanup_order = self.broker.new_limit_order(self.status.get_pair(),balance_in_account,"sell",self.status.get_take_profit_price(),no_retries=True,log="cleanup")
if cleanup_order is None:
self.broker.logger.log_this("Problems with the cleanup order, new_limit_order returned None",1,self.status.get_pair())
return 1
@ -477,13 +482,11 @@ class trader:
amount_of_so-=1
if optimal_order_size==0:
self.broker.logger.log_this("Not enough base to switch. Order size would be too small",1,self.status.get_pair())
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
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.status.get_pair())
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
return None,None
return optimal_order_size,amount_of_so
@ -521,8 +524,7 @@ class trader:
return 1
#Pauses trader
self.pause = True
self.status.set_pause_reason("switch_to_short")
self.set_pause(True,"switch_to_short")
#Fetch the real amount of available base
self.broker.logger.log_this(f"Fetching available {self.base}",2,self.status.get_pair())
@ -534,15 +536,13 @@ class trader:
min_base_size = self.broker.get_min_base_size(self.status.get_pair())
if min_base_size is None:
self.broker.logger.log_this("Error. Can't fetch market info from the exchange",1,self.status.get_pair())
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
return 1
#Check if there is enough base
if self.broker.amount_to_precision(self.status.get_pair(),free_base+self.status.get_take_profit_order()["amount"])<=min_base_size:
self.broker.logger.log_this("Error. Not enough base currency",1,self.status.get_pair())
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
return 1
#Calculate order size
@ -558,8 +558,7 @@ class trader:
self.broker.logger.log_this("Closing orders...",2,self.status.get_pair())
if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())==1:
self.broker.logger.log_this("Can't cancel the take profit order. Can't switch mode",1,self.status.get_pair())
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
return 1
if self.status.get_take_profit_order()["id"]!="":
self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())
@ -573,8 +572,8 @@ class trader:
"datetime": time.strftime("[%Y/%m/%d %H:%M:%S]")
})
try:
with open(f"status/{self.base}{self.quote}.oldlong","w") as s:
s.write(dumps(self.status.get_old_long(),indent=4))
with open(f"status/{self.base}{self.quote}.oldlong","w") as old_long_file_handler:
old_long_file_handler.write(dumps(self.status.get_old_long(),indent=4))
except Exception as e:
self.broker.logger.log_this(f"Exception while saving old_long file: {e}",1,self.status.get_pair())
@ -587,7 +586,6 @@ class trader:
self.broker.logger.log_this("Config file updated",2,self.status.get_pair())
except Exception as e:
self.broker.logger.log_this(f"Error. Can't write the config file. Exception: {e}",1,self.status.get_pair())
#self.pause = False
return 1
self.status.set_stop_when_profit(False)
#self.config.set_is_short(True)
@ -595,7 +593,7 @@ class trader:
return 0
def switch_to_long(self, ignore_old_long: bool = False, already_received_quote: float = 0) -> int:
def switch_to_long(self, ignore_old_long: bool = False, already_received_quote: float = 0, double_check_price: bool = True) -> int:
'''
Takes a short trader and changes the mode to long.
Only does it if the current trader was previously a long one.
@ -605,13 +603,22 @@ class trader:
self.broker.logger.log_this("Trader already in long mode, nothing to do",1,self.status.get_pair())
return 1
self.broker.logger.log_this("Attempting to switch to long trader",0,self.status.get_pair())
if double_check_price:
#Waits a moment to see if the price has moved too much
self.broker.logger.log_this("Confirming price...",2,self.status.get_pair())
time.sleep(self.broker.get_wait_time()*4)
if not self.check_old_long(True):
self.broker.logger.log_this("False positive. Nothing to do.",1,self.status.get_pair())
return 2
#Check old_long data
self.broker.logger.log_this("Checking if old long data is valid.",2,self.status.get_pair())
if not ignore_old_long and self.status.get_old_long()=={}:
self.broker.logger.log_this("Can't find old long info on status_dict, searching for oldlong file",1,self.status.get_pair())
try:
with open(f"status/{self.base}{self.quote}.oldlong") as f:
self.status.set_old_long(load(f))
with open(f"status/{self.base}{self.quote}.oldlong") as old_long_file_handler:
self.status.set_old_long(load(old_long_file_handler))
except Exception as e:
#self.write_to_log(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.status.get_pair()} | Can't find old long file"))
self.broker.logger.log_this(f"Can't file oldlong file. Exception: {e}",1,self.status.get_pair())
@ -619,32 +626,30 @@ class trader:
return 1
#Cancel open orders
try:
for order in self.status.get_safety_orders():
self.broker.cancel_order(order["id"],self.status.get_pair())
except Exception as e:
self.broker.logger.log_this(f"Error in cancel_order while cancelling safety order. Exception: {e}",1,self.status.get_pair())
try:
if self.status.get_take_profit_order() is not None:
self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())
else:
self.broker.logger.log_this("Safety order is None",1,self.status.get_pair())
except Exception as e:
self.broker.logger.log_this(f"Error in cancel_order while cancelling take profit order. Exception: {e}",1,self.status.get_pair())
self.broker.logger.log_this("Cancelling open orders",2,self.status.get_pair())
for order in self.status.get_safety_orders():
self.broker.cancel_order(order["id"],self.status.get_pair())
if self.status.get_take_profit_order() is not None:
self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())
else:
self.broker.logger.log_this("Safety order is None",1,self.status.get_pair())
#Sell all base currency
self.broker.logger.log_this(f"Selling {self.status.get_pair().split('/')[0]}",2,self.status.get_pair())
self.liquidate_base(ignore_profits=ignore_old_long, already_received_quote=already_received_quote)
if self.config.get_liquidate_after_switch():
self.broker.logger.log_this("Liquidate after switch active. Raising quit flag.",1,self.status.get_pair())
self.quit = True
return 1
return 0
#Rewrite config file (if it exists)
if path.isfile(f"configs/{self.base}{self.quote}.bak") and path.isfile(f"configs/{self.base}{self.quote}.json"):
with open(f"configs/{self.base}{self.quote}.bak") as c:
old_config = load(c)
with open(f"configs/{self.base}{self.quote}.json","w") as c:
c.write(dumps(old_config, indent=4))
self.broker.logger.log_this("Restoring config file from backup",2,self.status.get_pair())
with open(f"configs/{self.base}{self.quote}.bak") as backup_config_file_handler:
old_config = load(backup_config_file_handler)
with open(f"configs/{self.base}{self.quote}.json","w") as config_file_handler:
config_file_handler.write(dumps(old_config, indent=4))
if self.config.load_from_file()==1:
self.config.reset_to_default()
else:
@ -666,6 +671,7 @@ class trader:
self.status.set_so_amount(0)
#Done. Ready for start_trader
self.broker.logger.log_this("Finished setting up the switch to long.",2,self.status.get_pair())
return 0
@ -681,7 +687,7 @@ class trader:
self.broker.logger.log_this("Can't fetch free base",1,self.status.get_pair())
return 1
#send market order selling the total amount of base in the last take profit short order
order = self.broker.new_market_order(self.status.get_pair(),free_base,"sell")
order = self.broker.new_market_order(self.status.get_pair(),free_base,"sell",log="liquidate_base")
time.sleep(self.broker.get_wait_time()*2)
tries = self.broker.get_retries()
while True:
@ -714,15 +720,14 @@ class trader:
but KuCoin only supports order editing on high frequency orders.
'''
self.pause = True
self.status.set_pause_reason("force_close - order handling")
self.set_pause(True,"force_close - order handling")
#Close the take profit order
self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair())
#Send the market order
amount = self.status.get_take_profit_order()["amount"]
market_order = self.broker.new_market_order(self.status.get_pair(),amount,"sell",amount_in_base=True)
market_order = self.broker.new_market_order(self.status.get_pair(),amount,"sell",amount_in_base=True,log="force_close")
time.sleep(self.broker.get_wait_time()*2)
#Wait for it to be filled
@ -748,8 +753,7 @@ class trader:
the reporting and the restart of the trader.
'''
self.pause = True #To stop the main thread to iterate through this trader's orders (just in case)
self.status.set_pause_reason("take_profit_routine - order handling") #start_trader will set this flag to False again once it starts
self.set_pause(True,"take_profit_routine - order handling") #To stop the main thread to iterate through this trader's orders (just in case)
#Add the timestamp to the deals cache and trims it
self.deals_timestamps.append(time.time())
@ -765,7 +769,7 @@ class trader:
#Save the order in history.
if self.broker.get_follow_order_history():
self.status.update_deal_order_history(filled_order)
self.status.update_deal_order_history(filled_order, "tp")
#Cancel all the safety orders ASAP
for order in self.status.get_safety_orders():
@ -782,18 +786,19 @@ class trader:
partial_filled_price.append(closed_order["average"])
self.broker.logger.log_this(f"Old safety order is partially filled, ID: {closed_order['id']}, {closed_order['filled']}/{closed_order['amount']} {self.base} filled",1,self.status.get_pair())
if self.broker.get_follow_order_history():
self.status.update_deal_order_history(closed_order)
self.status.update_deal_order_history(closed_order, "partial_fill")
if closed_order["remaining"]!=0: #If this order is not completely filled, it is safe to assume that no order coming after this one was partially filled.
break
#Now we can clear the safety order list
self.status.set_safety_orders([])
#Handle the partial fills
if not self.status.get_is_short():
if not self.config.get_is_short():
#With short traders is just an accounting issue, since when the trader restarts it will be buying cheaper what it sold more expensive in the partially filled safety order(s)
if partial_filled_amount!=0 and len(partial_filled_price)>0 and partial_filled_amount>self.broker.get_min_base_size(self.status.get_pair()):
#send a market order and sum the profits and wait for it to be filled
market_order = self.broker.new_market_order(self.status.get_pair(),partial_filled_amount,"sell",amount_in_base=True)
self.broker.logger.log_this("Sending partial fill sell order...",1,self.status.get_pair())
market_order = self.broker.new_market_order(self.status.get_pair(),partial_filled_amount,"sell",amount_in_base=True,log="take_profit_routine-partial_fill")
time.sleep(self.broker.get_wait_time()*2)
tries = self.broker.get_retries()
while True:
@ -803,10 +808,11 @@ class trader:
partial_profit = market_order["cost"]-(avg_buy_price*partial_filled_amount)-self.parse_fees(market_order)[1]
self.status.set_partial_profit(self.status.get_partial_profit()+partial_profit)
break
self.broker.logger.log_this("Waiting for partial fill sell order to fill.",2,self.status.get_pair())
tries-=1
time.sleep(self.broker.get_wait_time())
if tries==0:
self.broker.logger.log_this("Partial fill sell order not filling.",1,self.status.get_pair())
self.broker.logger.log_this("Partial fill sell order not filled.",1,self.status.get_pair())
break
if not self.broker.check_for_duplicate_profit_in_db(filled_order):
@ -858,15 +864,13 @@ class trader:
if abs(filled_order["price"]-price_to_compare)/filled_order["price"]>self.broker.get_slippage_default_threshold():
self.broker.logger.log_this(f"Slippage threshold exceeded, waiting for cooldown and restarting trader",1,self.status.get_pair())
time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier())
#The trader is restarted by the instance instead of by itself to allow a couple of more seconds for the price to return to normal.
#This could also be the default behavior.
self.pause = False
self.set_pause(False)
self.restart = True
return 1
elif self.check_orderbook_depth(self.broker.get_slippage_default_threshold(),self.status.get_order_size(),filled_order["price"]):
self.broker.logger.log_this(f"Orderbook depth not sufficient, waiting for cooldown and restarting trader",1,self.status.get_pair())
time.sleep(self.broker.get_wait_time()*self.broker.get_cooldown_multiplier())
self.pause = False
self.set_pause(False)
self.restart = True
return 1
@ -874,9 +878,8 @@ class trader:
self.status.set_pause_reason("take_profit_routine - restart_trader call")
restart_trader = self.start_trader()
self.status.set_pause_reason("take_profit_routine - restart_trader call - start_trader() called")
#retries = self.broker.get_retries()
if restart_trader in self.trader_restart_errors.keys():
self.pause = False
self.set_pause(False)
self.restart = True
self.status.save_to_file(is_backup=True)
self.broker.logger.log_this(self.trader_restart_errors[restart_trader],1,self.status.get_pair())
@ -926,9 +929,9 @@ class trader:
self.broker.logger.log_this(f"Sending a new safety order ({i+1}/{orders_to_place})",2,self.status.get_pair())
so_size = self.gib_so_size(self.status.get_order_size(),self.status.get_so_amount()+1,self.config.get_safety_order_scale())
if self.config.get_is_short():
new_order = self.broker.new_limit_order(self.status.get_pair(),so_size,"sell",self.status.get_safety_price_table()[self.status.get_so_amount()+1])
new_order = self.broker.new_limit_order(self.status.get_pair(),so_size,"sell",self.status.get_safety_price_table()[self.status.get_so_amount()+1], log="send_new_safety_order_batch")
else:
new_order = self.broker.new_limit_order(self.status.get_pair(),so_size/self.status.get_safety_price_table()[self.status.get_so_amount()+1],"buy",self.status.get_safety_price_table()[self.status.get_so_amount()+1])
new_order = self.broker.new_limit_order(self.status.get_pair(),so_size/self.status.get_safety_price_table()[self.status.get_so_amount()+1],"buy",self.status.get_safety_price_table()[self.status.get_so_amount()+1],log="send_new_safety_order_batch")
if new_order==1:
self.broker.logger.log_this("Not enough balance to send a new safety order",1,self.status.get_pair())
@ -963,21 +966,26 @@ class trader:
return 1
#Pause the trader
self.pause = True
self.status.set_pause_reason("renew_tp_and_so_routine")
self.set_pause(True, "renew_tp_and_so_routine")
#Save the order
if self.broker.get_follow_order_history():
for item in filled_safety_orders:
self.status.update_deal_order_history(item)
self.status.update_deal_order_history(item, "so")
#Add the amount filled in the last safety orders to the totals
previous_base = self.status.get_base_bought()
previous_quote = self.status.get_quote_spent()
previous_fees_paid_in_quote = self.status.get_fees_paid_in_quote()
for order in filled_safety_orders:
safety_orders_to_remove_by_id.append(order["id"])
new_fees_base,new_fees_quote = self.parse_fees(order)
self.status.set_fees_paid_in_quote(self.status.get_fees_paid_in_quote() + new_fees_quote)
self.status.set_base_bought(self.status.get_base_bought() + order["filled"] - new_fees_base)
self.status.set_quote_spent(self.status.get_quote_spent() + order["cost"])
new_fees_base, new_fees_quote = self.parse_fees(order)
previous_fees_paid_in_quote += new_fees_quote
previous_base = previous_base + order["filled"] - new_fees_base
previous_quote += order["cost"]
self.status.set_base_bought(previous_base)
self.status.set_quote_spent(previous_quote)
self.status.set_fees_paid_in_quote(previous_fees_paid_in_quote)
#Remove the filled safety orders from the list
if safety_orders_to_remove_by_id!=[]:
@ -1000,7 +1008,7 @@ class trader:
if old_tp_order["filled"]>0:
self.broker.logger.log_this(f"Old take profit order is partially filled, id {old_tp_order['id']}",1,self.status.get_pair())
if self.broker.get_follow_order_history():
self.status.update_deal_order_history(old_tp_order)
self.status.update_deal_order_history(old_tp_order, "old_tp")
#self.status.set_base_bought(old_tp_order["remaining"])
# Partial profit calculation
if not self.config.get_is_short():
@ -1013,56 +1021,33 @@ class trader:
self.status.set_fees_paid_in_base(self.status.get_fees_paid_in_base() + self.parse_fees(old_tp_order)[0])
#Cooldown
time.sleep(self.broker.get_wait_before_new_safety_order())
#Send new SO(s)
#Do not send new orders if the max amount is reached or surpassed.
#It can happen if the max amount of concurrent orders is modified through an API call.
max_orders = self.config.get_concurrent_safety_orders() if not self.status.get_is_boosted() else self.config.get_boosted_concurrent_safety_orders()
if len(self.status.get_safety_orders())<max_orders:
self.send_new_safety_order_batch(max_orders-len(self.status.get_safety_orders()))
#Cooldown
time.sleep(self.broker.get_wait_time())
#time.sleep(self.broker.get_wait_before_new_safety_order())
#Send new TP 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.status.get_pair())
self.status.set_pause_reason(error_string)
return 4
return 4
#Cooldown
#time.sleep(self.broker.get_wait_before_new_safety_order())
#Send new safety order(s)
#Do not send new orders if the max amount is reached or surpassed.
max_orders = self.config.get_concurrent_safety_orders() if not self.status.get_is_boosted() else self.config.get_boosted_concurrent_safety_orders()
if len(self.status.get_safety_orders())<max_orders:
self.send_new_safety_order_batch(max_orders-len(self.status.get_safety_orders()))
#Update status dict
self.update_status(True)
#Toggle the pause flag
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
#Done
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 self.status.get_old_long()!={}:
price_exceeds = self.status.get_price()>float(self.status.get_old_long()["tp_price"])
if price_exceeds:
self.warnings["short_price_exceeds_old_long"] = True
else:
self.warnings["short_price_exceeds_old_long"] = False
self.warnings["speol_notified"] = False
if not self.warnings["speol_notified"] and price_exceeds:
#Only notify one time AND if autoswitch is off
self.warnings["speol_notified"] = True
if not self.config.get_autoswitch():
message = f"{self.base}@{self.status.get_price()} ({str(self.broker.exchange)}), exceeds old long price of {self.status.get_old_long()['tp_price']}"
self.broker.logger.log_this(message,0,self.status.get_pair())
return 0
def check_orderbook_depth(self, threshold: float, order_size: float, old_price: float = 0, size_in_quote = True) -> bool:
'''
@ -1107,6 +1092,21 @@ class trader:
return False
def check_old_long(self, fetch_price=False):
'''
Check if it is profitable to switch to back to long.
Returns True if it is profitable, False otherwise.
If selling the base currency left at the current market price plus the quote already received turns out to be more than the old long deal target,
it means that we already are in profit territory, switch back to long.
A more conservative approach would be old_target = self.status.get_old_long()["quote_spent"], just breaking even.
'''
price = self.status.get_price() if not fetch_price else self.broker.get_top_bid_price(self.status.get_pair())
old_target = self.status.get_old_long()["tp_price"]*self.status.get_old_long()["tp_amount"]
base_left = self.status.get_old_long()["tp_amount"]-self.status.get_base_bought()
return (base_left*price)+self.status.get_quote_spent()>=old_target
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.
@ -1117,25 +1117,16 @@ class trader:
self.update_status(False)
return 0
#Check if short price exceeds old long price. If so, send a Telegram message
if self.config.get_is_short() and self.status.get_old_long()!={} and self.config.get_check_old_long_price():
self.check_old_long_price()
self.status.set_pause_reason("check for autoswitch")
#If it's a short trader that used to be long AND autoswitch is enabled
if self.config.get_is_short() and self.config.get_autoswitch() and self.status.get_old_long()!={}:
#If selling the base currency left at the current market price plus the quote already received turns out to be more than the old long deal target,
# it means that we already are in profit territory, switch back to long.
#A more conservative approach would be old_target = self.status.get_old_long()["quote_spent"], just breaking even.
old_target = self.status.get_old_long()["tp_price"]*self.status.get_old_long()["tp_amount"]
base_left = self.status.get_old_long()["tp_amount"]-self.status.get_base_bought()
if (base_left*self.status.get_price())+self.status.get_quote_spent()>=old_target:
if self.check_old_long():
#Sell all base (market), report the profits and restart the trader
self.status.set_pause_reason("automatic_switch")
self.switch_to_long(already_received_quote=self.status.get_quote_spent())
if not self.config.get_liquidate_after_switch():
self.restart = True
return 1
if self.switch_to_long(already_received_quote=self.status.get_quote_spent())!=2:
if not self.config.get_liquidate_after_switch():
self.restart = True
return 1
#Check for autoswitch (long->short)
if not self.config.get_is_short() and self.status.get_so_amount()==self.status.get_no_of_safety_orders() and self.config.get_autoswitch():
self.switch_to_short()
@ -1169,17 +1160,6 @@ class trader:
self.status.set_pause_reason("check if tp_order is filled")
#Checks if the take profit order is filled
if self.status.get_take_profit_order()["id"] not in open_orders_ids:
# Check if the order has a wrong id. If so, update the order.
# To cover a very rare case that happens if the trader sends a new take profit order but is interrupted before saving the status.
# Not sure if it is worth to keep this code.
for order in open_orders_list:
if order["amount"]==self.status.get_take_profit_order()["amount"] and order["price"]==self.status.get_take_profit_order()["price"] and order["side"]==self.status.get_take_profit_order()["side"]:
#Right order, wrong id. Update order
self.broker.logger.log_this(f"Updating take profit order for {self.status.get_pair()}",1,self.status.get_pair())
self.status.set_take_profit_order(order)
self.update_status(True)
return 0
tp_status = self.broker.get_order(self.status.get_take_profit_order()["id"],self.status.get_pair())
if tp_status["status"]=="closed":
if tp_status["filled"]>0:
@ -1216,13 +1196,12 @@ class trader:
filled_ids.append(order["id"])
if filled_ids!=[]:
#closed_orders = self.broker.get_closed_orders(self.status.get_pair())
#filled_orders = [item for item in closed_orders if item["id"] in filled_ids and item["status"]=="closed"]
filled_orders = []
for id in filled_ids:
order = self.broker.get_order(id, self.status.get_pair())
if order["status"]=="closed":
filled_orders.append(order)
time.sleep(self.broker.get_wait_time())
if len(filled_orders)>0: #To make sure that the safety orders are actually filled (Kucoin sometimes sends incomplete order lists)
self.status.set_safety_orders_filled(self.status.get_safety_orders_filled()+len(filled_orders))
renew_outcome = self.renew_tp_and_so_routine(filled_orders)
@ -1242,8 +1221,7 @@ class trader:
return 1
elif renew_outcome==3:
self.broker.logger.log_this(f"Can't cancel old take profit order. renew_tp_and_so_routine returned 3",1,self.status.get_pair())
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
if self.config.get_attempt_restart():
self.status.save_to_file(is_backup=True)
self.restart = True
@ -1263,11 +1241,15 @@ class trader:
if condition_a and condition_b:
amount_to_send = max_concurrent_safety_orders-len(self.status.get_safety_orders())
self.pause = True
self.set_pause(True, "sending safety order batch")
self.send_new_safety_order_batch(amount_to_send)
self.pause = False
self.set_pause(False)
self.update_status(True)
#Base check
if self.base_diff_check and time.time()>self.base_check_time+self.base_check_interval:
self.base_check()
#Render status line(s)
self.status.set_status_string(self.generate_status_strings())
@ -1280,6 +1262,14 @@ class trader:
return 0
def base_check(self):
self.base_check_time = time.time()
current_base_balance = self.fetch_free_base()
#3. If self.base_amount_missing==current_base_balance: replace take profit order
#4. Set self.base_diff_check to False
def check_boosted(self):
'''
Checks if the trader qualifies for boost:
@ -1361,15 +1351,21 @@ class trader:
return 1
if self.config.get_is_short():
self.status.set_take_profit_price(self.status.get_quote_spent()/self.status.get_base_bought()*(1-(self.get_tp_level(self.status.get_so_amount())-1)))
self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"buy",self.status.get_take_profit_price()))
self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"buy",self.status.get_take_profit_price(),log="new_tp_order"))
else:
self.status.set_take_profit_price(self.status.get_quote_spent()/self.status.get_base_bought()*self.get_tp_level(self.status.get_so_amount()))
self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"sell",self.status.get_take_profit_price()))
if self.status.get_take_profit_order()==1: #This means that there was a miscalculation of base currency amount, let's correct it.
self.status.set_take_profit_order(self.broker.new_limit_order(self.status.get_pair(),self.status.get_base_bought(),"sell",self.status.get_take_profit_price(),log="new_tp_order"))
if self.status.get_take_profit_order()==1: #This means that there was a miscalculation of base currency amount
if self.config.get_is_short(): #If in short mode, we don't recalculate anything.
return 1
adjusted = self.adjust_base()
if adjusted is not None:
# if self.status.get_base_bought()-adjusted>=adjusted*self.min_base_difference:
# #Enabling base check
# self.broker.logger.log_this("Enabling base check",1,self.status.get_pair())
# self.base_diff_check = True
# self.base_check_time = time.time()
# self.base_amount_missing = self.status.get_base_bought()-adjusted
self.status.set_base_bought(adjusted)
self.status.set_take_profit_order(None) #Just to be able to iterate
if self.status.get_take_profit_order() not in [None,self.broker.get_empty_order()]:
@ -1522,7 +1518,7 @@ class trader:
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"])
return self.broker.new_limit_order(f"{self.base}/{new_quote}",old_order["amount"],old_order["side"],old_order["price"],log="quote_currency_replace_order")
def quote_currency_switch_configs(self, new_quote: str) -> int:
@ -1549,8 +1545,8 @@ class trader:
#If there is an old_long file, also copy it
if self.config.get_is_short() and self.status.get_old_long()!={}:
try:
with open(f"status/{self.base}{self.quote}.oldlong","w") as c:
c.write(dumps(self.status.get_old_long(), indent=4))
with open(f"status/{self.base}{self.quote}.oldlong","w") as old_long_file_handler:
old_long_file_handler.write(dumps(self.status.get_old_long(), indent=4))
except Exception as e:
self.broker.logger.log_this(f"Exception while writing new old_long file: {e}",1,self.status.get_pair())
@ -1655,7 +1651,6 @@ class trader:
line3 = ""
if self.status.get_base_bought()!=0:
line3 = draw_line(mid_price,low_price,high_price,self.status.get_quote_spent()/self.status.get_base_bought())
p = "*PAUSED*" if self.pause==True else ""
low_boundary_color = self.get_color("red")
price_color = self.get_color("white")
target_price_color = self.get_color("green")
@ -1695,7 +1690,7 @@ class trader:
safety_order_string = f"{self.status.get_safety_orders_filled()}/{self.get_color('cyan')}{concurrent_so_amount}{self.get_color('white')}/{self.status.get_no_of_safety_orders()}".rjust(27)
prices = f"{low_boundary_color}{low_boundary}{self.get_color('white')}|{price_color}{mid_boundary}{self.get_color('white')}|{target_price_color}{high_boundary}{self.get_color('white')}|{pct_color}{pct_to_profit_str}%{self.get_color('white')}"
line1 = f"{p}{pair_color}{self.status.get_pair().center(13)}{self.get_color('white')}| {safety_order_string} |{prices}| Uptime: {self.seconds_to_time(self.status.get_deal_uptime())}"
line1 = f"{pair_color}{self.status.get_pair().center(13)}{self.get_color('white')}| {safety_order_string} |{prices}| Uptime: {self.seconds_to_time(self.status.get_deal_uptime())}"
if self.status.get_is_boosted():
line1 = f"{line1} | BOOSTED"
if self.config.get_autoswitch():
@ -1716,6 +1711,16 @@ class trader:
return status_string
def set_pause(self, pause: bool, msg: str = "") -> None:
'''
Sets the pause state and reason
'''
self.pause = pause
self.status.set_pause_reason(msg)
return
def load_imported_trader(self) -> int:
'''
Loads status dictionary, orders and sets up variables
@ -1737,8 +1742,7 @@ class trader:
return 1
#Done
self.pause = False
self.status.set_pause_reason("")
self.set_pause(False)
self.update_status(True)
return 0

View File

@ -43,7 +43,8 @@ INSTANCE
10) edit_call_wait_time 11) reload_markets 12) fetch_full_log
13) paused_traders 14) fetch_log 15) edit_cooldown_multiplier
16) get_balance 17) cancel_global_last_call
18) mod_default_order_size
18) mod_default_order_size 19) toggle_log_orders
20) refresh_log_cache
EARN
31) toggle_pause 32) get_step_size 33) set_step_size
@ -341,6 +342,19 @@ if __name__=="__main__":
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
input("Press ENTER to continue ")
elif command==19:
print("toggle_log_orders turns on or off the logging of orders")
if input("Proceed? (Y/n) ") in ["Y","y",""]:
url = f"{base_url}{port}/toggle_log_orders"
print(json.loads(requests.post(url, headers=headers).content))
input("Press ENTER to continue ")
elif command==20:
print("refresh_log_cache refreshes the log cache")
if input("Proceed? (Y/n) ") in ["Y","y",""]:
url = f"{base_url}{port}/refresh_log_cache"
print(json.loads(requests.post(url, headers=headers).content))
input("Press ENTER to continue ")
######################
######## EARN ########

View File

@ -5,10 +5,8 @@ import calendar
import logging
import threading
import os
from collections import deque
from typing import Iterable, List, Tuple
from contextlib import contextmanager
from flask import Flask, jsonify, request, Response
from flask import Flask, jsonify, request
from waitress import serve