import datetime import json import os import sys import time from threading import Thread import sqlite3 import ccxt from flask import Flask, jsonify, request import exchange_wrapper import trader ''' In case the permissions of the certificate changes, reset them this way: / sudo su # chmod -R 755 /etc/letsencrypt/live/ # chmod -R 755 /etc/letsencrypt/archive/ # ll /etc/letsencrypt/ ''' version = "2024.10.25" ''' 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" 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. ''' 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}" 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 ''' try: return int(time.mktime(datetime.date(int(year), int(month), int(day)).timetuple())) except Exception as e: broker.logger.log_this(f"{e}") return 0 def import_instance(pair: str) -> int: broker.logger.log_this(f"Importing {pair}") #with open(f"status/{pair}.status", "r") as f: # status_file_contents = json.load(f) with open(f"configs/{pair}.json", "r") as g: config_file_contents = json.load(g) instances_to_add.append(trader.trader(broker,config_file_contents,is_import=True)) if pair not in tickers: tickers.append(pair) return 0 def add_instance(base: str, quote: str) -> int: #Check if the pair is already running pair = f"{base}{quote}" for x in running_instances: if f"{x.base}{x.quote}"==pair: broker.logger.log_this(f"Pair already running, duplicate instances are not allowed",1,pair) return 1 #Check if config file already exists; if not, generate a new one if not os.path.isfile(f"configs/{pair}.json"): broker.logger.log_this(f"Config file does not exist. Generating...",1,pair) details = generate_config_file(base,quote) with open(f"configs/{pair}.json","w") as cf: cf.write(json.dumps(details, indent=4)) else: with open(f"configs/{pair}.json", "r") as cf: details = json.load(cf) #Initialize the trader object and add the pair to the tickers list instances_to_add.append(trader.trader(broker,details)) if pair not in tickers: tickers.append(pair) return 0 def initialize_instance(pair: str) -> int: ''' Loads the pair config file and initializes the trader object by adding it to the running instances list ''' with open(f"configs/{pair}.json", "r") as y: config_details = json.load(y) broker.logger.log_this(f"Initializing {pair}") running_instances.append(trader.trader(broker,config_details)) if pair not in tickers: tickers.append(pair) return 0 def generate_config_file(base: str, quote: str) -> dict: ''' Generates a config file with default values for a given pair and returns that content in dictionary form. TODO: Add a pair check against exchange's tickers data to properly support BASEQUOTE input format (without a slash) 1. load tickers 2. search for pair in dictionary 3. assign proper base and quote values from the dictionary ''' return {"pair": f"{base}/{quote}", "order_size": broker.get_default_order_size(), "tp_level": 1.02, "no_of_safety_orders": 30, "safety_order_deviance": 2, "safety_order_scale": 0.0105, "write_logs": True, "cleanup": True, "telegram": True, "tp_mode": 3, "tp_table": [], "is_short": False, "autoswitch": False, "check_old_long_price": True, "attempt_restart": True, "dynamic_so_deviance": True, "dsd_range": 1, "slippage_default_threshold": .02 } def set_exchange(config: dict): ''' Takes the config dictionary as an input and returns the exchange object properly configured ''' 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 }) 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: try: order_list = broker.fetch_full_orders(tickers) for x in running_instances: if f"{base}/{quote}"==x.pair: x.pause = True #Backing up old status file x.write_status_file(True) #Here, we could open a duster (if needed) for order in order_list: if order["symbol"]==f"{base}/{quote}" and x.is_short and order["side"]=="sell": broker.logger.log_this(f"Cancelling old sell orders",2,f"{base}/{quote}") broker.cancel_order(order["id"],order["symbol"]) elif order["symbol"]==f"{base}/{quote}" and not x.is_short and order["side"]=="buy": broker.logger.log_this(f"Cancelling old buy orders",2,f"{base}/{quote}") broker.cancel_order(order["id"],order["symbol"]) running_instances.remove(x) 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,f"{base}/{quote}") return 1 def main_loop(): global last_market_reload global reload_interval global screen_buffer #global paused_pairs while True: #Restart traders that have the restart flag raised and remove traders that have the quit flag raised for x in running_instances: if x.restart and x.config_dict["attempt_restart"]: broker.logger.log_this(f"Restarting trader",1,x.pair) restart_pair_no_json(x.base,x.quote) if x.quit: #Here, check if a duster is needed broker.logger.log_this(f"Quit flag raised, removing pair.",0,x.pair) if f"{x.base}{x.quote}" in tickers: tickers.remove(f"{x.base}{x.quote}") broker.remove_pair_from_config(f"{x.base}{x.quote}") broker.rewrite_config_file() if x.pair in worker_status: del(worker_status[x.pair]) running_instances.remove(x) #Adds pending traders if bool(instances_to_add): for x in instances_to_add: running_instances.append(x) instances_to_add.clear() #Prepares the trader threads open_orders = broker.fetch_open_orders(tickers) pairs_to_fetch = [] online_pairs = [] for x in running_instances: threads.append(Thread(target=x.check_status,args=(open_orders,))) online_pairs.append(f"{x.base}{x.quote}") pairs_to_fetch.append(x.pair) #Here, append the dusters' pairs to pairs_to_fetch, if missing. # # # # # #Start the trader threads for t in threads: try: t.start() except Exception as e: broker.logger.log_this(f"Error starting thread - {e}") #Wait for the trader threads to complete for t in threads: try: t.join() except Exception as e: broker.logger.log_this(f"Error joining thread: {e}") threads.clear() #Fetch prices price_list = broker.get_prices(pairs_to_fetch) #Here, assign the prices to the dusters (if any) curr = 0 top = 0 for x in running_instances: if not x.is_short: curr += int(x.status_dict["so_amount"]) # For the safety order occupancy percentage calculation top += int(x.config_dict["no_of_safety_orders"]) # It shows the percentage of safety orders not filled if not x.quit: #Why? Maybe to protect return_status() from weird errors if the trader errored out? try: if x.pair in price_list and price_list[x.pair] is not None: x.status_dict["price"] = price_list[x.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,x.pair) #if "status_string" in x.status_dict: # screen_buffer.append(x.status_dict["status_string"]) worker_status[x.pair] = x.status_dict #Clear the screen buffer screen_buffer.clear() #Append worker data to screen buffer, shorts first. for x in running_instances: if x.is_short and "status_string" in x.status_dict: screen_buffer.append(x.status_dict["status_string"]) for x in running_instances: if not x.is_short and "status_string" in x.status_dict: screen_buffer.append(x.status_dict["status_string"]) #Updates some global status variables prior to deletion of those global_status["online_workers"] = online_pairs.copy() #Check for paused pairs global_status["paused_traders"] = [x.pair for x in running_instances if x.pause] if global_status["paused_traders"]: screen_buffer.append(f"{cyan}Paused pairs: {list(global_status['paused_traders'])}{white}") #Check for paused pairs for x in running_instances: if x.pause: screen_buffer.append(f"{x.pair} paused: {x.status_dict['pause_reason']}") #Prints general info instance_uptime = int(time.time()) - instance_start_time #long_traders = len([None for x in running_instances if not x.is_short]) #short_traders = len([None for x in running_instances if x.is_short]) screen_buffer.append(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {len(running_instances)} 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 is_testnet = "TESTNET " if broker.get_config()["is_sandbox"] else "" screen_buffer.append(f"{bright_white}{broker.get_config()['exchange'].upper()} {is_testnet}{white}| DCAv2 {version} | CCXT v{ccxt.__version__} | Safety order occupancy: {color}{so_index}%{white}") screen_buffer.append(blue + "="*80 + white) #Print screen buffer for line in screen_buffer: print(line) #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_instances: if instance.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 ''' #valid_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] #for line in data: # valid_keys.append(line[1]) 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 "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_global_status() return jsonify({'Error': 'API key invalid'}), 401 #@base_api.route("/paused_traders", methods=['GET']) #def return_paused_status(): # ''' # GET request # # Parameters: # None # ''' # if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: # return unwrapped_paused_traders() # return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/worker_status", methods=['GET']) def return_worker_status(): ''' GET request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/view_old_long", methods=["GET"]) def return_old_long(): ''' GET request Parameters: base: str quote: str from_file: bool ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/get_all_worker_status", methods=['GET']) def return_all_worker_status(): ''' GET request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_return_all_worker_status() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/add_pair", methods=['POST']) def add_pair(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/remove_pair", methods=['POST']) def remove_pair(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/restart_pair", methods=['POST']) def restart_pair(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/import_pair", methods=['POST']) def import_pair(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/switch_to_long", methods=['POST']) def switch_to_long(): ''' POST request Parameters: base: str quote: str calculate_profits: int ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/switch_to_short", methods=['POST']) def switch_to_short(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/load_old_long", methods=['POST']) def load_old_long(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/add_so", methods=['POST']) def add_so(): ''' POST request Parameters: base: str quote: str amount: int ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/mod_tp_level", methods=['POST']) def mod_tp_level(): ''' POST request Parameters: base: str quote: str amount: float ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/mod_global_tp_level", methods=['POST']) def mod_global_tp_level(): ''' POST request Parameters: amount: float ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/last_call", methods=['POST']) def last_call(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/deferred_last_call", methods=['POST']) def deferred_last_call(): ''' POST request Parameters: base: str quote: str yyyymmdd: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/toggle_pause", methods=['POST']) def toggle_pause(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/global_last_call", methods=['POST']) def global_last_call(): ''' POST request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_global_last_call() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/add_quote", methods=['POST']) def add_quote(): ''' POST request Parameters: base: str quote: str amount: float ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/missing_pairs", methods=['GET']) def missing_pairs(): ''' GET request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_missing_pairs() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/toggle_cleanup", methods=['POST']) def toggle_cleanup(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/toggle_autoswitch", methods=['POST']) #type:ignore def toggle_autoswitch(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/toggle_check_old_long_price", methods=['POST'])#type:ignore def toggle_check_old_long_price(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/switch_quote_currency", methods=['POST']) def switch_quote_currency(): ''' POST request Parameters: base: str quote: str new_quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/toggle_restart", methods=['POST']) def toggle_restart(): ''' POST request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_toggle_restart() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/toggle_telegram", methods=['POST']) def toggle_telegram(): ''' POST request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_toggle_telegram() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/server_time", methods=['GET']) def server_time(): ''' GET request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_server_time() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/trader_time", methods=['GET']) def trader_time(): ''' GET request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_trader_time() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/edit_loop_wait_time", methods=['POST']) def loop_wait_time(): ''' POST request Parameters: wait_time: float ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/edit_call_wait_time", methods=['POST']) def call_wait_time(): ''' POST request Parameters: wait_time: float ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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'}) return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/reload_markets", methods=['POST']) def reload_markets(): ''' POST request Parameters: base: str quote: str ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: return unwrapped_reload_markets() return jsonify({'Error': 'API key invalid'}), 401 @base_api.route("/reload_safety_order", methods=['POST']) def reload_safety_order(): ''' POST request Parameters: None ''' if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys: 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_safety_order(base,quote) except Exception as e: print(e) return jsonify({'Error': 'Halp'}) return jsonify({'Error': 'API key invalid'}), 401 def run_API(): base_api.run(host="0.0.0.0", port=broker.get_config()["port"]) ########################### # Unwrapped API functions # ########################### def unwrapped_global_status(): return jsonify(global_status) def unwrapped_return_worker_status(base,quote): if f"{base}/{quote}" in worker_status: return jsonify(worker_status[f"{base}/{quote}"]) return jsonify({"Error": "Worker does not exist"}) def unwrapped_return_all_worker_status(): return jsonify(worker_status) def unwrapped_add_pair(base,quote): try: #Check if the trader is already running for x in running_instances: if f"{base}/{quote}"==x.pair: broker.logger.log_this(f"Pair already running",1,f"{base}/{quote}") return jsonify({"Error": "Pair already running"}) #Check if the market exists and it's open markets = broker.exchange.load_markets() if f"{base}/{quote}" not in markets: broker.logger.log_this(f"Market does not exist",1,f"{base}/{quote}") return jsonify({"Error": "Market does not exist"}) if not markets[f"{base}/{quote}"]["active"]: broker.logger.log_this(f"Market is inactive",1,f"{base}/{quote}") return jsonify({"Error": "Market is inactive"}) broker.logger.log_this(f"Initializing trader",2,f"{base}/{quote}") add_instance(base,quote) broker.add_pair_to_config(f"{base}{quote}") #Adding the pair to the config file (if it's not there yet) #1. Read the config file with open(sys.argv[1],"r") as f: temp_details = json.load(f) if base+quote not in temp_details["pairs"]: #2. Save the current config file as .bak with open(f"{sys.argv[1]}.bak","w") as c: c.write(json.dumps(temp_details, indent=4)) #3. Add the pair to the right list temp_details["pairs"].append(f"{base}{quote}") #4. Write the config file with open(sys.argv[1],"w") as c: c.write(json.dumps(temp_details, indent=4)) broker.logger.log_this(f"Broker's config file updated",2,f"{base}/{quote}") else: broker.logger.log_this(f"Pair already included in the config file",2,f"{base}/{quote}") return jsonify({"Success": "Pair added"}) except Exception as e: broker.logger.log_this(f"Exception while initializing new instance: {e}",1,f"{base}/{quote}") return jsonify({"Error": "Error initializing new instance."}) def unwrapped_remove_pair(base,quote): try: for x in running_instances: if f"{base}/{quote}"==x.pair: x.quit = True return jsonify({"Success": "Pair to be removed"}) except Exception as e: broker.logger.log_this(f"Exception while removing instance: {e}",1,f"{base}/{quote}") return jsonify({"Error": "Halp"}) def unwrapped_restart_pair(base,quote): if restart_pair_no_json(base,quote)==0: return jsonify({"Success": "Pair restarted"}) return jsonify({"Error": "Halp"}) def unwrapped_import_pair(base,quote): try: import_instance(base+quote) broker.add_pair_to_config(f"{base}{quote}") broker.rewrite_config_file() broker.logger.log_this(f"Done",2,f"{base}/{quote}") return jsonify({"Success": "Pair imported successfully"}) except Exception as e: broker.logger.log_this(f"Exception while importing instance: {e}",1,f"{base}/{quote}") return jsonify({"Error": "Error importing instance"}) def unwrapped_switch_to_long(base,quote,calculate_profits): ''' Switches a pair to long mode. If calculate_profits is 0, it does not calculate the profits ''' 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 x in running_instances: if f"{base}/{quote}"==x.pair: x.pause = True if x.switch_to_long(ignore_old_long=ignore_old_long)==1: return jsonify({"Error": "Error in switch_to_long()"}) if x.start_bot()==1: x.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. ''' #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 x in running_instances: if f"{base}/{quote}"==x.pair and x.switch_to_short()==1: return jsonify({"Error": "Error in switch_to_short()"}) #Restart instance try: broker.logger.log_this(f"Reinitializing trader",2,f"{base}/{quote}") for x in running_instances: if f"{base}/{quote}"==x.pair: #Clearing some variables #x.fees_paid_in_quote = 0 #x.fees_paid_in_base = 0 x.tp_order = x.broker.empty_order x.so = x.broker.empty_order #x.safety_price_table.clear() #This clearing (probably) is the origin of the update_status error after switching to short. #x.safety_order_index = 0 #Reloading config file x.config_dict = x.reload_config_dict() #Enabling autoswitch x.config_dict["autoswitch"] = True if x.start_bot()==1: x.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,f"{base}/{quote}") 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 ''' #Load the file try: with open(f"{base}{quote}.oldlong") as ol: old_long = json.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."}) #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,f"{base}/{quote}") 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 x in running_instances: if x.pair==f"{base}/{quote}": x.status_dict["old_long"]=old_long x.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 ''' try: if int(from_file)==1: with open(f"{base}{quote}.oldlong") as ol: old_long = json.load(ol) return jsonify(old_long) for x in running_instances: if f"{base}/{quote}"==x.pair: return jsonify(x.status_dict["old_long"]) return jsonify({"Error": "Pair not found"}) except Exception as e: broker.logger.log_this(f"Exception while viewing old_long info: {e}",1,f"{base}/{quote}") 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. ''' try: for x in running_instances: if f"{base}/{quote}"==x.pair: x.pause = True #x.no_of_safety_orders += int(amount) x.config_dict["no_of_safety_orders"]+=int(amount) broker.logger.log_this("Recalculating safety price table...",1,f"{base}/{quote}") x.safety_price_table = x.calculate_safety_prices(x.start_price,x.config_dict["no_of_safety_orders"],x.config_dict["safety_order_deviance"]) broker.logger.log_this(f"Done. Added {amount} safety orders",1,f"{base}/{quote}") x.update_status(True) x.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,f"{base}/{quote}") return jsonify({"Error": "Error adding safety orders"}) 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. ''' try: for x in running_instances: if f"{base}/{quote}"==x.pair: x.config_dict["tp_level"]=float(amount) broker.logger.log_this("Done. The change will take effect when the next take profit order is placed",2,f"{base}/{quote}") 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,f"{base}/{quote}") return jsonify({"Error": "Error changing percentage"}) 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. ''' for x in running_instances: try: x.config_dict["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. ''' try: if f"{base}{quote}" in broker.get_pairs(): #read_config["pairs"].remove(base+quote) for x in running_instances: if f"{base}/{quote}"==x.pair: x.stop_when_profit = not x.stop_when_profit x.update_status(True) if x.stop_when_profit: 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. Like a VCR. ''' 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 x in running_instances: if f"{base}{quote}"==x.pair: x.config_dict["stop_time"] = limit #save config file to disk x.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. ''' try: toggle_pauses.append(f"{base}/{quote}") for instance in running_instances: if instance.pair==f"{base}/{quote}": if instance.pause: return jsonify({"Success": "Trader will be resumed"}) 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. ''' try: if broker.get_pairs!=[]: broker.clear_pairs() for x in running_instances: x.stop_when_profit = True broker.logger.log_this("Modified flag",2,f"{x.base}/{x.quote}") return jsonify({"Success": "All traders scheduled to go offline when profit is reached"}) else: for x in running_instances: x.stop_when_profit = False broker.logger.log_this("Modified flag",2,f"{x.base}/{x.quote}") broker.add_pair_to_config(f"{x.base}{x.quote}") return jsonify({"Success": "Last call canceled"}) 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 in the meantime. I do not recommend to use this, it's preferable to switch to short mode, but you do you. Maybe it's more useful in a bull market's high volatility moment. ''' for x in running_instances: if f"{base}/{quote}"==x.pair: if x.is_short: return jsonify({"Error": "Quote can't be added to short bots"}) x.pause = True new_average_price = (x.total_amount_of_quote+float(amount))/(x.total_amount_of_base+(float(amount)/x.status_dict["price"])) broker.logger.log_this(f"Your new average buy price will be {new_average_price} {x.quote}",2,f"{base}/{quote}") broker.logger.log_this(f"Your new take profit price price will be {new_average_price*x.get_tp_level()} {x.quote}",2,f"{base}/{quote}") new_order = broker.new_market_order(x.pair,float(amount),"buy") if new_order is None: broker.logger.log_this("Error: Market order returned None",2,f"{base}/{quote}") x.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"],x.pair) if returned_order==broker.empty_order: broker.logger.log_this("Problems sending the order",2,f"{base}/{quote}") x.pause = False return jsonify({"Error": "Problems sending the order"}) elif returned_order["status"]=="expired": x.pause = False return jsonify({"Error": "New order expired"}) elif returned_order["status"]=="closed": broker.logger.log_this("Order sent",2,f"{base}/{quote}") new_fees_in_base, new_fees_in_quote = x.parse_fees(returned_order) x.fees_paid_in_base += new_fees_in_base x.fees_paid_in_quote += new_fees_in_quote x.total_amount_of_base = x.total_amount_of_base + returned_order["filled"] - new_fees_in_base x.total_amount_of_quote += returned_order["cost"] broker.logger.log_this("Cancelling old take profit order and sending a new one",2,f"{base}/{quote}") attempts = 5 while broker.cancel_order(x.tp_order["id"],x.pair)==1: broker.logger.log_this("Can't cancel old take profit order, retrying...",2,f"{base}/{quote}") time.sleep(broker.get_wait_time()) attempts-=1 if attempts==0: broker.logger.log_this("Can't cancel old take profit order, cancelling...",2,f"{base}/{quote}") x.pause = False return jsonify({"Error": "Can't cancel old take profit order."}) x.take_profit_price = x.total_amount_of_quote/x.total_amount_of_base*x.get_tp_level() x.tp_order = broker.new_limit_order(x.pair,x.total_amount_of_base,"sell",x.take_profit_price) x.update_status(True) break else: broker.logger.log_this("Waiting for initial order to get filled",2,f"{base}/{quote}") broker.logger.log_this(f"{returned_order}",2,f"{base}/{quote}") time.sleep(broker.get_wait_time()) x.pause = False broker.logger.log_this("Done",2,f"{base}/{quote}") 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 ''' try: missing_pairs = broker.get_pairs() for trader in running_instances: 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_paused_traders(): # ''' # Returns a list of paused pairs # ''' # return jsonify({"paused_traders":global_status["paused_traders"]}) def unwrapped_toggle_cleanup(base,quote): try: pair_to_toggle = f"{base}/{quote}" for x in running_instances: if pair_to_toggle==x.pair: x.config_dict["cleanup"] = not x.config_dict["cleanup"] if x.config_dict["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,f"{base}{quote}") return jsonify({"Error": "Halp"}) return jsonify({"Error": "Task succesfully failed"}) def unwrapped_toggle_autoswitch(base,quote): try: pair_to_toggle = f"{base}/{quote}" for x in running_instances: if pair_to_toggle==x.pair: if x.config_dict["autoswitch"]: broker.logger.log_this("Autoswitch turned OFF",1,f"{base}/{quote}") x.config_dict["autoswitch"] = False return jsonify({"Success": "Autoswitch it's now OFF"}) else: broker.logger.log_this("Autoswitch turned ON",1,f"{base}/{quote}") x.config_dict["autoswitch"] = True return jsonify({"Success": "Autoswitch it's now ON"}) except Exception as e: broker.logger.log_this(f"Exception while toggling autoswitch: {e}",1,f"{base}{quote}") return jsonify({"Error": "Halp"}) def unwrapped_toggle_check_old_long_price(base,quote): try: pair_to_toggle = f"{base}/{quote}" for x in running_instances: if pair_to_toggle==x.pair: if x.config_dict["check_old_long_price"]: broker.logger.log_this("Check OFF",1,f"{base}/{quote}") x.config_dict["check_old_long_price"] = False return jsonify({"Success": "Old long price check turned OFF"}) else: broker.logger.log_this("Check ON",1,f"{base}/{quote}") x.config_dict["check_old_long_price"] = True return jsonify({"Success": "Old long price check turned ON"}) except Exception as e: broker.logger.log_this(f"Exception while toggling check_old_long_price: {e}",1,f"{base}{quote}") return jsonify({"Error": "Halp"}) def unwrapped_switch_quote_currency(base,quote,new_quote): try: pair_to_switch = f"{base}/{quote}" for trader in running_instances: if pair_to_switch==trader.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,f"{base}{quote}") return jsonify({"Error": "Halp"}) def unwrapped_toggle_restart(): new_config = broker.get_config() new_config["attempt_to_restart"] = not new_config["attempt_to_restart"] broker.set_config(new_config) return jsonify({"Success": "attempt_to_restart toggled successfully"}) def unwrapped_toggle_telegram(): 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(): return jsonify({"Time": time.time()}) def unwrapped_trader_time(): try: return jsonify({"Time": max(x.last_time_seen for x in running_instances)}) 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): broker.set_lap_time(wait_time) broker.logger.log_this("Done!") return jsonify({"Success": "Lap time modified successfully"}) def unwrapped_call_wait_time(wait_time): ''' Modifies the time between some API calls and retries. ''' broker.set_wait_time(wait_time) broker.logger.log_this("Done!") return jsonify({"Success": "Call wait time modified successfully"}) def unwrapped_reload_markets(): 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_reload_safety_order(base,quote): try: for trader in running_instances: if trader.pair==f"{base}/{quote}": trader.reload_safety_order() return jsonify({"Success": "Safety order reloaded successfully"}) except Exception as e: broker.logger.log_this(f"Exception while reloading safety order: {e}",1) return jsonify({"Error": "Safety order couldn't be reloaded"}) 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(sys.argv[1]) as f: read_config = json.load(f) except Exception as e: print(e) print("Wrong syntax. Correct syntax is 'python3 dcaruntime.py xxxxx.json', xxxxx.json being the config file.") os._exit(1) #Check for import or load import_mode = True if "--first_start" in sys.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,sys.argv[1]) #Also passes the config filename #Declaring some variables running_instances = [] open_orders = [] instances_to_add = [] online_pairs = [] toggle_pauses = [] tickers = [] threads = [] screen_buffer = [] worker_status = {} 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(): initialize_instance(x) 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(): import_instance(x) broker.logger.log_this(f"All instances imported!",2) #Threads to run: main loop and flask api main_threads = [Thread(target=main_loop,args=()),Thread(target=run_API,args=())] #Iterate indefinitely: for m in main_threads: m.start()