2025.08.14

This commit is contained in:
Nicolás Sánchez 2025-08-14 13:54:08 -03:00
parent 912bd77589
commit 4d23503cee
7 changed files with 116 additions and 129 deletions

View File

@ -1,3 +1,14 @@
2025.08.14:
. Refactored gib_so_size.
. Refactored seconds_to_time.
. Refactored linear_space.
. Refactored dca_cost_calculator.
. Refactored return_optimal_order_size.
. Minor refactor in generate_status_strings.
. Optimized imports.
. Deal_order_history now only stores the important parts of the orders to save some RAM.
. Removed deprecated "profit_to_file" method.
2025.08.12:
. Default "check_slippage" value now True.
. Removed capitalization from exchange name when sending trader quit notification.

View File

@ -1,5 +1,5 @@
import json
import time
from time import time
from json import dumps, load
class ConfigHandler:
'''
@ -41,7 +41,7 @@ class ConfigHandler:
#Loads from disk the config file (if it exists)
if self.load_from_file()==1:
#If the config file does not exist, write a new one with the default values and sign it with timestamp.
self.config_dictionary["generated_at"] = int(time.time())
self.config_dictionary["generated_at"] = int(time())
self.save_to_file()
if config_dict is not None:
self.config_dictionary = {**self.config_dictionary, **config_dict}
@ -315,7 +315,7 @@ class ConfigHandler:
# return 1
try:
with open(file_path, "w") as f:
f.write(json.dumps(self.config_dictionary, indent=4))
f.write(dumps(self.config_dictionary, indent=4))
return 0
except Exception as e:
self.broker.logger.log_this(f"Error saving config to file: {file_path}: {e}",1,self.get_pair())
@ -329,7 +329,7 @@ class ConfigHandler:
# return 1
try:
with open(file_path, "r") as f:
self.set_config({**self.default_config_dictionary, **json.load(f)})
self.set_config({**self.default_config_dictionary, **load(f)})
return 0
except Exception as e:
self.broker.logger.log_this(f"Config file does not exist or is not readable: {e}",1,self.get_pair())

View File

@ -1,8 +1,8 @@
import json
import time
import requests
import credentials
import sqlite3
from requests import get as requests_get
from json import load, dumps
from copy import deepcopy
@ -290,7 +290,7 @@ class Broker:
def reload_config_file(self):
try:
with open(self.config_filename) as f:
self.broker_config = json.load(f)
self.broker_config = load(f)
except Exception as e:
self.logger.log_this(f"Exception while reading the config file: {e}",1)
@ -340,9 +340,9 @@ class Broker:
try:
if backup:
with open(f"{self.exchange}.bak","w") as c:
c.write(json.dumps(self.broker_config, indent=4))
c.write(dumps(self.broker_config, indent=4))
with open(f"{self.config_filename}","w") as f:
f.write(json.dumps(self.broker_config, indent=4))
f.write(dumps(self.broker_config, indent=4))
return 0
except Exception as e:
self.logger.log_this(f"Problems writing the config file. Exception: {e}",1)
@ -1115,7 +1115,7 @@ class Logger:
send_text = f"https://api.telegram.org/bot{tg_credentials['token']}/sendMessage?chat_id={tg_credentials['chatid']}&parse_mode=Markdown&text={message}"
output = None
if self.broker_config["telegram"] or ignore_config:
output = requests.get(send_text,timeout=5).json() #5 seconds timeout. This could also be a tunable.
output = requests_get(send_text,timeout=5).json() #5 seconds timeout. This could also be a tunable.
if not output["ok"]:
self.log_this(f"Error in send_tg_message: {output}")
return 1

43
main.py
View File

@ -1,9 +1,9 @@
import datetime
import json
import os
import sys
import time
import logging
from sys import argv
from os import _exit as os_exit
from json import load
from datetime import date
from threading import Thread
from waitress import serve
@ -16,7 +16,7 @@ import exchange_wrapper
import trader
version = "2025.08.12"
version = "2025.08.14"
'''
Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
@ -42,14 +42,9 @@ def seconds_to_time(total_seconds: float) -> str:
str: The formatted string
'''
time_delta = datetime.timedelta(seconds=total_seconds)
hours = time_delta.seconds//3600
remainder = time_delta.seconds%3600
minutes = remainder//60
seconds = remainder%60
return f"{time_delta.days}:{hours:02d}:{minutes:02d}:{seconds:02d}"
days = int(total_seconds // 86400)
h, m, sec = int((total_seconds % 86400) // 3600), int((total_seconds % 3600) // 60), int(total_seconds % 60)
return f"{days}:{h:02d}:{m:02d}:{sec:02d}"
def time_to_unix(year: str, month: str, day: str) -> int:
@ -66,7 +61,7 @@ def time_to_unix(year: str, month: str, day: str) -> int:
'''
try:
return int(time.mktime(datetime.date(int(year), int(month), int(day)).timetuple()))
return int(time.mktime(date(int(year), int(month), int(day)).timetuple()))
except Exception as e:
broker.logger.log_this(f"{e}")
return 0
@ -1603,7 +1598,7 @@ def unwrapped_load_old_long(base,quote):
#Load the file
try:
with open(f"{base}{quote}.oldlong") as ol:
old_long = json.load(ol)
old_long = load(ol)
except Exception as e:
broker.logger.log_this(f"Exception while loading old_long file: {e}",1,f"{base}/{quote}")
return jsonify({"Error": "old_long file of that pair does not exist."})
@ -1640,7 +1635,7 @@ def unwrapped_view_old_long(base,quote,from_file):
try:
if int(from_file)==1:
with open(f"{base}{quote}.oldlong") as ol:
old_long = json.load(ol)
old_long = load(ol)
return jsonify(old_long)
for instance in running_traders:
if f"{base}/{quote}"==instance.config.get_pair():
@ -2407,16 +2402,16 @@ if __name__=="__main__":
#Loading config file
print(time.strftime("[%Y/%m/%d %H:%M:%S] | Loading config file..."))
try:
with open(sys.argv[1]) as f:
read_config = json.load(f)
with open(argv[1]) as f:
read_config = load(f)
except Exception as e:
print(e)
print("Wrong syntax. Correct syntax is 'python3 main.py xxxxx.json (--first_start)', xxxxx.json being the config file.")
os._exit(1)
os_exit(1)
#Check for import or load
import_mode = True
if "--first_start" in sys.argv:
if "--first_start" in argv:
import_mode = False
print(time.strftime("[%Y/%m/%d %H:%M:%S] | Initializing in FIRST START MODE, press enter to start..."))
else:
@ -2427,11 +2422,11 @@ if __name__=="__main__":
exchange = set_exchange(read_config)
if exchange is None:
print("Error initializing exchange. Check spelling and/or the exchange configuration file.")
os._exit(1)
os_exit(1)
#Creating the broker object
print(time.strftime(f"[%Y/%m/%d %H:%M:%S] | Connecting to {str(exchange)}..."))
broker = exchange_wrapper.Broker(exchange,read_config,sys.argv[1]) #Also passes the config filename
broker = exchange_wrapper.Broker(exchange,read_config,argv[1]) #Also passes the config filename
#Declaring some variables
running_traders = []
@ -2465,7 +2460,7 @@ if __name__=="__main__":
toggle = input(f"This will initialize {len(broker.get_pairs())} instances, proceed? (Y/n) ")
if toggle not in ["Y","y",""]:
broker.logger.log_this("Aborting initialization",2)
os._exit(1)
os_exit(1)
#broker.logger.log_this(f"Initializing {len(broker.get_pairs())} instances",2)
for x in broker.get_pairs():
symbol = broker.get_symbol(x)
@ -2477,7 +2472,7 @@ if __name__=="__main__":
toggle = input(f"This will import {len(broker.get_pairs())} instances, proceed? (Y/n) ")
if toggle not in ["Y","y",""]:
broker.logger.log_this("Aborting import",2)
os._exit(1)
os_exit(1)
#broker.logger.log_this(f"Importing {len(broker.get_pairs())} instances",2)
for x in broker.get_pairs():
symbol = broker.get_symbol(x)

View File

@ -1,5 +1,5 @@
import json
import time
from time import strftime
from json import dumps, load
class StatusHandler:
'''
@ -389,22 +389,40 @@ class StatusHandler:
def update_deal_order_history(self, new_deal: dict):
# if not isinstance(new_deal, dict):
# self.broker.logger.log_this(f"value provided is not a dict",1,self.get_pair())
self.status_dictionary["deal_order_history"].append(new_deal)
self.status_dictionary["deal_order_history"].append(self.strip_order(new_deal))
return 0
def strip_order(self, order):
try:
stripped_order = {"id": order["id"],
"symbol": order["symbol"],
"type": order["type"],
"side": order["side"],
"price": float(order["price"]),
"amount": float(order["amount"]),
"filled": float(order["filled"]),
"cost": float(order["cost"]),
"remaining": float(order["remaining"]),
"timestamp": order["timestamp"],
"fees": order["fees"]}
return stripped_order
except Exception as e:
self.broker.logger.log_this(f"Error stripping order: {e}",2)
return order
def save_to_file(self, file_path = None, is_backup = False):
if file_path is None:
file_path = self.status_file_path
if is_backup:
try:
with open(time.strftime(f"{file_path}_%Y-%m-%d_%H:%M:%S.json"), "w") as f:
f.write(json.dumps(self.status_dictionary, indent=4))
with open(strftime(f"{file_path}_%Y-%m-%d_%H:%M:%S.json"), "w") as f:
f.write(dumps(self.status_dictionary, indent=4))
except Exception as e:
self.broker.logger.log_this(f"Error creating status backup file: {e}",1)
try:
with open(file_path, "w") as f:
f.write(json.dumps(self.status_dictionary, indent=4))
f.write(dumps(self.status_dictionary, indent=4))
return 0
except Exception as e:
self.broker.logger.log_this(f"Error saving status to file: {file_path}: {e}",1)
@ -415,7 +433,7 @@ class StatusHandler:
file_path = self.status_file_path
try:
with open(file_path, "r") as f:
self.status_dictionary = {**self.default_status_dictionary, **json.load(f)}
self.status_dictionary = {**self.default_status_dictionary, **load(f)}
return 0
except Exception as e:
self.broker.logger.log_this(f"Error loading status from file: {file_path}: {e}",1)

View File

@ -12,6 +12,7 @@ Mandatory:
6. API documentation.
7. Implement api key hashing.
8. Dockerize.
9. Cache generated status strings, only recalculate when prices change.
Would be nice to have:

136
trader.py
View File

@ -1,7 +1,6 @@
import csv
import json
import time
import os
from os import path, remove
from json import dumps, load
from config_handler import ConfigHandler
from status_handler import StatusHandler
@ -22,9 +21,10 @@ class trader:
self.broker = broker
self.config = ConfigHandler(pair,broker)
self.base,self.quote = self.config.get_pair().split("/")
base_quote = self.config.get_pair()
self.base,self.quote = base_quote.split("/")
self.status = StatusHandler(broker, self.base, self.quote)
self.market = self.broker.fetch_market(self.config.get_pair())
self.market = self.broker.fetch_market(base_quote)
self.market_load_time = int(time.time())
self.market_reload_period = 86400 #Market reload period in seconds
self.status.set_start_time(int(time.time()))
@ -34,13 +34,14 @@ class trader:
#Check if there is an old_long file. If so, load it.
try:
with open(f"status/{self.base}{self.quote}.oldlong") as ol:
self.status.set_old_long(json.load(ol))
self.status.set_old_long(load(ol))
except Exception as e:
self.broker.logger.log_this(f"Exception: No old_long file. {e}",1,self.config.get_pair())
self.broker.logger.log_this(f"Exception: No old_long file. {e}",1,base_quote)
self.profit_filename = f"profits/{self.base}{self.quote}.profits"
self.log_filename = f"logs/{self.base}{self.quote}.log"
self.deals_timestamps = self.broker.get_trades_timestamps(self.config.get_pair(),self.config.get_boosted_time_range())
self.deals_timestamps = self.broker.get_trades_timestamps(base_quote,self.config.get_boosted_time_range())
self.status.set_pause_reason("Initialization")
if is_import:
@ -320,10 +321,10 @@ class trader:
:param scalar: float
:return: float
'''
total = order_size
for i in range(1,amount_of_so+1):
total+=self.gib_so_size(order_size,i,scalar)
return total
r = scalar * 100
if abs(r - 1.0) < 1e-6:
return order_size * (amount_of_so + 1)
return order_size * (r * (r**amount_of_so - 1) / (r - 1)) + order_size
def base_add_calculation(self, base_currency_amount: float, max_so: int = 100):
@ -358,35 +359,17 @@ class trader:
:param scalar: float
:return: float
'''
total_size = float(min_size)
#Calculate optimal step size
self.broker.logger.log_this("Calculating optimal step size...",2,self.config.get_pair())
#step = self.get_step_size()
#if step is None:
# step = min_size
#if step==0:
# step = min_size
#self.broker.logger.log_this(f"Step size is {step}",2,self.config.get_pair())
divisor = 10
while divisor>0:
#step = self.broker.amount_to_precision(self.config.get_pair(),min_size/divisor)
step = min_size/divisor
if step!=0: #When using amount_to_precision, this comes handy.
break
divisor-=1
#if step==0:
# step = self.broker.amount_to_precision(self.config.get_pair(),min_size)
previous_size = 0
self.broker.logger.log_this(f"Calculating optimal order size ...",2,self.config.get_pair())
while True: #This loop should have a safeguard
total_cost = self.dca_cost_calculator(total_size,amount_of_safety_orders,scalar)
if total_cost>=amount:
return previous_size
previous_size = total_size
total_size+=step
low, high = min_size, amount
best = 0.0
while high - low > min_size / 10:
mid = (low + high) / 2
cost = self.dca_cost_calculator(mid, amount_of_safety_orders, scalar)
if cost <= amount:
best = mid
low = mid
else:
high = mid
return best
def parse_fees(self, order: dict) -> tuple:
@ -570,7 +553,7 @@ class trader:
})
try:
with open(f"status/{self.base}{self.quote}.oldlong","w") as s:
s.write(json.dumps(self.status.get_old_long(),indent=4))
s.write(dumps(self.status.get_old_long(),indent=4))
except Exception as e:
self.broker.logger.log_this(f"Exception while saving old_long file: {e}",1,self.config.get_pair())
@ -607,7 +590,7 @@ class trader:
self.broker.logger.log_this("Can't find old long info on status_dict, searching for oldlong file",1,self.config.get_pair())
try:
with open(f"status/{self.base}{self.quote}.oldlong") as f:
self.status.set_old_long(json.load(f))
self.status.set_old_long(load(f))
except Exception as e:
#self.write_to_log(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.config.get_pair()} | Can't find old long file"))
self.broker.logger.log_this(f"Can't file oldlong file. Exception: {e}",1,self.config.get_pair())
@ -638,11 +621,11 @@ class trader:
return 1
#Rewrite config file (if it exists)
if os.path.isfile(f"configs/{self.base}{self.quote}.bak") and os.path.isfile(f"configs/{self.base}{self.quote}.json"):
if path.isfile(f"configs/{self.base}{self.quote}.bak") and path.isfile(f"configs/{self.base}{self.quote}.json"):
with open(f"configs/{self.base}{self.quote}.bak") as c:
old_config = json.load(c)
old_config = load(c)
with open(f"configs/{self.base}{self.quote}.json","w") as c:
c.write(json.dumps(old_config, indent=4))
c.write(dumps(old_config, indent=4))
if self.config.load_from_file()==1:
self.config.reset_to_default()
else:
@ -651,9 +634,9 @@ class trader:
self.config.save_to_file()
#Remove old_long file (if it exists)
if os.path.isfile(f"status/{self.base}{self.quote}.oldlong"):
if path.isfile(f"status/{self.base}{self.quote}.oldlong"):
self.broker.logger.log_this("Removing old_long file...",2,self.config.get_pair())
os.remove(f"status/{self.base}{self.quote}.oldlong")
remove(f"status/{self.base}{self.quote}.oldlong")
#Set up a few variables
self.status.set_fees_paid_in_quote(0)
@ -697,7 +680,6 @@ class trader:
profit = already_received_quote + market_tp_order["cost"] - self.status.get_old_long()["quote_spent"] - self.status.get_old_long()["fees_paid_in_quote"] - fees_paid
#Add profits to file and send telegram notifying profits
self.profit_to_file(profit,market_tp_order["id"])
self.profit_to_db(profit,market_tp_order["id"],self.broker.get_write_order_history())
self.broker.logger.log_this(f"Switch successful. Profit: {round(profit,2)} {self.quote}",0,self.config.get_pair())
self.broker.logger.log_this(f"Sell price: {market_tp_order['price']} {self.quote}",0,self.config.get_pair())
@ -769,7 +751,6 @@ class trader:
# Write the profit to file and send telegram message
if profit>0: #Negative profits are not saved because the cleanup takes care of the unsold base currency (the notorious small change issue that plagues some exchanges)
self.profit_to_file(profit,filled_order["id"])
self.profit_to_db(profit,filled_order["id"],self.broker.get_write_order_history())
else: #For logging purposes
self.broker.logger.log_this(f"NEGATIVE PROFIT - Total amount of base: {self.status.get_base_bought()}, base in the order: {filled_order['amount']}, base filled: {filled_order['filled']}, base 'profit': {base_profit}",1,self.config.get_pair())
@ -1170,7 +1151,9 @@ class trader:
'''
Returns a D:HH:MM:SS representation of total_seconds
'''
return f"{int(total_seconds / 86400)}:" + '%02d:%02d:%02d' % (int(total_seconds % 86400 / 3600), int(total_seconds % 3600 / 60), int(total_seconds % 60))
days = int(total_seconds // 86400)
h, m, sec = int((total_seconds % 86400) // 3600), int((total_seconds % 3600) // 60), int(total_seconds % 60)
return f"{days}:{h:02d}:{m:02d}:{sec:02d}"
def adjust_base(self):
@ -1212,28 +1195,14 @@ class trader:
return 1
def profit_to_file(self, amount: float, orderid: str) -> int:
'''
Saves the profit to the corresponding profit file
DEPRECATED. Use profit_to_db instead.
'''
try:
with open(self.profit_filename,"a") as profit_file:
profit_writer = csv.writer(profit_file, delimiter=",")
profit_writer.writerow([time.strftime("%Y-%m-%d"), amount, orderid])
except Exception as e:
self.broker.logger.log_this(f"Exception in profit_to_file: {e}",1,self.config.get_pair())
return 0
def profit_to_db(self, amount: float, orderid: str, write_deal_order_history: bool = False) -> int:
'''
Saves the profit to the db in the format (pair,timestamp,profit,exchange_name,order_id,order_history)
'''
retries = self.broker.get_retries()
retries = 5 #Hardcoded because it's not an API call
while retries>0:
try:
order_history = json.dumps(self.status.get_deal_order_history()) if write_deal_order_history else ""
order_history = dumps(self.status.get_deal_order_history()) if write_deal_order_history else ""
dataset = (time.time(),self.config.get_pair(),amount,self.broker.get_exchange_name(),str(orderid),order_history)
#Write profit to cache
self.broker.write_profit_to_cache(dataset)
@ -1241,7 +1210,7 @@ class trader:
except Exception as e:
self.broker.logger.log_this(f"Exception while writing profit: {e}",1,self.config.get_pair())
retries-=1
time.sleep(self.broker.get_wait_time())
time.sleep(.1) #Shorter wait time since it's not an API call
return 1
@ -1286,10 +1255,7 @@ class trader:
Returns the correct safety order size depending on the number
Scaling factor example: 5% = 0.0105
'''
order_size = starting_order_size
for _ in range(so_number):
order_size = order_size*scaling_factor*100
return order_size
return starting_order_size * (scaling_factor*100)**so_number
def clip_value(self,value,lower_limit,upper_limit):
@ -1334,13 +1300,9 @@ class trader:
- This is the only piece of code needed from Numpy
- Only executed when calculating the safety order table, so there's no need for outstanding performance.
'''
result = [start]
if amount in [0,1]:
return result
step = (start-stop)/(amount-1)
for _ in range(1,amount):
result.append(result[-1]-step)
return result
step = (stop - start) / (amount - 1)
return [start + i * step for i in range(amount)]
def switch_quote_currency(self, new_quote: str) -> int:
@ -1426,7 +1388,7 @@ class trader:
if self.config.get_is_short() and self.status.get_old_long()!={}:
try:
with open(f"status/{self.base}{self.quote}.oldlong","w") as c:
c.write(json.dumps(self.status.get_old_long(), indent=4))
c.write(dumps(self.status.get_old_long(), indent=4))
except Exception as e:
self.broker.logger.log_this(f"Exception while writing new old_long file: {e}",1,self.config.get_pair())
@ -1512,9 +1474,9 @@ class trader:
percentage_to_profit = 100
pct_to_profit_str = "XX.XX"
if self.status.get_price()!=0:
diff = abs(self.status.get_take_profit_price()-self.status.get_price())
percentage_to_profit = diff/self.status.get_price()*100
if mid_price!=0:
diff = abs(high_price-mid_price)
percentage_to_profit = diff/mid_price*100
#Formatting (on-screen percentage not longer than 4 digits)
pct_to_profit_str = "{:.2f}".format(percentage_to_profit)
@ -1525,7 +1487,7 @@ class trader:
line3 = ""
if self.status.get_base_bought()!=0:
line3 = draw_line(self.status.get_price(),self.status.get_next_so_price(),self.status.get_take_profit_price(),self.status.get_quote_spent()/self.status.get_base_bought())
line3 = draw_line(mid_price,low_price,high_price,self.status.get_quote_spent()/self.status.get_base_bought())
p = "*PAUSED*" if self.pause==True else ""
low_boundary_color = red
price_color = white
@ -1535,9 +1497,9 @@ class trader:
price_color = white
pair_color = yellow
if self.status.get_old_long()!={}:
if self.status.get_price()>self.status.get_old_long()["tp_price"]:
if mid_price>self.status.get_old_long()["tp_price"]:
price_color = bright_green
if self.status.get_take_profit_price()>self.status.get_old_long()["tp_price"]:
if high_price>self.status.get_old_long()["tp_price"]:
target_price_color = bright_green
#Set percentage's color
@ -1559,7 +1521,7 @@ class trader:
if old_target-self.status.get_quote_spent()>0 and base_left>0 and minimum_switch_price<low_price:
low_boundary_color = bright_green
low_boundary = '{:.20f}'.format(minimum_switch_price)[:decimals].center(decimals)
if self.status.get_price()!=0:
if mid_price!=0:
multiplier = int(self.status.get_old_long()["tp_price"]/self.status.get_price())
except Exception as e:
print(e)