DCAv2/main.py

2595 lines
90 KiB
Python

import time
import logging
import signal
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
from concurrent.futures import ThreadPoolExecutor, as_completed
import sqlite3
import ccxt
from flask import Flask, jsonify, request
import exchange_wrapper
import trader
version = "2025.08.30"
'''
Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
'''
yellow = "\033[0;33;40m"
green = "\033[0;32;40m"
red = "\033[0;31;40m"
blue = "\033[0;34;40m"
cyan = "\033[0;36;40m"
bright_white = "\033[0;97;40m"
bright_green = "\033[0;92;40m"
white = "\033[0;37;40m"
#Threading definitions
worker_threads_overprovisioning = 3 #Number of worker threads to create over the number of traders.
#A value between 1 and 5 is recommended.
#Make it larger if you plan to add a lot of traders,
#Only use 0 if you are sure that you won't be adding any.
executor = None
#Shutdown handler
def shutdown_handler(signum, _):
broker.logger.log_this(f"Received signal {signum}, shutting down as gracefully as possible...", 2)
if executor:
executor.shutdown(wait=True, timeout=5)
os_exit(0)
# Register signals for shutdown handler
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGTERM, shutdown_handler)
def seconds_to_time(total_seconds: float) -> str:
'''
Takes an int or float as an input and it returns a D:HH:MM:SS formatted string.
Parameters:
total_seconds (float): The number of seconds to convert
Returns:
str: The formatted string
'''
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:
'''
Takes three integer values as an input and returns the unix time corresponding to that input
Parameters:
year (str): The year to convert
month (str): The month to convert
day (str): The day to convert
Returns:
int: The unix time corresponding to the input
'''
try:
return int(time.mktime(date(int(year), int(month), int(day)).timetuple()))
except Exception as e:
broker.logger.log_this(f"{e}")
return 0
def import_instance(base: str, quote: str) -> int:
'''
Imports an previously running trader instance from the status file.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
int: 0 if successful
'''
broker.logger.log_this(f"Importing {base}/{quote}")
instances_to_add.append(trader.trader(broker,f"{base}/{quote}",is_import=True))
if f"{base}{quote}" not in tickers:
tickers.append(f"{base}{quote}")
return 0
def add_instance(base: str, quote: str) -> int:
'''
Adds a new instance of the trader class to the running_traders list.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
int: 0 if the instance was successfully added, 1 if the pair is already running.
'''
#Check if the pair is already running
pair = f"{base}{quote}"
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)
return 1
#Initialize the trader object and add the pair to the tickers list
instances_to_add.append(trader.trader(broker,f"{base}/{quote}"))
if pair not in tickers:
tickers.append(pair)
return 0
def initialize_instance(base: str, quote: str) -> int:
'''
Loads the pair config file and initializes the trader object by adding it to the running instances list
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
int: 0 if successful
'''
broker.logger.log_this(f"Initializing {f'{base}/{quote}'}")
running_traders.append(trader.trader(broker,f'{base}/{quote}'))
if f'{base}{quote}' not in tickers:
tickers.append(f'{base}{quote}')
return 0
def set_exchange(config: dict):
'''
Takes the config dictionary as an input and returns the exchange object
Parameters
config (dict): The config dictionary
Returns
exchange (ccxt.Exchange): The exchange object
'''
timeout = 10000
if config["exchange"]=="binance":
exchange_class = getattr(ccxt, "binance")
exchange = exchange_class({
"apiKey": config["key"],
"secret": config["secret"],
"timeout": timeout,
"enableRateLimit": True
})
exchange.options["warnOnFetchOpenOrdersWithoutSymbol"] = False
if config["is_sandbox"]:
exchange.set_sandbox_mode(True)
elif config["exchange"]=="kucoin":
exchange_class = getattr(ccxt, "kucoin")
exchange = exchange_class({
"apiKey": config["key"],
"secret": config["secret"],
"password": config["password"],
"timeout": timeout,
"enableRateLimit": True
})
elif config["exchange"]=="okex":
exchange_class = getattr(ccxt, "okx")
exchange = exchange_class({
"apiKey": config["key"],
"secret": config["secret"],
"password": config["password"],
"timeout": timeout,
"enableRateLimit": True
})
elif config["exchange"]=="gateio":
exchange_class = getattr(ccxt, "gateio")
exchange = exchange_class({
"apiKey": config["key"],
"secret": config["secret"],
"timeout": timeout,
"enableRateLimit": True
})
elif config["exchange"]=="hitbtc":
exchange_class = getattr(ccxt, "hitbtc")
exchange = exchange_class({
"apiKey": config["key"],
"secret": config["secret"],
"timeout": timeout,
"enableRateLimit": True
})
elif config["exchange"]=="poloniex":
exchange_class = getattr(ccxt, "poloniex")
exchange = exchange_class({
"apiKey": config["key"],
"secret": config["secret"],
"timeout": timeout,
"enableRateLimit": True
})
elif config["exchange"]=="bybit":
exchange_class = getattr(ccxt, "bybit")
exchange = exchange_class({
"apiKey": config["key"],
"secret": config["secret"],
"timeout": timeout,
"enableRateLimit": True
})
exchange.options["acknowledged"] = True
if config["is_sandbox"]:
exchange.set_sandbox_mode(True)
else:
print(f"{time.strftime('[%Y/%m/%d %H:%M:%S]')} | Exchange not known or misspelled")
return None
exchange.options['createMarketBuyOrderRequiresPrice'] = False
return exchange
def restart_pair_no_json(base: str, quote: str) -> int:
'''
Trader restart routine
Parameters:
base (str): Base currency
quote (str): Quote currency
Returns:
int: 0 if successful, 1 if not
'''
try:
symbol = f"{base}/{quote}"
order_list = broker.fetch_full_orders(tickers)
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.pause = True
#Backing up old status file
instance.status.save_to_file(is_backup=True)
#Here, we could open a duster (if needed)
for order in order_list:
if order["symbol"]==symbol and instance.config.get_is_short() and order["side"]=="sell":
broker.logger.log_this(f"Cancelling old sell orders",2,symbol)
broker.cancel_order(order["id"],order["symbol"])
elif order["symbol"]==symbol and not instance.config.get_is_short() and order["side"]=="buy":
broker.logger.log_this(f"Cancelling old buy orders",2,symbol)
broker.cancel_order(order["id"],order["symbol"])
try:
running_traders.remove(instance)
except ValueError:
broker.logger.log_this(f"Instance {instance.status.get_pair()} not found in running_traders.",1,instance.status.get_pair())
add_instance(base,quote)
return 0
return 1
except Exception as e:
broker.logger.log_this(f"Exception in restart_pair_no_json: {e}",1,symbol)
return 1
def main_routine():
global last_market_reload
global reload_interval
global screen_buffer
executor = ThreadPoolExecutor(max_workers=len(running_traders)+worker_threads_overprovisioning)
is_testnet = "TESTNET " if broker.get_config()["is_sandbox"] else ""
exchange_version_label = f"{bright_white}{broker.get_config()['exchange'].upper()} {is_testnet}{white}| DCAv2 {version} | CCXT v{ccxt.__version__}"
separator_line = blue + "="*80 + white
while True:
#Restart traders that have the restart flag raised and remove traders that have the quit flag raised
for instance in running_traders:
if instance.restart and instance.config.get_attempt_restart():
broker.logger.log_this(f"Restarting trader",1,instance.status.get_pair())
restart_pair_no_json(instance.base,instance.quote)
if instance.quit:
#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
if f"{instance.base}{instance.quote}" in tickers:
tickers.remove(f"{instance.base}{instance.quote}")
broker.remove_pair_from_config(f"{instance.base}{instance.quote}")
broker.rewrite_config_file()
try:
running_traders.remove(instance)
except ValueError:
broker.logger.log_this(f"Instance {instance.status.get_pair()} not found in running_traders.",1,instance.status.get_pair())
#Adds pending traders
if bool(instances_to_add):
for instance in instances_to_add:
running_traders.append(instance)
instances_to_add.clear()
#Prepares the trader threads
futures = []
pairs_to_fetch = []
online_pairs = []
open_orders = broker.fetch_open_orders(tickers)
for instance in running_traders:
future = executor.submit(instance.check_status, open_orders)
futures.append(future)
online_pairs.append(f"{instance.base}{instance.quote}")
pairs_to_fetch.append(instance.status.get_pair())
#Delete no longer used data
del open_orders
#Fetch prices
price_list = broker.get_prices(pairs_to_fetch)
#Here, assign the prices to the dusters (if any)
for future in as_completed(futures):
try:
future.result()
except Exception as e:
broker.logger.log_this(f"Error in thread - {e}")
curr = 0
top = 0
long_traders_status_strings = []
short_traders_status_strings = []
paused_traders_status_strings = []
global_status["paused_traders"].clear()
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
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))
try:
if instance.status.get_pair() in price_list and price_list[instance.status.get_pair()] is not None:
instance.get_status_dict()["price"] = price_list[instance.status.get_pair()]
except Exception as e:
broker.logger.log_this(f"Exception while querying for pair price, key not present on price_list dictionary: {e}",1,instance.status.get_pair())
#Add paused traders to the paused trader list
if instance.pause:
global_status["paused_traders"].append(instance.status.get_pair())
paused_traders_status_strings.append(f"{cyan}Paused pairs: {list(global_status['paused_traders'])}{white}")
#Delete no longer used data
del price_list
#Append worker data to screen buffer, shorts first.
screen_buffer.extend(short_traders_status_strings + long_traders_status_strings)
#Appends paused pairs to the screen buffer
if paused_traders_status_strings!=[]:
screen_buffer.extend(paused_traders_status_strings)
#Updates some global status variables prior to deletion of those
if len(running_traders)!=len(global_status["online_workers"]):
global_status["online_workers"] = [instance.status.get_pair() for instance in running_traders]
#Prints general info
instance_uptime = int(time.time()) - instance_start_time
screen_buffer.append(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {len(running_traders)} traders online | Instance uptime: {seconds_to_time(instance_uptime)}"))
if top==0: #To protect from division by zero when there are no traders active
so_index=100
else:
so_index = round(curr/top*100,2)
color = red
if so_index<70:
color = yellow
if so_index<35:
color = green
screen_buffer.append(f"{exchange_version_label} | Safety order occupancy: {color}{so_index}%{white}")
screen_buffer.append(separator_line)
#Print screen buffer
for line in screen_buffer:
print(line)
#Clear the screen buffer
screen_buffer.clear()
#Updates global status and remove keys that should not be public
global_status["instance_uptime"] = instance_uptime
global_status["config"] = broker.get_config()
for item in ["bot_chatID", "bot_token", "key", "secret", "password"]:
if item in global_status["config"]:
del(global_status["config"][item])
#Toggle pauses
if toggle_pauses:
for instance in running_traders:
if instance.status.get_pair() in toggle_pauses:
instance.pause = not instance.pause
toggle_pauses.clear()
#Checks if market reload is due
if time.time()-last_market_reload>reload_interval:
broker.reload_markets()
last_market_reload = time.time()
#Take a deep breath and start all over again
time.sleep(broker.get_lap_time())
def load_keys_from_db(file_name: str) -> list:
'''
Load valid API keys
Parameters
----------
file_name : str
Name of the database file
Returns
-------
valid_keys : list
List of valid API keys
'''
database_connection = sqlite3.connect(file_name)
database_cursor = database_connection.cursor()
database_cursor.execute("SELECT * FROM credentials_table")
data = database_cursor.fetchall()
database_connection.close()
valid_keys = [line[1] for line in data]
return valid_keys
def display_splashscreen():
'''
Display splash screen
'''
print("""
###### ##### # #####
# # # # # # # #
# # # # # #
# # # # # # # #####
# # # ####### # # #
# # # # # # # # #
###### ##### # # ## #######
""")
print(time.strftime(f"[%Y/%m/%d %H:%M:%S] | DCAv2 version {version}"))
print(time.strftime(f"[%Y/%m/%d %H:%M:%S] | Using CCXT version {ccxt.__version__}"))
return None
#########################
######### API ###########
#########################
base_api = Flask(__name__)
@base_api.route("/global_status", methods=['GET'])
def return_global_status():
'''
GET request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_global_status()
@base_api.route("/paused_traders", methods=['GET'])
def return_paused_status():
'''
GET request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_paused_traders()
@base_api.route("/worker_status", methods=['GET'])
def return_worker_status():
'''
GET request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
base = request.args.get("base")
quote = request.args.get("quote")
return unwrapped_return_worker_status(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/view_old_long", methods=["GET"])
def return_old_long():
'''
GET request
Parameters:
base: str
quote: str
from_file: bool
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
base = request.args.get("base")
quote = request.args.get("quote")
from_file = request.args.get("from_file")
return unwrapped_view_old_long(base,quote,from_file)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/switch_to_long_price", methods=["GET"])
def return_switch_price():
'''
GET request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
base = request.args.get("base")
quote = request.args.get("quote")
return unwrapped_switch_to_long_price(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/base_add_so_calculation", methods=["GET"])
def return_base_add_so_calculation():
'''
GET request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
base = request.args.get("base")
quote = request.args.get("quote")
return unwrapped_base_add_so_calculation(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/get_all_worker_status", methods=['GET'])
def return_all_worker_status():
'''
GET request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_return_all_worker_status()
@base_api.route("/add_pair", methods=['POST'])
def add_pair():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_add_pair(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/remove_pair", methods=['POST'])
def remove_pair():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_remove_pair(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/restart_pair", methods=['POST'])
def restart_pair():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_restart_pair(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/import_pair", methods=['POST'])
def import_pair():
'''
POST request
Parameters:
base: str
quote: str
forced_tp_id: str
forced_so_id: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_import_pair(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/switch_to_long", methods=['POST'])
def switch_to_long():
'''
POST request
Parameters:
base: str
quote: str
calculate_profits: int
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
calculate_profits = data["calculate_profits"]
return unwrapped_switch_to_long(base,quote,calculate_profits)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/switch_to_short", methods=['POST'])
def switch_to_short():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_switch_to_short(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/load_old_long", methods=['POST'])
def load_old_long():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_load_old_long(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/add_so", methods=['POST'])
def add_so():
'''
POST request
Parameters:
base: str
quote: str
amount: int
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
amount = data["amount"]
return unwrapped_add_safety_orders(base,quote,amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/mod_tp_level", methods=['POST'])
def mod_tp_level():
'''
POST request
Parameters:
base: str
quote: str
amount: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
amount = data["amount"]
return unwrapped_mod_tp_level(base,quote,amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/mod_order_size", methods=['POST'])
def mod_order_size():
'''
POST request
Parameters:
base: str
quote: str
amount: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
amount = data["amount"]
return unwrapped_mod_order_size(base,quote,amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/mod_concurrent_safety_orders", methods=['POST'])
def mod_concurrent_safety_orders():
'''
POST request
Parameters:
base: str
quote: str
amount: int
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
amount = data["amount"]
return unwrapped_mod_concurrent_safety_orders(base,quote,amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/mod_boosted_concurrent_safety_orders", methods=['POST'])
def mod_boosted_concurrent_safety_orders():
'''
POST request
Parameters:
base: str
quote: str
amount: int
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
amount = data["amount"]
return unwrapped_mod_boosted_concurrent_safety_orders(base,quote,amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/mod_default_order_size", methods=['POST'])
def mod_default_order_size():
'''
POST request
Parameters:
amount: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
amount = data["amount"]
return unwrapped_mod_default_order_size(amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/mod_global_tp_level", methods=['POST'])
def mod_global_tp_level():
'''
POST request
Parameters:
amount: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
amount = data["amount"]
return unwrapped_mod_global_tp_level(amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/last_call", methods=['POST'])
def last_call():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_last_call(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/deferred_last_call", methods=['POST'])
def deferred_last_call():
'''
POST request
Parameters:
base: str
quote: str
yyyymmdd: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
yyyymmdd = data["yyyymmdd"]
return unwrapped_deferred_last_call(base,quote,yyyymmdd)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/toggle_pause", methods=['POST'])
def toggle_pause():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_toggle_pause(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/global_last_call", methods=['POST'])
def global_last_call():
'''
POST request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_global_last_call()
@base_api.route("/cancel_global_last_call", methods=['POST'])
def cancel_global_last_call():
'''
POST request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_cancel_global_last_call()
@base_api.route("/add_quote", methods=['POST'])
def add_quote():
'''
POST request
Parameters:
base: str
quote: str
amount: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
amount = data["amount"]
return unwrapped_add_quote(base,quote,amount)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/missing_pairs", methods=['GET'])
def missing_pairs():
'''
GET request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_missing_pairs()
@base_api.route("/toggle_cleanup", methods=['POST'])
def toggle_cleanup():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_toggle_cleanup(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/toggle_autoswitch", methods=['POST'])
def toggle_autoswitch():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_toggle_autoswitch(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/toggle_liquidate_after_switch", methods=['POST'])
def toggle_liquidate_after_switch():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_toggle_liquidate_after_switch(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/toggle_check_old_long_price", methods=['POST'])
def toggle_check_old_long_price():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_toggle_check_old_long_price(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/switch_quote_currency", methods=['POST'])
def switch_quote_currency():
'''
POST request
Parameters:
base: str
quote: str
new_quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
new_quote = data["new_quote"]
return unwrapped_switch_quote_currency(base,quote,new_quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/toggle_restart", methods=['POST'])
def toggle_restart():
'''
POST request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_toggle_restart()
@base_api.route("/toggle_telegram", methods=['POST'])
def toggle_telegram():
'''
POST request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_toggle_telegram()
@base_api.route("/server_time", methods=['GET'])
def server_time():
'''
GET request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_server_time()
@base_api.route("/get_log_list", methods=['GET'])
def get_log_list():
'''
GET request
Parameters:
coin: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_get_log_list()
@base_api.route("/get_balance", methods=['GET'])
def get_balance():
'''
GET request
Parameters:
coin: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
coin = request.args.get("coin")
return unwrapped_get_balance(coin)
@base_api.route("/get_deals_cache", methods=['GET'])
def get_deals_cache():
'''
GET request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_get_deals_cache()
@base_api.route("/trader_time", methods=['GET'])
def trader_time():
'''
GET request
Parameters:
None
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_trader_time()
@base_api.route("/edit_loop_wait_time", methods=['POST'])
def loop_wait_time():
'''
POST request
Parameters:
wait_time: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
wait_time = data["wait_time"]
return unwrapped_loop_wait_time(wait_time)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/edit_call_wait_time", methods=['POST'])
def call_wait_time():
'''
POST request
Parameters:
wait_time: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
wait_time = data["wait_time"]
return unwrapped_call_wait_time(wait_time)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/edit_cooldown_multiplier", methods=['POST'])
def edit_cooldown_multiplier():
'''
POST request
Parameters:
cooldown_multiplier: float
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
multiplier = data["cooldown_multiplier"]
return unwrapped_edit_cooldown_multiplier(multiplier)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
@base_api.route("/reload_markets", methods=['POST'])
def reload_markets():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
return unwrapped_reload_markets()
@base_api.route("/reload_trader_config", methods=['POST'])
def reload_trader_config():
'''
POST request
Parameters:
base: str
quote: str
'''
if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys:
return jsonify({'Error': 'API key invalid'}), 401
try:
if request.json is None:
return jsonify({'Error': 'request.json is None'})
data = request.json
base = data["base"]
quote = data["quote"]
return unwrapped_reload_trader_config(base,quote)
except Exception as e:
print(e)
return jsonify({'Error': 'Halp'})
def run_API():
serve(base_api, host="0.0.0.0", port=broker.get_config()["port"])
###########################
# Unwrapped API functions #
###########################
def unwrapped_global_status():
'''
Returns the global status dictionary of the instance.
'''
return jsonify(global_status)
def unwrapped_paused_traders():
'''
Returns the paused_traders field from the global status dictionary of the instance.
'''
return jsonify(global_status["paused_traders"])
def unwrapped_return_worker_status(base,quote):
'''
Returns the individual status dictionary of the trader.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
dict: The status dictionary of the trader.
'''
symbol = f"{base}/{quote}"
for instance in running_traders:
if instance.status.get_pair() == symbol:
return jsonify(instance.status.get_status())
return jsonify({"Error": "Worker does not exist"})
def unwrapped_return_all_worker_status():
'''
Returns the status dictionary of all traders.
Returns:
dict: The status dictionary of all traders.
'''
return {instance.status.get_pair(): instance.status.get_status() for instance in running_traders}
def unwrapped_add_pair(base,quote):
'''
Adds a trader to the instance.
Parameters:
base (str): The base currency of the trader.
quote (str): The quote currency of the trader.
Returns:
jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
#Check if the trader is already running
for instance in running_traders:
if symbol==instance.status.get_pair():
broker.logger.log_this(f"Pair already running",1,symbol)
return jsonify({"Error": "Pair already running"})
#Check if the market exists and it's open
if not broker.validate_market(symbol):
return jsonify({"Error": "Market error. Check logs for more info."})
broker.logger.log_this(f"Initializing trader",2,symbol)
add_instance(base,quote)
broker.add_pair_to_config(f"{base}{quote}")
broker.rewrite_config_file(backup=True)
return jsonify({"Success": "Pair added"})
except Exception as e:
broker.logger.log_this(f"Exception while initializing new instance: {e}",1,symbol)
return jsonify({"Error": "Error initializing new instance."})
def unwrapped_remove_pair(base,quote):
'''
Removes a trader from and instance
Parameters:
base (str): The base currency of the pair
quote (str): The quote currency of the pair
Returns:
jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.quit = True
return jsonify({"Success": "Pair to be removed"})
except Exception as e:
broker.logger.log_this(f"Exception while removing instance: {e}",1,symbol)
return jsonify({"Error": "Halp"})
def unwrapped_restart_pair(base,quote):
'''
Restarts a trader instance
Parameters:
base (str): The base currency of the pair
quote (str): The quote currency of the pair
Returns:
jsonified dictionary detailing the outcome of the operation.
'''
if restart_pair_no_json(base,quote)==0:
return jsonify({"Success": "Pair restarted"})
return jsonify({"Error": "Halp"})
def unwrapped_import_pair(base,quote):
'''
Imports a previously running pair
Parameters:
base (str): The base currency of the pair
quote (str): The quote currency of the pair
Returns:
jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
import_instance(base,quote)
broker.add_pair_to_config(f"{base}{quote}")
broker.rewrite_config_file()
broker.logger.log_this(f"Done",2,symbol)
return jsonify({"Success": "Pair imported successfully"})
except Exception as e:
broker.logger.log_this(f"Exception while importing instance: {e}",1,symbol)
return jsonify({"Error": "Error importing instance"})
def unwrapped_switch_to_long(base,quote,calculate_profits):
'''
Switches a pair to long mode.
Parameters:
base (str): The base currency of the pair
quote (str): The quote currency of the pair
calculate_profits (int): 1 if you want to calculate the profits, 0 if you don't
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
ignore_old_long = int(calculate_profits)!=1
#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"})
for instance in running_traders:
if f"{base}/{quote}"==instance.status.get_pair():
instance.pause = True
if instance.switch_to_long(ignore_old_long=ignore_old_long)==1:
return jsonify({"Error": "Error in switch_to_long()"})
if instance.start_trader()==1:
instance.quit = True
return jsonify({"Error": "Error switching to long mode (wAPI)"})
return jsonify({"Success": "Pair switched to long mode"})
return jsonify({"Error": "Pair not found"})
def unwrapped_switch_to_short(base,quote):
'''
Switches a pair to short mode.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
#Close trader and orders and pull info our of the orders
symbol = f"{base}/{quote}"
if f"{base}{quote}" not in broker.get_pairs():
return jsonify({"Error": "Pair not running"})
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()"})
#Restart instance
try:
broker.logger.log_this(f"Reinitializing trader",2,symbol)
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.status.set_take_profit_order(instance.broker.empty_order)
instance.so = instance.broker.empty_order
#Reloading config file
instance.config.load_from_file()
#Enabling autoswitch
instance.config.set_autoswitch(True)
if instance.start_trader()==1:
instance.quit = True
return jsonify({"Error": "Error switching to short mode (wAPI)"})
return jsonify({"Success": "Pair switched to short mode"})
except Exception as e:
broker.logger.log_this(f"Exception while reinitializing instance: {e}",1,symbol)
return jsonify({"Error": "Can't initialize trader"})
return jsonify({"Error": "Halp"})
def unwrapped_load_old_long(base,quote):
'''
Loads an old_long file to the status dictionary of a trader
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
#Load the file
try:
symbol = f"{base}/{quote}"
with open(f"{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)
return jsonify({"Error": "old_long file of that pair does not exist."})
#Check that the file has the proper keys
if not any(["tp_price" in old_long, "tp_amount" in old_long, "quote_spent" in old_long, "datetime" in old_long]):
broker.logger.log_this(f"old_long file invalid: keys missing.",1,symbol)
return jsonify({"Error": "File is invalid or missing keys"})
#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.
for instance in running_traders:
if instance.status.get_pair()==symbol:
instance.get_status_dict()["old_long"]=old_long
instance.update_status(True)
return jsonify({"Success": "old_long file loaded to status_dict"})
return jsonify({"Error": "Pair not found"})
def unwrapped_view_old_long(base,quote,from_file):
'''
Returns the content of an old_long file
Parameters:
base (str): The base currency of the pair
quote (str): The quote currency of the pair
from_file (int): 1 if the file is to be loaded from the file system, 0 if it is to be loaded from the trader's status dictionary.
Returns:
jsonify: A jsonified dictionary containing the old_long info.
'''
try:
symbol = f"{base}/{quote}"
if int(from_file)==1:
with open(f"{base}{quote}.oldlong") as ol:
old_long = load(ol)
return jsonify(old_long)
for instance in running_traders:
if symbol==instance.status.get_pair():
if "old_long" in instance.get_status_dict():
return jsonify(instance.get_status_dict()["old_long"])
return jsonify({"Error": "No old_long info found"})
return jsonify({"Error": "Pair not found"})
except Exception as e:
broker.logger.log_this(f"Exception while viewing old_long info: {e}",1,symbol)
return jsonify({"Error": f"{e}"})
def unwrapped_switch_to_long_price(base,quote):
'''
Returns the content of an old_long file
Parameters:
base (str): The base currency of the pair
quote (str): The quote currency of the pair
from_file (int): 1 if the file is to be loaded from the file system, 0 if it is to be loaded from the trader's status dictionary.
Returns:
jsonify: A jsonified dictionary containing the old_long info.
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
if "old_long" in instance.get_status_dict():
#minimum_switch_price = (old_target - quote_already_in)/base_left
old_target = instance.get_status_dict()["old_long"]["tp_price"]*instance.get_status_dict()["old_long"]["tp_amount"]
base_left = instance.get_status_dict()["old_long"]["tp_amount"]-instance.get_status_dict()["base_bought"]
minimum_switch_price = (old_target - instance.get_status_dict()["quote_spent"])/base_left
return jsonify({"switch_price": minimum_switch_price})
return jsonify({"Error": "No old_long info found"})
return jsonify({"Error": "Pair not found"})
except Exception as e:
broker.logger.log_this(f"Exception while viewing old_long info: {e}",1,symbol)
return jsonify({"Error": f"{e}"})
def unwrapped_add_safety_orders(base,quote,amount):
'''
Increases the amount of safety orders that a trader can use. Once the current deal is closed, the value returns to the one in the config
file.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
amount (str): The amount of safety orders to add.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.pause = True
#x.no_of_safety_orders += int(amount)
instance.config.set_no_of_safety_orders(instance.config.get_no_of_safety_orders()+int(amount))
broker.logger.log_this("Recalculating safety price table...",1,symbol)
instance.status.set_safety_price_table(instance.calculate_safety_prices(instance.status.get_start_price(),instance.config.get_no_of_safety_orders(),instance.config.get_safety_order_deviance()))
broker.logger.log_this(f"Done. Added {amount} safety orders",1,symbol)
instance.update_status(True)
instance.pause = False
return jsonify({"Success": f"Done. Added {amount} safety orders"})
return jsonify({"Error": "Pair not found"})
except Exception as e:
broker.logger.log_this(f"{e}",2,symbol)
return jsonify({"Error": "Error adding safety orders"})
def unwrapped_base_add_so_calculation(base,quote):
'''
Calculates the amount of safety order that can be added to a short trader with the current available base currency.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
jsonify: A jsonified dictionary with the amount of orders that can be added.
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
free_base = instance.fetch_free_base()
if free_base is None:
return jsonify({"Error": "Can't fetch amount of free base on the exchange"})
amount_of_orders = instance.base_add_calculation(free_base)
return jsonify({"Amount": amount_of_orders, "Free base on exchange": free_base})
return jsonify({"Error": "Can't find the pair in the running instances"})
except Exception as e:
broker.logger.log_this(f"{e}",2,symbol)
return jsonify({"Error": "Error in unwrapped_base_add_so_calculation"})
def unwrapped_mod_tp_level(base,quote,amount):
'''
Modifies the take profit percentage of a pair. It applies the new percentage only after a new TP order is sent.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
amount (str): The new take profit percentage.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_tp_level(float(amount))
broker.logger.log_this("Done. The change will take effect when the next take profit order is placed",2,symbol)
return jsonify({"Success": "Success. The change will take effect when the next TP order is placed"})
except Exception:
broker.logger.log_this("Error changing percentage. Ignoring...",2,symbol)
return jsonify({"Error": "Error changing percentage"})
def unwrapped_mod_order_size(base,quote,amount):
'''
Modifies the order size of a pair. It applies the new order size as soon as a new deal is started.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
amount (str): The new order size.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_order_size(float(amount))
instance.config.save_to_file()
broker.logger.log_this("Done. The change will take effect when the next deal is started",2,symbol)
return jsonify({"Success": "Success. The change will take effect when the next deal is started"})
except Exception:
broker.logger.log_this("Error changing order size. Ignoring...",2,symbol)
return jsonify({"Error": "Error changing order size"})
def unwrapped_mod_concurrent_safety_orders(base,quote,amount):
'''
Modifies the amount of safety orders that a trader keeps opened at the same time.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
amount (str): The new amount.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_concurrent_safety_orders(int(amount))
instance.config.save_to_file()
broker.logger.log_this("Done. The change will take effect as new safety orders are sent or filled",2,symbol)
return jsonify({"Success": "Success. The change will take effect as new safety orders are sent or filled"})
except Exception:
broker.logger.log_this("Error changing safety orders amount. Ignoring...",2,symbol)
return jsonify({"Error": "Error changing safety orders amount"})
def unwrapped_mod_boosted_concurrent_safety_orders(base,quote,amount):
'''
Modifies the amount of safety orders that a trader keeps opened at the same time while boosted.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
amount (str): The new amount.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_boosted_concurrent_safety_orders(int(amount))
instance.config.save_to_file()
broker.logger.log_this("Done. The change will take effect as new safety orders are sent or filled",2,symbol)
return jsonify({"Success": "Success. The change will take effect as new safety orders are sent or filled"})
except Exception:
broker.logger.log_this("Error changing safety orders amount. Ignoring...",2,symbol)
return jsonify({"Error": "Error changing safety orders amount"})
def unwrapped_mod_default_order_size(amount):
'''
Modifies the default order size of a broker.
Parameters:
amount (str): The new order size.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
try:
broker.set_default_order_size(float(amount))
broker.logger.log_this(f"Done. Default order size modified to {float(amount)}",2)
return jsonify({"Success": f"Success. Default order size modified to {float(amount)}"})
except Exception:
broker.logger.log_this("Error modifying default order size",2)
return jsonify({"Error": "Error modifying default order size"})
def unwrapped_mod_global_tp_level(amount):
'''
Modifies the take profit percentage of all pairs. It applies the new percentage only after a new TP order is sent.
Parameters:
amount (float): The new percentage to be used.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
for instance in running_traders:
try:
instance.config.set_tp_level(float(amount))
broker.logger.log_this("Done. The change will take effect when the next take profit order is placed",2)
except Exception:
broker.logger.log_this("Error changing percentage. Ignoring.",2)
return jsonify({"Success": "Success. The change will take effect when the next TP order is placed"})
def unwrapped_last_call(base,quote):
'''
Signals the trader to stop opening new deals once the current one is closed.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation
'''
try:
symbol = f"{base}/{quote}"
if symbol in broker.get_pairs(): #Why was this?
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.status.set_stop_when_profit(not instance.status.get_stop_when_profit())
instance.update_status(True)
if instance.status.get_stop_when_profit():
instance.config.set_autoswitch(False)
instance.update_status(True)
return jsonify({"Success": "Trader scheduled to go offline when profit is reached"})
return jsonify({"Success": "Last call cancelled"})
return jsonify({"Error": "Trader does not exist"})
except Exception:
return jsonify({"Error": "Halp"})
def unwrapped_deferred_last_call(base,quote,yyyymmdd):
'''
Programs the trader to not open new deals from a certain future date.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
yyyymmdd (str): The date in YYYYMMDD format.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
year = yyyymmdd[:4]
month = yyyymmdd[4:6]
day = yyyymmdd[6:8]
limit = time_to_unix(year,month,day)
if limit==0:
return jsonify({"Error": "Can't convert date to unix"})
for instance in running_traders:
if f"{base}{quote}"==instance.status.get_pair():
instance.config.set_programmed_stop_time(limit)
instance.config.set_programmed_stop(True)
#save config file to disk
instance.broker.rewrite_config_file()
return jsonify({"Success": f"Trader scheduled to go offline when profit is reached after {limit}"})
except Exception:
return jsonify({"Error": "Halp"})
def unwrapped_toggle_pause(base,quote):
'''
Toggles the pause flag of a trader.
When a trader is paused, no new safety orders are sent to the exchange and the take profit order is unmonitored.
Although it could be useful to close the trader if the tp order is filled anyway.
Parameters:
base (str): The base currency of the trader.
quote (str): The quote currency of the trader.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
toggle_pauses.append(symbol)
for instance in running_traders:
if instance.status.get_pair()==symbol:
if instance.pause:
instance.status.set_pause_reason("")
return jsonify({"Success": "Trader will be resumed"})
instance.status.set_pause_reason("User requested pause")
return jsonify({"Success": "Trader will be paused"})
return jsonify({"Error": "Trader does not exist"})
except Exception:
return jsonify({"Error": "Halp"})
def unwrapped_global_last_call():
'''
Signals all traders to stop opening new trades when the current ones closes.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
if broker.get_pairs!=[]:
#broker.clear_pairs()
for instance in running_traders:
instance.status.set_stop_when_profit(True)
instance.config.set_autoswitch(False)
broker.logger.log_this("Modified flag",2,f"{instance.base}/{instance.quote}")
return jsonify({"Success": "All traders scheduled to go offline when profit is reached"})
return jsonify({"Error": "No traders running"})
except Exception:
return jsonify({"Error": "Halp"})
def unwrapped_cancel_global_last_call():
'''
Cancels the last call.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
if broker.get_pairs!=[]:
#broker.clear_pairs()
for instance in running_traders:
instance.status.set_stop_when_profit(False)
broker.logger.log_this("Modified flag",2,f"{instance.base}/{instance.quote}")
return jsonify({"Success": "Last call canceled"})
return jsonify({"Error": "No traders running"})
except Exception:
return jsonify({"Error": "Halp"})
def unwrapped_add_quote(base,quote,amount):
'''
Adds more funds to the deal, bringing down the average buy price.
I do not recommend to use this, it's preferable to add safety orders or switching to short mode, but you do you.
Maybe it's more useful in a bull market's high volatility moment.
Parameters:
base (str): The base currency of the pair
quote (str): The quote currency of the pair
amount (float): The amount of quote currency to add
Returns:
json: A jsonified dictionary detailing the outcome of the operation.
'''
for instance in running_traders:
if f"{base}/{quote}"==instance.status.get_pair():
if instance.config.get_is_short():
return jsonify({"Error": "Quote can't be added to short traders"})
instance.pause = True
new_average_price = (instance.status.get_quote_spent()+float(amount))/(instance.status.get_base_bought()+(float(amount)/instance.status.get_price()))
broker.logger.log_this(f"Your new average buy price will be {new_average_price} {quote}",2,instance.status.get_pair())
broker.logger.log_this(f"Your new take profit price price will be {new_average_price*instance.get_tp_level()} {quote}",2,instance.status.get_pair())
new_order = broker.new_market_order(instance.status.get_pair(),float(amount),"buy")
if new_order is None:
broker.logger.log_this("Error: Market order returned None",2,instance.status.get_pair())
instance.pause = False
return jsonify({"Error": "Market order returned None"})
while True:
time.sleep(broker.get_wait_time())
returned_order = broker.get_order(new_order["id"],instance.status.get_pair())
if returned_order==broker.empty_order:
broker.logger.log_this("Problems sending the order",2,instance.status.get_pair())
instance.pause = False
return jsonify({"Error": "Problems sending the order"})
elif returned_order["status"]=="expired":
instance.pause = False
return jsonify({"Error": "New order expired"})
elif returned_order["status"]=="closed":
broker.logger.log_this("Order sent",2,instance.status.get_pair())
new_fees_in_base, new_fees_in_quote = instance.parse_fees(returned_order)
instance.status.set_fees_paid_in_base(instance.status.get_fees_paid_in_base() + new_fees_in_base)
instance.status.set_fees_paid_in_quote(instance.status.get_fees_paid_in_quote() + new_fees_in_quote)
instance.status.set_base_bought(instance.status.get_base_bought() + returned_order["filled"] - new_fees_in_base)
instance.status.set_quote_spent(instance.status.get_quote_spent()+returned_order["cost"])
broker.logger.log_this("Cancelling old take profit order and sending a new one",2,instance.status.get_pair())
attempts = 5
while broker.cancel_order(instance.status.get_take_profit_order()["id"],instance.status.get_pair())==1:
broker.logger.log_this("Can't cancel old take profit order, retrying...",2,instance.status.get_pair())
time.sleep(broker.get_wait_time())
attempts-=1
if attempts==0:
broker.logger.log_this("Can't cancel old take profit order, cancelling...",2,instance.status.get_pair())
instance.pause = False
return jsonify({"Error": "Can't cancel old take profit order."})
instance.status.set_take_profit_price(instance.status.get_quote_spent()/instance.status.get_base_bought()*instance.get_tp_level())
instance.status.set_take_profit_order(broker.new_limit_order(instance.status.get_pair(),instance.status.get_base_bought(),"sell",instance.status.get_take_profit_price()))
instance.update_status(True)
break
else:
broker.logger.log_this("Waiting for initial order to get filled",2,instance.status.get_pair())
broker.logger.log_this(f"{returned_order}",2,instance.status.get_pair())
time.sleep(broker.get_wait_time())
instance.pause = False
broker.logger.log_this("Done",2,instance.status.get_pair())
return jsonify({"Success": "Quote added successfully"})
return jsonify({"Error": "Something horrible happened :S"})
def unwrapped_missing_pairs():
'''
Returns a list of the pairs that are not running that are included in the config file
Returns:
jsonify: A jsonify object with a list of the missing pairs
'''
try:
missing_pairs = broker.get_pairs()
for trader in running_traders:
if f"{trader.base}{trader.quote}" in missing_pairs:
missing_pairs.remove(f"{trader.base}{trader.quote}")
return jsonify({"Success": missing_pairs})
except Exception as e:
broker.logger.log_this(f"Exception while querying for missing pairs: {e}",1)
return jsonify({"Error": "Error fetching running pairs"})
def unwrapped_toggle_cleanup(base,quote):
'''
Signals a trader to enable or disable the cleanup routine.
Parameters:
base (str): The base currency of the pair to toggle
quote (str): The quote currency of the pair to toggle
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
instance.config.set_cleanup(not instance.config.get_cleanup())
if instance.config.get_cleanup():
return jsonify({"Success": "Cleanup turned ON"})
return jsonify({"Success": "Cleanup turned OFF"})
except Exception as e:
broker.logger.log_this(f"Exception while toggling cleanup: {e}",1,symbol)
return jsonify({"Error": "Halp"})
return jsonify({"Error": "Task failed successfully"})
def unwrapped_toggle_autoswitch(base,quote):
'''
Signals a trader to enable or disable autoswitch.
Parameters:
base (str): The base currency of the trading pair.
quote (str): The quote currency of the trading pair.
Returns:
A jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
if instance.config.get_autoswitch():
broker.logger.log_this("Autoswitch turned OFF",1,symbol)
instance.config.set_autoswitch(False)
return jsonify({"Success": "Autoswitch is now OFF"})
else:
broker.logger.log_this("Autoswitch turned ON",1,symbol)
instance.config.set_autoswitch(True)
return jsonify({"Success": "Autoswitch is now ON"})
return jsonify({"Error": "Trader not running"})
except Exception as e:
broker.logger.log_this(f"Exception while toggling autoswitch: {e}",1,symbol)
return jsonify({"Error": "Halp"})
def unwrapped_toggle_liquidate_after_switch(base,quote):
'''
Signals a trader to enable or disable quitting after switching from short to long.
Parameters:
base (str): The base currency of the trading pair.
quote (str): The quote currency of the trading pair.
Returns:
A jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
if instance.config.get_liquidate_after_switch():
broker.logger.log_this("Liquidate after switch turned OFF",1,symbol)
instance.config.set_liquidate_after_switch(False)
return jsonify({"Success": "Liquidate after switch is now OFF"})
else:
broker.logger.log_this("Liquidate after switch turned ON",1,symbol)
instance.config.set_liquidate_after_switch(True)
return jsonify({"Success": "Liquidate after switch is now ON"})
return jsonify({"Error": "Trader not running"})
except Exception as e:
broker.logger.log_this(f"Exception while toggling liquidate after switch: {e}",1,symbol)
return jsonify({"Error": "Halp"})
def unwrapped_toggle_check_old_long_price(base,quote):
'''
Signals to the trader if it should compare the current price to the old_long price stored in the old_long dictionary.
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
for instance in running_traders:
if symbol==instance.status.get_pair():
if instance.config.get_check_old_long_price():
broker.logger.log_this("Check OFF",1,symbol)
instance.config.set_check_old_long_price(False)
return jsonify({"Success": "Old long price check turned OFF"})
else:
broker.logger.log_this("Check ON",1,symbol)
instance.config.set_check_old_long_price(True)
return jsonify({"Success": "Old long price check turned ON"})
return jsonify({"Error": "Trader not running"})
except Exception as e:
broker.logger.log_this(f"Exception while toggling check_old_long_price: {e}",1,symbol)
return jsonify({"Error": "Halp"})
def unwrapped_switch_quote_currency(base,quote,new_quote):
'''
Switches the quote currency of a trader.
Parameters:
base (str): The base currency of the trader.
quote (str): The quote currency of the trader.
new_quote (str): The new quote currency to switch to.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
symbol = f"{base}/{quote}"
for trader in running_traders:
if symbol==trader.status.get_pair():
#Pause the trader
trader.pause = True
#Call x.switch_quote_currency
if trader.switch_quote_currency(new_quote)==1:
return jsonify({"Error": "Swap failed. Check log files for details."})
#Resume the trader
trader.pause = False
return jsonify({"Success": "Mission successful"})
return jsonify({"Error": "Trader not found"})
except Exception as e:
broker.logger.log_this(f"Exception while switching quote currency: {e}",1,symbol)
return jsonify({"Error": "Halp"})
def unwrapped_toggle_restart():
'''
Signals to an instance if it should restart a trader if there is an error.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
new_config = broker.get_config()
new_config["attempt_to_restart"] = not new_config["attempt_to_restart"]
broker.set_config(new_config)
if broker.get_config()["attempt_to_restart"]:
return jsonify({"Success": "attempt_to_restart enabled"})
return jsonify({"Success": "attempt_to_restart disabled"})
def unwrapped_toggle_telegram():
'''
Switches on or off the Telegram notifications
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
new_config = broker.get_config()
new_config["telegram"] = not new_config["telegram"]
broker.set_config(new_config)
broker.logger.set_telegram_notifications(new_config["telegram"])
toggle = "ON" if new_config["telegram"] else "OFF"
return jsonify({"Success": f"Telegram successfully toggled {toggle}"})
def unwrapped_server_time():
'''
Returns the server time.
Returns:
jsonify: A jsonified dictionary containing the server time.
'''
return jsonify({"Time": time.time()})
def unwrapped_trader_time():
'''
Returns the time of the last trader instance lap.
Returns:
jsonify: A jsonified dictionary containing the time of the last trader instance lap.
'''
try:
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)
return jsonify({"Error": str(e)})
def unwrapped_loop_wait_time(wait_time):
'''
Modifies the amount of time an instance waits between laps.
Parameters:
wait_time (int): The new amount of time to wait between laps.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
broker.set_lap_time(wait_time)
broker.logger.log_this("Done!")
return jsonify({"Success": "Lap time modified successfully"})
def unwrapped_get_log_list():
'''
Retrieves the last n entries from the broker's logger.
This list is kept on memory, to avoid having to read the log file every time.
Parameters:
None
Returns:
jsonify: A jsonified dictionary containing the last n entries from the log file.
'''
return jsonify({"Logs": broker.logger.get_log_list()})
def unwrapped_get_deals_cache():
'''
Retrieves the last n entries from the broker's logger.
This list is kept on memory, to avoid having to read the log file every time.
Parameters:
None
Returns:
jsonify: A jsonified dictionary containing the last n entries from the log file.
'''
return jsonify({"Deals": broker.get_deals_cache()})
def unwrapped_call_wait_time(wait_time):
'''
Modifies the time between some API calls and retries.
Parameters:
wait_time (float): The new amount of time to wait between calls and retries.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
broker.set_wait_time(wait_time)
broker.logger.log_this("Done!")
return jsonify({"Success": "Call wait time modified successfully"})
def unwrapped_edit_cooldown_multiplier(cooldown_multiplier):
'''
Modifies the broker's cooldown multiplier.
Parameters:
cooldown_multiplier (float): The new cooldown multiplier.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
old_value = broker.get_cooldown_multiplier()
broker.set_cooldown_multiplier(cooldown_multiplier)
broker.logger.log_this(f"Done! New cooldown multiplier changed from {old_value} seconds to {broker.get_cooldown_multiplier()} seconds.")
return jsonify({"Success": "Cooldown multiplier modified successfully"})
def unwrapped_reload_markets():
'''
Reloads the markets from the exchange.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
try:
broker.reload_markets()
return jsonify({"Success": "Markets reloaded successfully"})
except Exception as e:
broker.logger.log_this(f"Exception while reloading markets: {e}",1)
return jsonify({"Error": "Markets couldn't be reloaded"})
def unwrapped_get_balance(coin):
'''
Returns the balance of a given coin.
Parameters:
coin (str): The coin to get the balance of.
Returns:
dict: A dictionary containing the balance of the coin.
'''
try:
balance = broker.get_coins_balance(coin)
if balance not in [None,0]:
return jsonify({f"{coin}": balance[coin]['free']})
return jsonify({"Error": f"Balance query returned {balance}"})
except Exception as e:
broker.logger.log_this(f"Exception while querying balance: {e}",1)
return jsonify({"Error": "Balance could not be queried"})
def unwrapped_reload_trader_config(base,quote):
'''
Reloads the config file of the trader
Parameters:
base (str): The base currency of the pair.
quote (str): The quote currency of the pair.
Returns:
jsonify: A jsonified dictionary detailing the outcome of the operation.
'''
symbol = f"{base}/{quote}"
for trader in running_traders:
if trader.status.get_pair() == symbol:
if trader.config.load_from_file()==0:
return jsonify({"Success": "Config file reloaded"})
return jsonify({"Error": "Error reloading config file"})
return jsonify({"Error": "Worker does not exist"})
if __name__=="__main__":
#Logo
display_splashscreen()
#Setup some variables
instance_start_time = int(time.time())
last_market_reload = time.time()
reload_interval = 3600 #Market reload interval (in seconds)
#Loading config file
print(time.strftime("[%Y/%m/%d %H:%M:%S] | Loading config file..."))
try:
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)
#Check for import or load
import_mode = True
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:
print(time.strftime("[%Y/%m/%d %H:%M:%S] | Initializing in IMPORT MODE, press <Enter> to start..."))
input()
#Load exchange config
exchange = set_exchange(read_config)
if exchange is None:
print("Error initializing exchange. Check spelling and/or the exchange configuration file.")
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,argv[1]) #Also passes the config filename
#Declaring some variables
running_traders = []
instances_to_add = []
online_pairs = []
toggle_pauses = []
tickers = []
screen_buffer = []
global_status = {
"name": broker.get_config()["exchange"].upper(),
"instance_uptime": 0,
"online_workers": [],
"paused_traders": [],
"version": version,
"ccxt_version": f"{ccxt.__version__}",
"config": broker.get_config()
}
#Remove some keys that should not be public
for item in ["bot_chatID", "bot_token", "key", "secret", "password"]:
global_status["config"].pop(item,None)
#Load valid API keys
valid_keys = load_keys_from_db("utils/api_credentials.db")
#Initialize instances
if not import_mode:
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)
#broker.logger.log_this(f"Initializing {len(broker.get_pairs())} instances",2)
for x in broker.get_pairs():
symbol = broker.get_symbol(x)
if symbol!="Error":
initialize_instance(symbol.split("/")[0],symbol.split("/")[1])
else:
broker.logger.log_this(f"Can't initialize {x}, symbol query returned Error",1)
else:
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)
#broker.logger.log_this(f"Importing {len(broker.get_pairs())} instances",2)
for x in broker.get_pairs():
symbol = broker.get_symbol(x)
if symbol!="Error":
import_instance(symbol.split("/")[0],symbol.split("/")[1])
else:
broker.logger.log_this(f"Can't import {x}, symbol query returned Error",1)
broker.logger.log_this(f"All instances imported!",2)
#Waitress logging
waitress_logger = logging.getLogger('waitress')
waitress_logger.setLevel(logging.ERROR)
#Threads to run: flask api
api_thread = Thread(target=run_API,args=(), daemon=True)
api_thread.start()
try:
main_routine()
except KeyboardInterrupt:
api_thread.join()
shutdown_handler(signal.SIGINT, None)