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(),
"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):

View File

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

View File

@ -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))

View File

@ -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:

View File

@ -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())<max_orders:
self.send_new_safety_order_batch(len(filled_safety_orders))
#Cooldown
time.sleep(self.broker.get_wait_time())
#Send new TP order
if self.send_new_tp_order()==1:
@ -1268,7 +1271,7 @@ class trader:
'''
try:
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)
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():