From 0d753aa3cd3a127afafe6f30870119c90e0f2e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20S=C3=A1nchez?= Date: Mon, 15 Sep 2025 18:55:59 -0300 Subject: [PATCH] 2025.09.15 --- changelog.txt | 11 ++++ exchange_wrapper.py | 133 +++++++++++++++++++++++--------------------- main.py | 50 +++++++++-------- trader.py | 71 +++++++++++------------ 4 files changed, 141 insertions(+), 124 deletions(-) diff --git a/changelog.txt b/changelog.txt index 92d56ef..d5af89c 100755 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,14 @@ +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. diff --git a/exchange_wrapper.py b/exchange_wrapper.py index f779dd9..03acced 100755 --- a/exchange_wrapper.py +++ b/exchange_wrapper.py @@ -14,16 +14,16 @@ 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_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) #Initialize database @@ -503,26 +503,26 @@ class Broker: return [] - def fetch_full_orders(self,pairs=None) -> list: - ''' - Returns a list of all orders on the exchange + # 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 - ''' + # :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 [] + # 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: @@ -536,9 +536,16 @@ 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() in ["binance","kucoin"]: + 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 + else: + return self.exchange.fetch_open_orders() except Exception as e: self.logger.log_this(f"Exception in fetch_open_orders: {e}",2) return [] @@ -563,25 +570,25 @@ class Broker: 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 + # 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 - ''' + # :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 [] + # 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 @@ -605,25 +612,25 @@ class Broker: return [] - def get_opened_orders_binance(self,pairs): - ''' - Returns a list of all the open orders on the exchange + # 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 - ''' + # :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 [] + # 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): diff --git a/main.py b/main.py index 8a2b8e3..a256814 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ import exchange_wrapper import trader -version = "2025.09.12" +version = "2025.09.14" ''' Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors @@ -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: @@ -1606,7 +1612,7 @@ 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 + instance.set_pause(True, "Switching to long mode") if instance.switch_to_long(ignore_old_long=ignore_old_long)==1: return jsonify({"Error": "Error in switch_to_long()"}) if instance.start_trader()==1: @@ -1774,14 +1780,14 @@ 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 + instance.set_pause(True, "Adding safety orders") #x.no_of_safety_orders += int(amount) instance.config.set_no_of_safety_orders(instance.config.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 +2103,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 +2137,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 +2147,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"}) @@ -2324,14 +2330,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: diff --git a/trader.py b/trader.py index 1b37c19..460bec4 100755 --- a/trader.py +++ b/trader.py @@ -67,12 +67,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,8 +127,7 @@ 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(): self.broker.logger.log_this("Calculating optimal order size...",2,self.status.get_pair()) @@ -293,8 +292,7 @@ class trader: 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 @@ -443,10 +441,8 @@ class trader: 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: + if balance_to_clean >= self.broker.get_min_base_size(self.status.get_pair()): self.broker.logger.log_this(f"Balance to clean: {balance_to_clean} {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(),no_retries=True) @@ -478,13 +474,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_sizeself.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()) - 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 @@ -868,7 +855,7 @@ class trader: restart_trader = self.start_trader() self.status.set_pause_reason("take_profit_routine - restart_trader call - start_trader() called") 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()) @@ -955,8 +942,7 @@ 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(): @@ -1027,8 +1013,7 @@ class trader: self.update_status(True) #Toggle the pause flag - self.pause = False - self.status.set_pause_reason("") + self.set_pause(False) #Done return 0 @@ -1234,8 +1219,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 @@ -1255,9 +1239,9 @@ 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) #Render status line(s) @@ -1708,6 +1692,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 @@ -1729,8 +1723,7 @@ class trader: return 1 #Done - self.pause = False - self.status.set_pause_reason("") + self.set_pause(False) self.update_status(True) return 0 \ No newline at end of file