From 069cff2402463f261ae67b3a3d169a137ae98fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20S=C3=A1nchez?= Date: Sun, 24 Aug 2025 14:58:38 -0300 Subject: [PATCH] std/boosted concurrent safety orders --- config_handler.py | 23 +++++++++++++++++------ exchange_wrapper.py | 3 --- main.py | 8 ++++---- todo.txt | 23 ++++++++++------------- trader.py | 39 +++++++++++++++++++++------------------ 5 files changed, 52 insertions(+), 44 deletions(-) diff --git a/config_handler.py b/config_handler.py index 617d351..83a2680 100644 --- a/config_handler.py +++ b/config_handler.py @@ -14,7 +14,8 @@ class ConfigHandler: "order_size": self.broker.get_default_order_size(), "no_of_safety_orders": 30, "max_short_safety_orders": 45, - "max_concurrent_safety_orders": 5, + "concurrent_safety_orders": 3, + "boosted_concurrent_safety_orders": 5, "safety_order_deviance": 2, "safety_order_scale": 0.0105, "dynamic_so_deviance": True, @@ -69,8 +70,11 @@ class ConfigHandler: def get_max_short_safety_orders(self): return self.config_dictionary["max_short_safety_orders"] - def get_max_concurrent_safety_orders(self): - return self.config_dictionary["max_concurrent_safety_orders"] + def get_concurrent_safety_orders(self): + return self.config_dictionary["concurrent_safety_orders"] + + def get_boosted_concurrent_safety_orders(self): + return self.config_dictionary["boosted_concurrent_safety_orders"] def get_safety_order_deviance(self): return self.config_dictionary["safety_order_deviance"] @@ -177,11 +181,18 @@ class ConfigHandler: self.config_dictionary["max_short_safety_orders"] = max_short_safety_orders return 0 - def set_max_concurrent_safety_orders(self, max_concurrent_safety_orders: int): - # if not isinstance(max_concurrent_safety_orders, int): + def set_concurrent_safety_orders(self, concurrent_safety_orders: int): + # if not isinstance(concurrent_safety_orders, int): # self.broker.logger.log_this(f"Max concurrent safety orders provided is not an integer",1,self.get_pair()) # return 1 - self.config_dictionary["max_concurrent_safety_orders"] = max_concurrent_safety_orders + self.config_dictionary["concurrent_safety_orders"] = concurrent_safety_orders + return 0 + + def set_boosted_concurrent_safety_orders(self, boosted_concurrent_safety_orders: int): + # if not isinstance(concurrent_safety_orders, int): + # self.broker.logger.log_this(f"Max concurrent safety orders provided is not an integer",1,self.get_pair()) + # return 1 + self.config_dictionary["boosted_concurrent_safety_orders"] = boosted_concurrent_safety_orders return 0 def set_safety_order_deviance(self, safety_order_deviance: int): diff --git a/exchange_wrapper.py b/exchange_wrapper.py index 538eb0f..1232e5a 100755 --- a/exchange_wrapper.py +++ b/exchange_wrapper.py @@ -1147,8 +1147,5 @@ class Logger: return 0 -class Order: - def __init__(self, order: dict = {}): - pass \ No newline at end of file diff --git a/main.py b/main.py index 9e97d00..1b35840 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ import exchange_wrapper import trader -version = "2025.08.23" +version = "2025.08.24" ''' Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors @@ -340,9 +340,9 @@ def main_routine(): global_status["paused_traders"].clear() for instance in running_traders: if not instance.config.get_is_short(): - curr += int(instance.get_status_dict()["so_amount"]) # For the safety order occupancy percentage calculation - top += int(instance.config.get_no_of_safety_orders()) # It shows the percentage of safety orders not filled - if "status_string" in instance.get_status_dict(): # status_strings + curr += int(instance.status.get_so_amount()) # For the safety order occupancy percentage calculation + top += int(instance.config.get_no_of_safety_orders()) + if "status_string" in instance.get_status_dict(): long_traders_status_strings.append(str(instance)) elif "status_string" in instance.get_status_dict(): short_traders_status_strings.append(str(instance)) diff --git a/todo.txt b/todo.txt index 81a5c9e..d73f08a 100755 --- a/todo.txt +++ b/todo.txt @@ -1,19 +1,16 @@ Mandatory: ========= -1. Stats webpage. -2. Maintain local orderbooks for each trading pair, which enables: +0. Stats webpage. +1. Maintain local orderbooks for each trading pair, which enables: 2a. Smart order pricing: Prioritization of fill speed over instant profit or vice versa -3. Proper handling of order price too high/low in OKX (rare, it happens when under heavy volatility). -4. Multiple safety orders open at the same time (to catch big volatility spikes more effectively) -5. Things that should be objects (it's not 1994): - * Orders. - * Config (Mostly done). - * Status (Mostly done). -6. API documentation. -7. Implement api key hashing. -8. Dockerize. -9. Cache generated status strings, only recalculate when prices change. -10. Inspect orderbook liquidity prior to changing mode from short to long (big sell market order needs to have liquidity). +2. Proper handling of order price too high/low in OKX (rare, it happens when under heavy volatility). +3. API documentation. +4. Implement api key hashing. +5. Dockerize. +6. Inspect orderbook liquidity prior to changing mode from short to long (big sell market order needs to have liquidity). +7. API endpoint to modify the amount of concurrent safety orders +8. Maybe when boosted, also increment the amount of concurrent safety orders? +9. Use create_orders ccxt method to send the batch of safety orders (Binance does not support it in spot trading) Would be nice to have: diff --git a/trader.py b/trader.py index 4cdbe6e..a887278 100755 --- a/trader.py +++ b/trader.py @@ -270,7 +270,8 @@ class trader: # 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.config.get_pair()) - max_initial_safety_orders = min(self.config.get_max_concurrent_safety_orders(),self.config.get_no_of_safety_orders()) #To never send more than the max amount of safety orders + amount_of_so = self.config.get_concurrent_safety_orders() if not self.status.get_is_boosted() else self.config.get_boosted_concurrent_safety_orders() + max_initial_safety_orders = min(amount_of_so,self.config.get_no_of_safety_orders()) #To never send more than the max amount of safety orders orders_placed = self.send_new_safety_order_batch(max_initial_safety_orders) if orders_placed is not None: self.broker.logger.log_this(f"{orders_placed}/{max_initial_safety_orders} safety orders placed",2,self.config.get_pair()) @@ -299,12 +300,12 @@ class trader: :return: int ''' try: - try: - self.status.set_next_so_price(self.status.get_safety_price_table()[self.status.get_so_amount()]) - except Exception as e: - self.broker.logger.log_this(f"Is safety_price_table populated? Exception: {e} | Safety price table: {self.status.get_safety_price_table()} | Safety order index: {self.status.get_so_amount()}",1,self.config.get_pair()) if self.status.get_safety_orders()!=[]: self.status.set_next_so_price(self.status.get_safety_orders()[0]["price"]) + elif len(self.status.get_safety_price_table())>self.status.get_safety_orders_filled(): + self.status.set_next_so_price(self.status.get_safety_price_table()[self.status.get_safety_orders_filled()]) + else: + self.status.set_next_so_price(0) self.status.set_is_paused(self.pause) self.status.set_is_short(self.config.get_is_short()) self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) @@ -768,7 +769,7 @@ class trader: 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.config.get_pair()) self.broker.logger.log_this(f"Fill price: {filled_order['price']} {self.quote}",2,self.config.get_pair()) - self.broker.logger.log_this(f"Safety orders triggered: {self.status.get_so_amount()-1}",2,self.config.get_pair()) + self.broker.logger.log_this(f"Safety orders triggered: {self.status.get_safety_orders_filled()}",2,self.config.get_pair()) self.status.set_pause_reason("take_profit_routine - check time limit") #Checks if there is a time limit for the trader @@ -821,12 +822,11 @@ class trader: Sends a new safety order batch to the broker :param amount: int - The amount of safety orders to send. :return: The amount of orders succesfully sent. None if an error occurs. - - If the amount of orders returned is less than the amount expected, we should not try to send more safety orders. """ if amount<1: return 0 + orders_to_place = min(self.config.get_no_of_safety_orders()-self.status.get_so_amount(),amount) if orders_to_place<1: return 0 @@ -888,12 +888,6 @@ class trader: new_order_list.append(order) self.status.set_safety_orders(new_order_list) - #Cooldown - time.sleep(self.broker.get_wait_before_new_safety_order()) - - #Send new SO(s) - orders_sent = self.send_new_safety_order_batch(len(filled_safety_orders)) - #Cancel old TP order if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.config.get_pair())==1: error_string = f"{self.config.get_pair()} | {self.status.get_take_profit_order()['id']} | Old TP order probably filled. Can't cancel. This trader should be restarted" @@ -917,9 +911,18 @@ class trader: self.status.set_quote_spent(self.status.get_quote_spent() - old_tp_order["cost"]) self.status.set_fees_paid_in_quote(self.status.get_fees_paid_in_quote() + self.parse_fees(old_tp_order)[1]) #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_time()) + 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())0 else "" - message = f"{self.config.get_pair()} closed a {'short' if self.config.get_is_short() else 'long'} trade.\nProfit: {round(profit,6)} {self.quote}{extra}\nSafety orders triggered: {self.status.get_so_amount()-1}\nTake profit price: {order['price']} {self.quote}\nTrade size: {round(order['cost'],2)} {self.quote}\nDeal uptime: {self.seconds_to_time(self.status.get_deal_uptime())}\nOrder ID: {order['id']}\nExchange: {self.broker.get_exchange_name().capitalize()}\n" + message = f"{self.config.get_pair()} closed a {'short' if self.config.get_is_short() else 'long'} trade.\nProfit: {round(profit,6)} {self.quote}{extra}\nSafety orders triggered: {self.status.get_safety_orders_filled()}\nTake profit price: {order['price']} {self.quote}\nTrade size: {round(order['cost'],2)} {self.quote}\nDeal uptime: {self.seconds_to_time(self.status.get_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: @@ -1552,7 +1555,7 @@ class trader: except Exception as e: print(e) - safety_order_string = f"{self.status.get_safety_orders_filled()}/{self.config.get_no_of_safety_orders()}{self.get_color('cyan')} {len(self.status.get_safety_orders())}{self.get_color('white')}".rjust(6) + safety_order_string = f"{self.status.get_safety_orders_filled()}/{self.get_color('cyan')}{len(self.status.get_safety_orders())}{self.get_color('white')}/{self.config.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.config.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():