std/boosted concurrent safety orders

This commit is contained in:
Nicolás Sánchez 2025-08-24 14:58:38 -03:00
parent 6bf3df0418
commit 069cff2402
5 changed files with 52 additions and 44 deletions

View File

@ -14,7 +14,8 @@ class ConfigHandler:
"order_size": self.broker.get_default_order_size(), "order_size": self.broker.get_default_order_size(),
"no_of_safety_orders": 30, "no_of_safety_orders": 30,
"max_short_safety_orders": 45, "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_deviance": 2,
"safety_order_scale": 0.0105, "safety_order_scale": 0.0105,
"dynamic_so_deviance": True, "dynamic_so_deviance": True,
@ -69,8 +70,11 @@ class ConfigHandler:
def get_max_short_safety_orders(self): def get_max_short_safety_orders(self):
return self.config_dictionary["max_short_safety_orders"] return self.config_dictionary["max_short_safety_orders"]
def get_max_concurrent_safety_orders(self): def get_concurrent_safety_orders(self):
return self.config_dictionary["max_concurrent_safety_orders"] 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): def get_safety_order_deviance(self):
return self.config_dictionary["safety_order_deviance"] return self.config_dictionary["safety_order_deviance"]
@ -177,11 +181,18 @@ class ConfigHandler:
self.config_dictionary["max_short_safety_orders"] = max_short_safety_orders self.config_dictionary["max_short_safety_orders"] = max_short_safety_orders
return 0 return 0
def set_max_concurrent_safety_orders(self, max_concurrent_safety_orders: int): def set_concurrent_safety_orders(self, concurrent_safety_orders: int):
# if not isinstance(max_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()) # self.broker.logger.log_this(f"Max concurrent safety orders provided is not an integer",1,self.get_pair())
# return 1 # 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 return 0
def set_safety_order_deviance(self, safety_order_deviance: int): def set_safety_order_deviance(self, safety_order_deviance: int):

View File

@ -1147,8 +1147,5 @@ class Logger:
return 0 return 0
class Order:
def __init__(self, order: dict = {}):
pass

View File

@ -18,7 +18,7 @@ import exchange_wrapper
import trader 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 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() global_status["paused_traders"].clear()
for instance in running_traders: for instance in running_traders:
if not instance.config.get_is_short(): if not instance.config.get_is_short():
curr += int(instance.get_status_dict()["so_amount"]) # For the safety order occupancy percentage calculation curr += int(instance.status.get_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 top += int(instance.config.get_no_of_safety_orders())
if "status_string" in instance.get_status_dict(): # status_strings if "status_string" in instance.get_status_dict():
long_traders_status_strings.append(str(instance)) long_traders_status_strings.append(str(instance))
elif "status_string" in instance.get_status_dict(): elif "status_string" in instance.get_status_dict():
short_traders_status_strings.append(str(instance)) short_traders_status_strings.append(str(instance))

View File

@ -1,19 +1,16 @@
Mandatory: Mandatory:
========= =========
1. Stats webpage. 0. Stats webpage.
2. Maintain local orderbooks for each trading pair, which enables: 1. Maintain local orderbooks for each trading pair, which enables:
2a. Smart order pricing: Prioritization of fill speed over instant profit or vice versa 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). 2. 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) 3. API documentation.
5. Things that should be objects (it's not 1994): 4. Implement api key hashing.
* Orders. 5. Dockerize.
* Config (Mostly done). 6. Inspect orderbook liquidity prior to changing mode from short to long (big sell market order needs to have liquidity).
* Status (Mostly done). 7. API endpoint to modify the amount of concurrent safety orders
6. API documentation. 8. Maybe when boosted, also increment the amount of concurrent safety orders?
7. Implement api key hashing. 9. Use create_orders ccxt method to send the batch of safety orders (Binance does not support it in spot trading)
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).
Would be nice to have: Would be nice to have:

View File

@ -270,7 +270,8 @@ class trader:
# Send the initial batch of safety orders # Send the initial batch of safety orders
self.status.set_pause_reason("start_trader - sending 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()) 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) orders_placed = self.send_new_safety_order_batch(max_initial_safety_orders)
if orders_placed is not None: 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()) 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 :return: int
''' '''
try: 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()!=[]: if self.status.get_safety_orders()!=[]:
self.status.set_next_so_price(self.status.get_safety_orders()[0]["price"]) 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_paused(self.pause)
self.status.set_is_short(self.config.get_is_short()) self.status.set_is_short(self.config.get_is_short())
self.status.set_no_of_safety_orders(self.config.get_no_of_safety_orders()) 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 "" 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"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"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") self.status.set_pause_reason("take_profit_routine - check time limit")
#Checks if there is a time limit for the trader #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 Sends a new safety order batch to the broker
:param amount: int - The amount of safety orders to send. :param amount: int - The amount of safety orders to send.
:return: The amount of orders succesfully sent. None if an error occurs. :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: if amount<1:
return 0 return 0
orders_to_place = min(self.config.get_no_of_safety_orders()-self.status.get_so_amount(),amount) orders_to_place = min(self.config.get_no_of_safety_orders()-self.status.get_so_amount(),amount)
if orders_to_place<1: if orders_to_place<1:
return 0 return 0
@ -888,12 +888,6 @@ class trader:
new_order_list.append(order) new_order_list.append(order)
self.status.set_safety_orders(new_order_list) 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 #Cancel old TP order
if self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.config.get_pair())==1: 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" 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"
@ -918,6 +912,15 @@ class trader:
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_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]) #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(len(filled_safety_orders))
#Cooldown #Cooldown
time.sleep(self.broker.get_wait_time()) time.sleep(self.broker.get_wait_time())
@ -1268,7 +1271,7 @@ class trader:
''' '''
try: try:
extra = f" and {round(base_profit,6)} {self.base}" if base_profit>0 else "" extra = f" and {round(base_profit,6)} {self.base}" if base_profit>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) self.broker.logger.send_tg_message(message)
return 0 return 0
except Exception as e: except Exception as e:
@ -1552,7 +1555,7 @@ class trader:
except Exception as e: except Exception as e:
print(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')}" 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())}" 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(): if self.status.get_is_boosted():