- Fixed tp mode 2 non-functional

- Fixed duster binance fee estimation
- Fixed executor variable shadowing breaking graceful shutdown
- Fixed infinite loop in cancel_order
- Fixed modifying running_traders during iteration
- Fixed missing "status/" prefix in old_long file paths
- Removed double TP order cancellation while switching to short.
- Added locks to prevent race conditions on running_traders.
This commit is contained in:
Nicolás Sánchez 2026-06-03 18:43:40 -03:00
parent 12999a2189
commit ffe58e2c0d
5 changed files with 389 additions and 317 deletions

View File

@ -1,3 +1,13 @@
2026.06.03:
. Fixed tp mode 2 non-functional
. Fixed duster binance fee estimation
. Fixed executor variable shadowing breaking graceful shutdown
. Fixed infinite loop in cancel_order
. Fixed modifying running_traders during iteration
. Fixed missing "status/" prefix in old_long file paths
. Removed double TP order cancellation while switching to short.
. Added locks to prevent race conditions on running_traders.
2025.12.01:
. Modified log output of new_market_order.
. Modified Kucoin's case in min_amount_of_base.

View File

@ -189,7 +189,8 @@ class duster:
if self.broker.get_exchange_name()=="binance": #CCXT still to this day does not take Binance fees into account.
try:
fee_rate = self.broker.fetch_market["maker"] if order["type"]=="limit" else self.broker.fetch_market["taker"]
market = self.broker.fetch_maker(self.duster_status["pair"])
fee_rate = market["maker"] if order["type"]=="limit" else market["taker"]
except Exception as e:
self.broker.logger.log_this(f"Exception fetching market information: {e}. Using default fee rate of 0.1%",1,f"{base}{quote}")
fee_rate = 0.001

View File

@ -622,12 +622,12 @@ class Broker:
:return: 0 if order was succesfully canceled, 1 if not
'''
tries = self.retries//2
while tries>0:
cancel_attempts = self.retries//2
while cancel_attempts > 0:
try:
while self.get_order(id,symbol)["status"]=="open":
self.exchange.cancel_order(id, symbol)
time.sleep(self.wait_time)
if self.get_order(id, symbol)["status"] != "open":
return 0
except Exception as e:
if self.get_order(id, symbol)["status"] == "canceled":
@ -635,11 +635,29 @@ class Broker:
self.logger.log_this(f"Exception in cancel_order: id {id} - exception: {e}",1)
if no_retries:
break
cancel_attempts -= 1
time.sleep(self.wait_time)
tries-=1
return 1
# tries = self.retries//2
# while tries>0:
# try:
# while self.get_order(id,symbol)["status"]=="open":
# self.exchange.cancel_order(id,symbol)
# time.sleep(self.wait_time)
# return 0
# except Exception as e:
# if self.get_order(id,symbol)["status"]=="canceled":
# return 0
# self.logger.log_this(f"Exception in cancel_order: id {id} - exception: {e}",1)
# if no_retries:
# break
# time.sleep(self.wait_time)
# tries-=1
# return 1
def amount_to_precision(self,pair,amount):
try:
return float(self.exchange.amount_to_precision(pair,amount))

64
main.py
View File

@ -5,7 +5,7 @@ from sys import argv
from os import _exit as os_exit
from json import load
from datetime import date
from threading import Thread
from threading import Thread, Lock
from waitress import serve
from concurrent.futures import ThreadPoolExecutor, as_completed
@ -18,7 +18,7 @@ import exchange_wrapper
import trader
version = "2025.12.01"
version = "2026.06.03"
'''
Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
@ -43,7 +43,7 @@ executor = None
def shutdown_handler(signum, _):
broker.logger.log_this(f"Received signal {signum}, shutting down.", 2)
if executor:
executor.shutdown(wait=True, timeout=5)
executor.shutdown(wait=True)
os_exit(0)
# Register signals for shutdown handler
@ -119,6 +119,7 @@ def add_instance(base: str, quote: str) -> int:
#Check if the pair is already running
pair = f"{base}{quote}"
with traders_lock:
for instance in running_traders:
if f"{instance.base}{instance.quote}"==pair:
broker.logger.log_this(f"Pair already running, duplicate traders are not allowed",1,pair)
@ -143,6 +144,7 @@ def initialize_instance(base: str, quote: str) -> int:
int: 0 if successful
'''
broker.logger.log_this(f"Initializing {f'{base}/{quote}'}")
with traders_lock:
running_traders.append(trader.trader(broker,f'{base}/{quote}'))
if f'{base}{quote}' not in tickers:
tickers.append(f'{base}{quote}')
@ -246,6 +248,7 @@ def restart_pair_no_json(base: str, quote: str) -> int:
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.set_pause(True, "Restarting trader")
@ -282,6 +285,7 @@ def main_routine():
global last_market_reload
global reload_interval
global screen_buffer
global executor
executor = ThreadPoolExecutor(max_workers=len(broker.get_config()["pairs"])+worker_threads_overprovisioning)
is_testnet = "TESTNET " if broker.get_config()["is_sandbox"] else ""
@ -290,11 +294,18 @@ def main_routine():
while True:
#Restart traders that have the restart flag raised and remove traders that have the quit flag raised
to_restart = []
to_remove = []
with traders_lock:
for instance in running_traders:
if instance.restart and instance.config.get_attempt_restart():
to_restart.append(instance)
if instance.quit:
to_remove.append(instance)
for instance in to_restart:
broker.logger.log_this(f"Restarting trader",1,instance.status.get_pair())
restart_pair_no_json(instance.base,instance.quote)
if instance.quit:
for instance in to_remove:
#Here, check if a duster is needed
broker.logger.log_this(f"{broker.get_exchange_name()} | Quit flag raised, removing trader.",0,instance.status.get_pair())
broker.logger.log_this(f"{broker.get_exchange_name()} | Quit flag raised, removing trader: {instance.status.get_pair()}",-1) #Forced message to TG
@ -309,6 +320,7 @@ def main_routine():
#Adds pending traders
if bool(instances_to_add):
with traders_lock:
for instance in instances_to_add:
running_traders.append(instance)
instances_to_add.clear()
@ -317,11 +329,12 @@ def main_routine():
futures = []
pairs_to_fetch = []
online_pairs = []
with traders_lock:
for instance in running_traders:
pairs_to_fetch.append(instance.status.get_pair())
open_orders = broker.fetch_open_orders(pairs_to_fetch)
with traders_lock:
for instance in running_traders:
future = executor.submit(instance.check_status, open_orders)
futures.append(future)
@ -343,6 +356,8 @@ def main_routine():
short_traders_status_strings = []
paused_traders_status_strings = []
global_status["paused_traders"].clear()
with traders_lock:
for instance in running_traders:
if not instance.config.get_is_short():
curr += int(instance.status.get_so_amount()) # For the safety order occupancy percentage calculation
@ -374,6 +389,7 @@ def main_routine():
#Updates some global status variables prior to deletion of those
if len(running_traders)!=len(global_status["online_workers"]):
with traders_lock:
global_status["online_workers"] = [instance.status.get_pair() for instance in running_traders]
#Prints general info
@ -409,6 +425,7 @@ def main_routine():
#Toggle pauses
if toggle_pauses:
with traders_lock:
for instance in running_traders:
if instance.status.get_pair() in toggle_pauses:
instance.pause = not instance.pause
@ -1505,6 +1522,7 @@ def unwrapped_return_worker_status(base,quote):
dict: The status dictionary of the trader.
'''
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if instance.status.get_pair() == symbol:
return jsonify(instance.status.get_status())
@ -1518,7 +1536,7 @@ def unwrapped_return_all_worker_status():
Returns:
dict: The status dictionary of all traders.
'''
with traders_lock:
return {instance.status.get_pair(): instance.status.get_status() for instance in running_traders}
@ -1538,6 +1556,7 @@ def unwrapped_add_pair(base,quote):
symbol = f"{base}/{quote}"
#Check if the trader is already running
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
broker.logger.log_this(f"Pair already running",1,symbol)
@ -1572,6 +1591,7 @@ def unwrapped_remove_pair(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.quit = True
@ -1639,6 +1659,7 @@ def unwrapped_switch_to_long(base,quote,calculate_profits):
#Close trader and orders and pull info our of the orders
if f"{base}{quote}" not in broker.get_pairs():
return jsonify({"Error": "Pair not running"})
with traders_lock:
for instance in running_traders:
if f"{base}/{quote}"==instance.status.get_pair():
instance.set_pause(True, "Switching to long mode")
@ -1667,6 +1688,7 @@ def unwrapped_switch_to_short(base,quote):
symbol = f"{base}/{quote}"
if f"{base}{quote}" not in broker.get_pairs():
return jsonify({"Error": "Pair not running"})
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair() and instance.switch_to_short()==1:
return jsonify({"Error": "Error in switch_to_short()"})
@ -1674,6 +1696,7 @@ def unwrapped_switch_to_short(base,quote):
#Restart instance
try:
broker.logger.log_this(f"Reinitializing trader",2,symbol)
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.status.set_take_profit_order(instance.broker.empty_order)
@ -1709,7 +1732,7 @@ def unwrapped_load_old_long(base,quote):
#Load the file
try:
symbol = f"{base}/{quote}"
with open(f"{base}{quote}.oldlong") as ol:
with open(f"status/{base}{quote}.oldlong") as ol:
old_long = load(ol)
except Exception as e:
broker.logger.log_this(f"Exception while loading old_long file: {e}",1,symbol)
@ -1723,6 +1746,7 @@ def unwrapped_load_old_long(base,quote):
#Maybe here we could also check that the keys have the proper values
#Creates (or modifies) a key in the status dictionary and assigns the contents of the file to that same key.
with traders_lock:
for instance in running_traders:
if instance.status.get_pair()==symbol:
instance.get_status_dict()["old_long"]=old_long
@ -1747,9 +1771,10 @@ def unwrapped_view_old_long(base,quote,from_file):
try:
symbol = f"{base}/{quote}"
if int(from_file)==1:
with open(f"{base}{quote}.oldlong") as ol:
with open(f"status/{base}{quote}.oldlong") as ol:
old_long = load(ol)
return jsonify(old_long)
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
if "old_long" in instance.get_status_dict():
@ -1776,6 +1801,7 @@ def unwrapped_switch_to_long_price(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
if "old_long" in instance.get_status_dict():
@ -1807,6 +1833,7 @@ def unwrapped_add_safety_orders(base,quote,amount):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.set_pause(True, "Adding safety orders")
@ -1837,6 +1864,7 @@ def unwrapped_base_add_so_calculation(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
free_base = instance.fetch_free_base()
@ -1865,6 +1893,7 @@ def unwrapped_mod_tp_level(base,quote,amount):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_tp_level(float(amount))
@ -1890,6 +1919,7 @@ def unwrapped_mod_order_size(base,quote,amount):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_order_size(float(amount))
@ -1916,6 +1946,7 @@ def unwrapped_mod_concurrent_safety_orders(base,quote,amount):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_concurrent_safety_orders(int(amount))
@ -1942,6 +1973,7 @@ def unwrapped_mod_boosted_concurrent_safety_orders(base,quote,amount):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_boosted_concurrent_safety_orders(int(amount))
@ -1983,7 +2015,7 @@ def unwrapped_mod_global_tp_level(amount):
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
with traders_lock:
for instance in running_traders:
try:
instance.config.set_tp_level(float(amount))
@ -2007,6 +2039,7 @@ def unwrapped_last_call(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.status.set_stop_when_profit(not instance.status.get_stop_when_profit())
@ -2041,6 +2074,7 @@ def unwrapped_deferred_last_call(base,quote,yyyymmdd):
limit = time_to_unix(year,month,day)
if limit==0:
return jsonify({"Error": "Can't convert date to unix"})
with traders_lock:
for instance in running_traders:
if f"{base}{quote}"==instance.status.get_pair():
instance.config.set_programmed_stop_time(limit)
@ -2068,6 +2102,7 @@ def unwrapped_toggle_pause(base,quote):
try:
symbol = f"{base}/{quote}"
toggle_pauses.append(symbol)
with traders_lock:
for instance in running_traders:
if instance.status.get_pair()==symbol:
if instance.pause:
@ -2088,6 +2123,7 @@ def unwrapped_global_last_call():
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
with traders_lock:
for instance in running_traders:
instance.status.set_stop_when_profit(True)
instance.config.set_autoswitch(False)
@ -2105,6 +2141,7 @@ def unwrapped_cancel_global_last_call():
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
with traders_lock:
for instance in running_traders:
instance.status.set_stop_when_profit(False)
broker.logger.log_this("Modified flag",2,f"{instance.base}/{instance.quote}")
@ -2126,7 +2163,7 @@ def unwrapped_add_quote(base,quote,amount):
Returns:
json: A jsonified dictionary detailing the outcome of the operation.
'''
with traders_lock:
for instance in running_traders:
if f"{base}/{quote}"==instance.status.get_pair():
if instance.config.get_is_short():
@ -2213,6 +2250,7 @@ def unwrapped_toggle_cleanup(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_cleanup(not instance.config.get_cleanup())
@ -2239,6 +2277,7 @@ def unwrapped_force_trader_close(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
outcome = instance.force_close()
@ -2265,6 +2304,7 @@ def unwrapped_toggle_autoswitch(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
if instance.config.get_autoswitch():
@ -2295,6 +2335,7 @@ def unwrapped_toggle_liquidate_after_switch(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
if instance.config.get_liquidate_after_switch():
@ -2325,6 +2366,7 @@ def unwrapped_toggle_check_old_long_price(base,quote):
try:
symbol = f"{base}/{quote}"
with traders_lock:
for instance in running_traders:
if symbol==instance.status.get_pair():
if instance.config.get_check_old_long_price():
@ -2439,6 +2481,7 @@ def unwrapped_trader_time():
'''
try:
with traders_lock:
return jsonify({"Time": max(instance.last_time_seen for instance in running_traders)})
except Exception as e:
broker.logger.log_this(f"Exception while retrieving trader_time: {e}",1)
@ -2634,6 +2677,7 @@ if __name__=="__main__":
broker = exchange_wrapper.Broker(exchange,read_config,argv[1]) #Also passes the config filename
#Declaring some variables
traders_lock = Lock()
running_traders = []
instances_to_add = []
online_pairs = []

View File

@ -560,8 +560,6 @@ class trader:
self.broker.logger.log_this("Can't cancel the take profit order. Can't switch mode",1,self.status.get_pair())
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())
#Save the old take profit order info for later use
self.broker.logger.log_this("Saving state in status_dict",2,self.status.get_pair())
@ -1307,6 +1305,7 @@ class trader:
if len(self.config.get_tp_table())>=order_index:
tp_level = self.config.get_tp_table()[order_index] #Custom percentage table
tp_level = self.config.get_tp_table()[-1]
else:
tp_level = self.config.get_tp_level()
elif self.config.get_tp_mode()==3: #Linear percentage table
profit_table = self.linear_space(self.config.get_tp_level()+0.005,self.config.get_tp_level()-0.005,self.status.get_no_of_safety_orders())