Initial commit
This commit is contained in:
commit
a8daf9c8b1
|
|
@ -0,0 +1,14 @@
|
|||
__pycache__/
|
||||
.vscode/
|
||||
api_credentials.db
|
||||
credentials.py
|
||||
DCAv2.code-workspace
|
||||
profits/my_database.db
|
||||
profits/profits_database.db
|
||||
utils/certs/
|
||||
utils/__pycache__/
|
||||
utils/close.py
|
||||
utils/credentials.py
|
||||
utils/set_exchange.py
|
||||
utils/stuff.py
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"exchange": "binance",
|
||||
"is_sandbox": true,
|
||||
"simulate_market_orders": false,
|
||||
"key": "",
|
||||
"secret": "",
|
||||
"pairs": [
|
||||
"BTCUSDT",
|
||||
"BNBUSDT",
|
||||
"ETHUSDT",
|
||||
"XRPUSDT",
|
||||
"TRXUSDT",
|
||||
"LTCUSDT"
|
||||
],
|
||||
"reconnect": 30,
|
||||
"lap_time": 1,
|
||||
"host": "0.0.0.0",
|
||||
"port": "5006",
|
||||
"telegram": false,
|
||||
"bot_token": "",
|
||||
"bot_chatID": "",
|
||||
"attempt_to_restart": true,
|
||||
"default_order_size": 15,
|
||||
"unified_order_query": true
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"pair": "BCC/USDT",
|
||||
"order_size": 15,
|
||||
"tp_level": 1.02,
|
||||
"no_of_safety_orders": 23,
|
||||
"safety_order_deviance": 2,
|
||||
"safety_order_scale": 0.0105,
|
||||
"write_logs": false,
|
||||
"calculate_fees": true,
|
||||
"cleanup": true,
|
||||
"telegram": true,
|
||||
"tp_mode": 3,
|
||||
"tp_table": [],
|
||||
"is_short": true,
|
||||
"autoswitch": true,
|
||||
"attempt_restart": true,
|
||||
"check_old_long_price": true,
|
||||
"dynamic_so_deviance": true,
|
||||
"dsd_range": 1,
|
||||
"bias": -0.5
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
import time
|
||||
import json
|
||||
import csv
|
||||
|
||||
class duster:
|
||||
def __init__(self,broker,status_info,current_price,importing=False):
|
||||
'''
|
||||
Initializes duster.
|
||||
|
||||
broker: broker object from exchange_wrapper
|
||||
status_info: old status_dict dictionary of the trader or old duster_status from an old duster
|
||||
importing: Boolean value, if True, the status_info dictionary is already formated as a duster. If not, it's formatted as an old status_dict dictionary from a trader.
|
||||
'''
|
||||
|
||||
self.broker = broker
|
||||
if not importing:
|
||||
order = self.broker.get_order(status_info["tp_order_id"],status_info["market"]["symbol"])
|
||||
self.duster_status = {"duster_id": status_info["tp_order_id"],
|
||||
"pair": status_info["market"]["symbol"],
|
||||
"amount_spent": status_info["quote_spent"],
|
||||
"current_price": current_price,
|
||||
"deal_close_price": order["price"],
|
||||
"volume_on_close": order["price"]*order["amount"],
|
||||
"start_time": time.time()
|
||||
}
|
||||
else:
|
||||
self.duster_status = status_info
|
||||
|
||||
self.quit = False
|
||||
self.pause = False #Maybe useful in the future
|
||||
|
||||
|
||||
def __str__(self):
|
||||
'''
|
||||
BASE/QUOTE | order_id followed | current_price | deal_close_price | total_volume_on_close | pct_to_profit | uptime
|
||||
'''
|
||||
|
||||
yellow = "\033[0;33;40m"
|
||||
green = "\033[0;32;40m"
|
||||
red = "\033[0;31;40m"
|
||||
cyan = "\033[0;36;40m"
|
||||
white = "\033[0;37;40m"
|
||||
|
||||
decimals = 11
|
||||
low_percentage = 1
|
||||
mid_percentage = 10
|
||||
high_percentage = 20
|
||||
|
||||
#safety_order_string = f"{x.status_dict['so_amount']-1}/{x.config_dict['no_of_safety_orders']}".rjust(5)
|
||||
|
||||
#Check if necessary
|
||||
mid_price = 0
|
||||
high_price = 0
|
||||
if self.duster_status["price"] is not None:
|
||||
mid_price = self.duster_status["current_price"]
|
||||
if self.duster_status["deal_close_price"] is not None:
|
||||
high_price = self.duster_status["deal_close_price"]
|
||||
|
||||
mid_boundary = '{:.20f}'.format(mid_price)[:decimals].center(decimals)
|
||||
high_boundary = '{:.20f}'.format(high_price)[:decimals].center(decimals)
|
||||
percentage_to_profit = 100
|
||||
pct_to_profit_str = "XX.XX"
|
||||
if self.duster_status not in [0,None] and self.duster_status["current_price"]!=0:
|
||||
diff = abs(self.duster_status["deal_close_price"]-self.duster_status["current_price"])
|
||||
percentage_to_profit = diff/self.duster_status["current_price"]*100
|
||||
|
||||
#Formatting (on-screen percentage not longer than 4 digits)
|
||||
pct_to_profit_str = "{:.2f}".format(percentage_to_profit)
|
||||
if len(pct_to_profit_str)==4:
|
||||
pct_to_profit_str = f" {pct_to_profit_str}"
|
||||
elif len(pct_to_profit_str)==6:
|
||||
pct_to_profit_str = pct_to_profit_str[:5]
|
||||
|
||||
p = "*PAUSED*" if self.pause==True else ""
|
||||
price_color = white
|
||||
pair_color = cyan
|
||||
|
||||
#Set percentage's color
|
||||
pct_color = white
|
||||
if percentage_to_profit<low_percentage:
|
||||
pct_color = green
|
||||
if percentage_to_profit>mid_percentage:
|
||||
pct_color = yellow
|
||||
if percentage_to_profit>high_percentage:
|
||||
pct_color = red
|
||||
|
||||
prices = f"{price_color}{mid_boundary}{white}|{green}{high_boundary}{white}"
|
||||
percentage = f"{pct_color}{pct_to_profit_str}%{white}"
|
||||
return f"{p}{pair_color}{self.duster_status['pair'].center(13)}{white}| {self.duster_status['id']} |{prices}| {self.duster_status['volume_on_close']} | {percentage} | Uptime: {self.seconds_to_time(time.time()-self.duster_status['start_time'])}"
|
||||
|
||||
|
||||
def set_current_price(self,price: float):
|
||||
'''
|
||||
Writes the current price on the status dictionary
|
||||
'''
|
||||
try:
|
||||
self.duster_status["current_price"] = float(price)
|
||||
except Exception as e:
|
||||
self.broker.logger.log_this(f"Error in duster. Set_current_price received a wrong value. {e}",1,self.duster_status["pair"])
|
||||
return 0
|
||||
|
||||
|
||||
def get_duster_status(self):
|
||||
'''
|
||||
Returns the status dictionary
|
||||
'''
|
||||
|
||||
return self.duster_status
|
||||
|
||||
|
||||
def save_duster_status(self):
|
||||
'''
|
||||
Save duster status in a file.
|
||||
'''
|
||||
|
||||
try:
|
||||
with open(f"status/{self.duster_status['id']}.duster_status","w") as status_file:
|
||||
status_file.write(json.dumps(self.duster_status, indent=4))
|
||||
return 0
|
||||
except Exception as e:
|
||||
self.broker.logger.log_this(f"Exception while writing duster status file: {e}",1,self.duster_status["pair"])
|
||||
return 1
|
||||
|
||||
|
||||
def load_duster_status(self):
|
||||
'''
|
||||
Loads duster status to the status dictionary.
|
||||
'''
|
||||
|
||||
try:
|
||||
with open(f"status/{self.duster_status['id']}.duster_status") as status_file:
|
||||
self.duster_status = json.load(status_file)
|
||||
return 0
|
||||
except Exception as e:
|
||||
self.broker.logger.log_this(f"Exception while reading duster status file: {e}",1,self.duster_status["pair"])
|
||||
return 1
|
||||
|
||||
|
||||
def check_duster_status(self,open_orders):
|
||||
'''
|
||||
Checks duster status
|
||||
'''
|
||||
|
||||
if open_orders==[] or self.pause:
|
||||
return 0
|
||||
|
||||
if self.duster_status["id"] not in open_orders:
|
||||
order = self.broker.get_order(self.duster_status["id"],self.duster_status["pair"])
|
||||
if order["status"]=="":
|
||||
self.broker.logger.log_this(f"In check_duster_status, get_order returned an empty order",1,self.duster_status["pair"])
|
||||
return 1
|
||||
elif order["status"]=="closed":
|
||||
self.report_profit(order)
|
||||
self.quit = True
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def report_profit(self,closed_order):
|
||||
'''
|
||||
Saves profit in the profit file and sends the telegram notification.
|
||||
'''
|
||||
|
||||
_, fees_paid = self.parse_fees(closed_order)
|
||||
profit = closed_order["cost"]-self.duster_status["amount_spent"]-fees_paid
|
||||
|
||||
#Write to file
|
||||
profit_filename = f"profits/{self.duster_status['pair'].remove('/')}.profit"
|
||||
try:
|
||||
with open(profit_filename,"a") as profit_file:
|
||||
profit_writer = csv.writer(profit_file, delimiter=",")
|
||||
profit_writer.writerow([time.strftime("%Y-%m-%d"), profit, closed_order["id"]])
|
||||
except Exception as e:
|
||||
self.broker.logger.log_this(f"Exception in profit_to_file: {e}",1,self.duster_status["pair"])
|
||||
|
||||
#Send notification
|
||||
self.broker.logger.log_this(f"Duster closed a deal. Profit: {'{:.4f}'.format(profit)} {self.duster_status['pair'].split('/')[1]}",0,self.duster_status["pair"])
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def parse_fees(self,order):
|
||||
'''
|
||||
Returns the fees paid ordered in "base,quote"
|
||||
'''
|
||||
basefee = 0
|
||||
quotefee = 0
|
||||
base,quote = self.duster_status["pair"].split("/")
|
||||
|
||||
if self.broker.get_exchange_name()=="binance": #CCXT still to this day does not take Binance fees into account.
|
||||
try:
|
||||
fee_rate = self.broker.fetch_market["maker"] if order["type"]=="limit" else self.broker.fetch_market["taker"]
|
||||
except Exception as e:
|
||||
self.broker.logger.log_this(f"Exception fetching market information: {e}. Using default fee rate of 0.1%",1,f"{base}{quote}")
|
||||
fee_rate = 0.001
|
||||
if order["side"]=="buy":
|
||||
basefee = order["filled"]*float(fee_rate)
|
||||
elif order["side"]=="sell":
|
||||
#To be implemented
|
||||
#Maybe:
|
||||
#quotefee = order["cost"]*float(fee_rate)
|
||||
pass
|
||||
return basefee,quotefee
|
||||
|
||||
for x in order["fees"]:
|
||||
if x["currency"]==base:
|
||||
basefee+=float(x["cost"])
|
||||
if x["currency"]==quote:
|
||||
quotefee+=float(x["cost"])
|
||||
return basefee,quotefee
|
||||
|
||||
|
||||
def seconds_to_time(self,total_seconds):
|
||||
'''
|
||||
Takes an int or float as an input and it returns a D:HH:MM:SS formatted string.
|
||||
'''
|
||||
return f"{int(total_seconds/86400)}:" + '%02d:%02d:%02d' % (int((total_seconds % 86400) / 3600), int((total_seconds % 3600) / 60), int(total_seconds % 60))
|
||||
|
|
@ -0,0 +1,928 @@
|
|||
import json
|
||||
import time
|
||||
import requests
|
||||
import credentials
|
||||
import sqlite3
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
class broker:
|
||||
def __init__(self,exchange,read_config,config_filename):
|
||||
self.config_filename = config_filename
|
||||
self.read_config = read_config
|
||||
self.exchange = exchange
|
||||
self.last_price = 0
|
||||
self.wait_time = 1 #Default wait time for API breathing room
|
||||
self.cooldown_multiplier = 2 #Cooldown multiplier of the value above between trader restarts or when slippage is exceeded (this should be in the config file)
|
||||
self.empty_order = {"id": "", "status": "", "filled": 0, "remaining": 0, "price": 0, "cost": 0, "fees": [], "symbol": ""}
|
||||
self.retries = read_config["retries"] if "retries" in self.read_config else 10
|
||||
self.slippage_default_threshold = self.read_config["slippage_default_threshold"] if "slippage_default_threshold" in read_config else .03
|
||||
self.logger = logger(self.read_config)
|
||||
self.write_order_history = True #This should be a toggle in config_file
|
||||
|
||||
self.database_connection = sqlite3.connect("profits/profits_database.db")
|
||||
self.database_cursor = self.database_connection.cursor()
|
||||
self.database_cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS profits_table (
|
||||
timestamp REAL PRIMARY KEY,
|
||||
pair TEXT,
|
||||
amount REAL,
|
||||
exchange_name TEXT,
|
||||
order_id TEXT,
|
||||
order_history TEXT
|
||||
)
|
||||
''')
|
||||
self.database_connection.commit()
|
||||
self.database_connection.close()
|
||||
|
||||
self.exchange.load_markets()
|
||||
|
||||
|
||||
def all_markets(self,no_retries=False):
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
return self.exchange.load_markets()
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in reload_markets: {e}")
|
||||
if no_retries:
|
||||
break
|
||||
retries-=1
|
||||
time.sleep(self.wait_time)
|
||||
return {}
|
||||
|
||||
|
||||
def reload_markets(self):
|
||||
try:
|
||||
self.exchange.load_markets(reload=True)
|
||||
return 0
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in reload_markets: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
def write_profit_to_db(self,dataset,no_retries=False):
|
||||
'''
|
||||
dataset format: (timestamp,pair,amount,exchange_name,order_id,order_history)
|
||||
'''
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
database_connection = sqlite3.connect("profits/profits_database.db")
|
||||
database_cursor = database_connection.cursor()
|
||||
database_cursor.execute('INSERT INTO profits_table VALUES(?, ?, ?, ?, ?, ?)', dataset)
|
||||
database_connection.commit()
|
||||
database_connection.close()
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in write_profit_to_db: {e}")
|
||||
if no_retries:
|
||||
break
|
||||
retries-=1
|
||||
time.sleep(self.wait_time)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
def check_for_duplicate_profit_in_db(self,order,no_retries=False):
|
||||
'''
|
||||
SQLite implementation of check_for_duplicate_profit():
|
||||
Compares the id of the last profit order with the one in the database.
|
||||
'''
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
database_connection = sqlite3.connect("profits/profits_database.db")
|
||||
database_cursor = database_connection.cursor()
|
||||
database_cursor.execute(f"SELECT * FROM profits_table WHERE pair = '{order['symbol']}' ORDER BY timestamp DESC LIMIT 1;")
|
||||
rows = database_cursor.fetchall()
|
||||
database_connection.close()
|
||||
if rows==[]:
|
||||
return False
|
||||
return order["id"]==rows[0][4]
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in check_for_duplicate_profit_in_db: {e}",1)
|
||||
if no_retries:
|
||||
break
|
||||
retries-=1
|
||||
time.sleep(self.wait_time)
|
||||
return False
|
||||
|
||||
|
||||
def get_write_order_history(self):
|
||||
return self.write_order_history
|
||||
|
||||
def get_cooldown_multiplier(self):
|
||||
return self.cooldown_multiplier
|
||||
|
||||
def set_cooldown_multiplier(self, value:int):
|
||||
self.cooldown_multiplier = value
|
||||
return 0
|
||||
|
||||
def get_default_order_size(self):
|
||||
return self.read_config["default_order_size"]
|
||||
|
||||
|
||||
def set_default_order_size(self,size):
|
||||
try:
|
||||
self.read_config["default_order_size"] = float(size)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in set_default_order_size: {e}",1)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def get_slippage_default_threshold(self):
|
||||
return self.slippage_default_threshold
|
||||
|
||||
|
||||
def set_slippage_default_threshold(self,threshold):
|
||||
try:
|
||||
self.slippage_default_threshold = float(threshold)
|
||||
return 0
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in set_slippage_default_threshold: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
def get_retries(self):
|
||||
return self.retries
|
||||
|
||||
|
||||
def set_retries(self,amount):
|
||||
try:
|
||||
self.retries = int(amount)
|
||||
return 0
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in set_retries: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
def get_empty_order(self):
|
||||
return self.empty_order
|
||||
|
||||
|
||||
def get_exchange_name(self):
|
||||
return self.read_config["exchange"]
|
||||
|
||||
|
||||
def set_wait_time(self,sec):
|
||||
'''
|
||||
Sets the default wait time between some API calls
|
||||
'''
|
||||
try:
|
||||
new_time = float(sec)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in set_wait_time: {e}")
|
||||
return 1
|
||||
self.wait_time = new_time
|
||||
return 0
|
||||
|
||||
|
||||
def get_wait_time(self):
|
||||
'''
|
||||
Returns the default wait time between some API calls
|
||||
'''
|
||||
return self.wait_time
|
||||
|
||||
|
||||
def get_config(self):
|
||||
return deepcopy(self.read_config)
|
||||
|
||||
|
||||
def set_config(self,new_config):
|
||||
self.read_config = deepcopy(new_config)
|
||||
return 0
|
||||
|
||||
|
||||
def reload_config_file(self):
|
||||
try:
|
||||
with open(self.config_filename) as f:
|
||||
self.read_config = json.load(f)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception while reading the config file: {e}",1)
|
||||
|
||||
|
||||
def add_pair_to_config(self,pair):
|
||||
if pair not in self.read_config["pairs"]:
|
||||
self.read_config["pairs"].append(pair)
|
||||
return 0
|
||||
return 1
|
||||
|
||||
|
||||
def remove_pair_from_config(self,pair):
|
||||
try:
|
||||
if pair in self.read_config["pairs"]:
|
||||
self.read_config["pairs"].remove(pair)
|
||||
return 0
|
||||
self.logger.log_this("Pair does not exist - Can't remove from read_config",1,pair)
|
||||
return 2
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Problems removing pair: {e}",1,pair)
|
||||
return 1
|
||||
|
||||
|
||||
def get_pairs(self):
|
||||
return self.read_config["pairs"]
|
||||
|
||||
|
||||
def clear_pairs(self):
|
||||
self.read_config["pairs"].clear()
|
||||
return 0
|
||||
|
||||
|
||||
def get_lap_time(self):
|
||||
return self.read_config["lap_time"]
|
||||
|
||||
|
||||
def set_lap_time(self,new_lap_time):
|
||||
try:
|
||||
self.read_config["lap_time"]=float(new_lap_time)
|
||||
return 0
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Can't set new lap time. {new_lap_time} is an invalid entry. Exception: {e}",1)
|
||||
return 1
|
||||
|
||||
|
||||
def rewrite_config_file(self):
|
||||
try:
|
||||
with open(f"{self.config_filename}","w") as f:
|
||||
f.write(json.dumps(self.read_config, indent=4))
|
||||
return 0
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Problems writing the config file. Exception: {e}",1)
|
||||
return 1
|
||||
|
||||
|
||||
def get_order_book(self,symbol,no_retries=False):
|
||||
'''
|
||||
Returns the complete orderbook
|
||||
'''
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
return self.exchange.fetch_order_book(symbol)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_order_book: {e}",1)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
retries-=1
|
||||
return {}
|
||||
|
||||
|
||||
def find_minimum_viable_price(self,order_book,amount,side):
|
||||
suma = 0
|
||||
data = order_book["bids"] if side=="sell" else order_book["asks"]
|
||||
for x in data:
|
||||
suma += x[1]
|
||||
if suma>=amount:
|
||||
return x[0]
|
||||
|
||||
|
||||
def get_prices(self,pair_list=None,no_retries=False):
|
||||
'''
|
||||
Returns the closing prices of all the pairs in the pair_list list
|
||||
|
||||
:param pair_list: list of pairs to get prices for
|
||||
:param no_retries: if True, the function will not retry if it fails
|
||||
:return: dictionary {pair: price}
|
||||
'''
|
||||
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
if self.read_config["exchange"]=="binance":
|
||||
a = self.exchange.fetch_last_prices(pair_list)
|
||||
return {x: a[x]["price"] for x in a.keys()}
|
||||
else:
|
||||
#a = self.exchange.fetch_tickers(pair_list)
|
||||
a = self.exchange.fetch_tickers()
|
||||
#return {x.upper(): a[x]["close"] for x in a.keys() if x.upper() in pair_list}
|
||||
if pair_list is None:
|
||||
return {x: a[x]["close"] for x in a.keys()}
|
||||
return {x: a[x]["close"] for x in a.keys() if x in pair_list}
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_prices: {e}",1)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
retries-=1
|
||||
return {}
|
||||
|
||||
|
||||
def get_ticker_price(self,symbol,no_retries=False):
|
||||
'''
|
||||
Returns the closing price of a trading pair
|
||||
|
||||
:param symbol: trading pair symbol
|
||||
:param no_retries: if True, will not retry if exception occurs
|
||||
:return: closing price of trading pair
|
||||
'''
|
||||
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
pair = symbol
|
||||
a = self.exchange.fetch_ticker(pair)
|
||||
self.last_price = a["close"]
|
||||
return self.last_price
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_ticker_price: {e}",1)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
retries-=1
|
||||
return self.last_price
|
||||
|
||||
|
||||
def get_mid_price(self,symbol):
|
||||
'''
|
||||
Retrieves the orderbook and returns the average price [(top bid + top ask)/2]
|
||||
|
||||
:param symbol: the symbol to get the mid price for
|
||||
:return: the mid price
|
||||
'''
|
||||
|
||||
orderbook = self.get_order_book(symbol)
|
||||
if orderbook=={}:
|
||||
self.logger.log_this("Can't fetch orderbook (from get_mid_price)",1,symbol)
|
||||
return self.get_ticker_price(symbol)
|
||||
try:
|
||||
mid_price = (orderbook["asks"][0][0]+orderbook["bids"][0][0])/2
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception getting mid_price: {e}",1,symbol)
|
||||
return self.get_ticker_price(symbol)
|
||||
return self.price_to_precision(symbol,mid_price)
|
||||
|
||||
|
||||
def get_coins_balance(self,no_retries=False):
|
||||
'''
|
||||
Retrieves the balance of all coins on the exchange
|
||||
|
||||
:param no_retries: if True, it will not retry on failure
|
||||
:return: list of all coins and their balance on the exchange
|
||||
'''
|
||||
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
return self.exchange.fetch_balance()
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_coins_balance: {e}",1)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
retries-=1
|
||||
return []
|
||||
|
||||
|
||||
def fetch_full_orders(self,pairs=None) -> list:
|
||||
'''
|
||||
Returns a list of all orders on the exchange
|
||||
|
||||
:param pairs: list of pairs to get orders for
|
||||
:return: list of orders
|
||||
'''
|
||||
|
||||
if pairs is None:
|
||||
pairs = []
|
||||
try:
|
||||
orders = []
|
||||
if self.read_config["exchange"]=="binance":
|
||||
orders = self.get_opened_orders_binance(pairs)
|
||||
else:
|
||||
orders = self.get_opened_orders()
|
||||
return [] if orders is None else orders
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in fetch_full_orders: {e}",2)
|
||||
return []
|
||||
|
||||
|
||||
def fetch_open_orders(self,pairs=None) -> list:
|
||||
'''
|
||||
Returns a list of IDs of all open orders on the exchange
|
||||
|
||||
:param pairs: list of pairs to get opened orders
|
||||
:return: list of IDs of all open orders
|
||||
'''
|
||||
|
||||
if pairs is None:
|
||||
pairs = []
|
||||
try:
|
||||
id_list = []
|
||||
if self.read_config["exchange"]=="binance":
|
||||
orders = self.get_opened_orders_binance(pairs)
|
||||
else:
|
||||
orders = self.get_opened_orders()
|
||||
if orders!=[]:
|
||||
id_list.extend(x["id"] for x in orders)
|
||||
return id_list
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in fetch_open_orders: {e}",2)
|
||||
return []
|
||||
|
||||
|
||||
def get_opened_orders(self,no_retries=False): #It should return a list of all opened orders
|
||||
'''
|
||||
Returns a list of all the orders on the exchange
|
||||
|
||||
:param pairs: list of pairs
|
||||
:return: list of all the open orders on the exchange
|
||||
'''
|
||||
|
||||
retries = self.retries
|
||||
while retries>0:
|
||||
try:
|
||||
return self.exchange.fetch_open_orders()
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_opened_orders: {e}",1)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
retries-=1
|
||||
return []
|
||||
|
||||
|
||||
def get_opened_orders_binance(self,pairs):
|
||||
'''
|
||||
Returns a list of all the open orders on the exchange
|
||||
|
||||
:param pairs: list of pairs
|
||||
:return: list of all the open orders on the exchange
|
||||
'''
|
||||
|
||||
try:
|
||||
if "unified_order_query" in self.read_config and self.read_config["unified_order_query"] is True:
|
||||
return self.exchange.fetch_open_orders()
|
||||
result = []
|
||||
for pair in pairs:
|
||||
a = self.exchange.fetch_open_orders(pair)
|
||||
result.extend(iter(a))
|
||||
return result
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_opened_orders_binance: {e}",1)
|
||||
return []
|
||||
|
||||
|
||||
def cancel_order(self,id,symbol,no_retries=False):
|
||||
'''
|
||||
Receives an order id and cancels the corresponding order
|
||||
|
||||
:param id: order id
|
||||
:param symbol: pair
|
||||
:param no_retries: if True, the function will not retry to cancel the order
|
||||
:return: 0 if order was succesfully canceled, 1 if not
|
||||
'''
|
||||
|
||||
pair = symbol
|
||||
tries = self.retries//2
|
||||
while tries>0:
|
||||
try:
|
||||
while self.get_order(id,pair)["status"]=="open":
|
||||
self.exchange.cancel_order(id,symbol=pair)
|
||||
time.sleep(self.wait_time)
|
||||
return 0
|
||||
except Exception as e:
|
||||
if self.get_order(id,pair)["status"]=="canceled":
|
||||
return 0
|
||||
self.logger.log_this(f"Exception in cancel_order: id {id} - exception: {e}",1)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
tries-=1
|
||||
return 1
|
||||
|
||||
|
||||
def amount_to_precision(self,pair,amount):
|
||||
try:
|
||||
return float(self.exchange.amount_to_precision(pair,amount))
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Can't convert amount {amount} to precision. Exception: {e}",1,pair)
|
||||
return amount
|
||||
|
||||
|
||||
def price_to_precision(self,pair,price):
|
||||
try:
|
||||
return float(self.exchange.price_to_precision(pair,price))
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Can't convert price {price} to precision. Exception: {e}",1,pair)
|
||||
return price
|
||||
|
||||
|
||||
def cost_to_precision(self,pair,amount):
|
||||
try:
|
||||
return float(self.exchange.cost_to_precision(pair,amount))
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Can't convert cost {amount} to precision. Exception: {e}",1,pair)
|
||||
return amount
|
||||
|
||||
|
||||
def new_simulated_market_order(self,symbol,size,side,amount_in_base=False,no_retries=False):
|
||||
'''
|
||||
TODO: Emulating Market Orders With Limit Orders
|
||||
|
||||
It is also possible to emulate a market order with a limit order.
|
||||
|
||||
WARNING this method can be risky due to high volatility, use it at your own risk and only use it when you know really well what you're doing!
|
||||
|
||||
Most of the time a market sell can be emulated with a limit sell at a very low price – the exchange will automatically make it a taker order for market price
|
||||
(the price that is currently in your best interest from the ones that are available in the order book). When the exchange detects that you're selling for a very low price
|
||||
it will automatically offer you the best buyer price available from the order book. That is effectively the same as placing a market sell order. Thus market orders can be
|
||||
emulated with limit orders (where missing).
|
||||
|
||||
The opposite is also true – a market buy can be emulated with a limit buy for a very high price. Most exchanges will again close your order for best available price,
|
||||
that is, the market price.
|
||||
|
||||
However, you should never rely on that entirely, ALWAYS test it with a small amount first! You can try that in their web interface first to verify the logic. You can sell
|
||||
the minimal amount at a specified limit price (an affordable amount to lose, just in case) and then check the actual filling price in trade history.
|
||||
|
||||
:param symbol: The symbol of the asset you want to place a market order for.
|
||||
:param size: The size of the order you want to place.
|
||||
:param side: The side of the order you want to place (buy or sell)
|
||||
:param amount_in_base: Signals is the size parameter is nominated in base or quote currency
|
||||
:param no_retries: If True, the function will not try to fetch the order again if it fails
|
||||
'''
|
||||
|
||||
retries = self.retries//2
|
||||
pair = symbol
|
||||
while retries>0:
|
||||
try:
|
||||
if self.read_config["exchange"]=="gateio" and side=="buy" and not amount_in_base:
|
||||
new_order = self.exchange.create_market_buy_order_with_cost(pair, size)
|
||||
else:
|
||||
order_book = self.get_order_book(symbol)
|
||||
if order_book=={}:
|
||||
self.logger.log_this(f"new_simulated_market_order. Order book returned an empty dictionary",1,symbol)
|
||||
return self.empty_order
|
||||
if amount_in_base or side!="buy":
|
||||
base_amount = self.amount_to_precision(pair,size)
|
||||
else:
|
||||
avg_price = self.average_price_depth(order_book,size,"sell")
|
||||
base_amount = size/avg_price if avg_price is not None else size/self.get_ticker_price(symbol)
|
||||
price = self.find_minimum_viable_price(order_book,base_amount,side)
|
||||
#Maybe check for slippage here instead of within the trader itself? idk
|
||||
new_order = self.exchange.create_order(pair,"limit",side,base_amount,price)
|
||||
time.sleep(self.wait_time)
|
||||
return self.get_order(new_order["id"],pair)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"new_simulated_market_order exception: {e}",1,symbol)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
retries -= 1
|
||||
return self.empty_order
|
||||
|
||||
|
||||
def weighted_average(self,prices,weights):
|
||||
'''
|
||||
Given a list of prices and a list of weights, returns the weighted average of those prices.
|
||||
|
||||
:param prices: list of prices
|
||||
:param weights: list of weights
|
||||
'''
|
||||
|
||||
return sum(prices[i]*weights[i] for i in range(len(prices)))/sum(weights)
|
||||
|
||||
|
||||
def average_price_depth(self,order_book,size,side):
|
||||
'''
|
||||
Given the size of the order in quote, it returns the average price that a market BUY order of that amount would get in the
|
||||
current orderbook.
|
||||
|
||||
:param order_book: the orderbook
|
||||
:param size: the size of the order in quote
|
||||
:param side: the side of the order
|
||||
'''
|
||||
|
||||
quote = 0
|
||||
prices = []
|
||||
weights = []
|
||||
dataset = order_book["asks"] if side=="buy" else order_book["bids"]
|
||||
for x in dataset:
|
||||
prices.append(x[0])
|
||||
weights.append(x[1])
|
||||
quote+=x[1]*x[0]
|
||||
if quote>=size:
|
||||
#Now we calculate the weighted average
|
||||
return self.weighted_average(prices,weights)
|
||||
return None
|
||||
|
||||
|
||||
def new_market_order(self,symbol,size,side,amount_in_base=False,no_retries=False): #It should send a new market order to the exchange
|
||||
'''
|
||||
Sends a new market order to the exchange.
|
||||
|
||||
:param symbol: The symbol of the asset.
|
||||
:param size: The size of the order.
|
||||
:param side: The side of the order.
|
||||
:param amount_in_base: Whether the amount is nominated in base or quote currency.
|
||||
:param no_retries: If True, the function will not try to fetch the order again if it fails
|
||||
'''
|
||||
|
||||
if self.read_config["simulate_market_orders"]:
|
||||
return self.new_simulated_market_order(symbol,size,side,amount_in_base=amount_in_base)
|
||||
retries = self.retries
|
||||
pair = symbol
|
||||
while retries>0:
|
||||
try:
|
||||
if side=="buy":
|
||||
to_buy = float(size)
|
||||
if not amount_in_base:
|
||||
order_book = self.get_order_book(symbol)
|
||||
if order_book=={}:
|
||||
self.logger.log_this(f"new_market_order - Orderbook request returned an empty dictionary - Not using orderbook. Switching to a less precise method.",1,symbol)
|
||||
price = self.get_ticker_price(symbol)
|
||||
else:
|
||||
price = self.average_price_depth(order_book,size,side)
|
||||
if price is None:
|
||||
#Something failed, back to the basics
|
||||
self.logger.log_this(f"new_market_order - average_price_depth returned None. Switching to a less precise method.",1,symbol)
|
||||
price = self.get_ticker_price(symbol)
|
||||
to_buy = float(size)/price
|
||||
amount = self.amount_to_precision(pair,to_buy)
|
||||
else:
|
||||
amount = self.amount_to_precision(pair,size) #Market sell orders are ALWAYS nominated in baseexchange.create_order(pair,"market",side,amount)
|
||||
|
||||
#self.logger.log_this(f"Order to be sent: {side} {amount}",1,pair)
|
||||
order_to_send = self.exchange.create_order(pair,"market",side,amount)
|
||||
time.sleep(self.wait_time)
|
||||
return self.get_order(order_to_send["id"],pair)
|
||||
# Because Kucoin "order does not exist" problem
|
||||
#if order_to_send["amount"] is not None: #
|
||||
# return self.get_order(order_to_send["id"],pair) #
|
||||
#self.logger.log_this(f"Error sending order: Null order returned",2,pair) #
|
||||
#self.cancel_order(order_to_send["id"],symbol,no_retries=True) #
|
||||
#retries-=1 #
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in new_market_order: {e}",1,pair)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
retries-=1
|
||||
return None
|
||||
|
||||
|
||||
def not_enough_balance_error(self, error_object):
|
||||
'''
|
||||
Checks if the error is a balance error.
|
||||
Receives an error object.
|
||||
Returns True if the error is a balance error, False otherwise.
|
||||
|
||||
:param error_object: The error object.
|
||||
:return: Boolean value.
|
||||
'''
|
||||
|
||||
error_text = str(error_object)
|
||||
return "insufficient" in error_text.lower() or "BALANCE_NOT_ENOUGH" in error_text or "Low available balance" in error_text
|
||||
|
||||
|
||||
def new_limit_order(self,symbol,size,side,price,no_retries=False):
|
||||
'''
|
||||
Sends a new limit order.
|
||||
|
||||
:param symbol: The symbol of the order.
|
||||
:param size: The size of the order.
|
||||
:param side: The side of the order.
|
||||
:param price: The price of the order.
|
||||
:param no_retries: If True, the function will not retry to send the order if there is an error.
|
||||
'''
|
||||
|
||||
tries = self.retries
|
||||
pair = symbol
|
||||
while tries>=0:
|
||||
try:
|
||||
order_to_send = self.exchange.create_order(pair,"limit",side,self.amount_to_precision(pair,size),price)
|
||||
time.sleep(self.wait_time)
|
||||
return self.get_order(order_to_send["id"],pair)
|
||||
#if order_to_send["amount"] is not None: # Because Kucoin etc etc
|
||||
# return self.get_order(order_to_send["id"],pair) #
|
||||
#self.logger.log_this(f"Error sending order: Null order returned",2,pair) #
|
||||
#self.cancel_order(order_to_send["id"],symbol,no_retries=True) #
|
||||
#retries-=1
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in new_limit_order - Side: {side} - Size: {size} - {self.amount_to_precision(pair,size)} - Exception: {e}",1,symbol)
|
||||
if self.not_enough_balance_error(e):
|
||||
if tries<=self.retries//2: #Halves the amount of retries if there is a balance error.
|
||||
return 1
|
||||
if self.get_exchange_name()=="binance":
|
||||
#If the exchange is Binance, it doesn't try that hard to resend the order: Since CCXT doesn't handle binance fees at all,
|
||||
#instead of guesstimating the fees, it's easier and more precise to query the exchange for the remaining base currency
|
||||
#and send the order with that amount.
|
||||
tries-=1
|
||||
if no_retries:
|
||||
break
|
||||
tries-=1
|
||||
time.sleep(self.wait_time)
|
||||
return None
|
||||
|
||||
|
||||
def get_order(self,id,symbol,no_retries=False):
|
||||
'''
|
||||
Gets an order from the exchange.
|
||||
:param id: The id of the order.
|
||||
:param symbol: The symbol of the order.
|
||||
:param no_retries: If True, the function will not try to fetch the order again if it fails.
|
||||
:return: The order.
|
||||
'''
|
||||
if id=="":
|
||||
return self.empty_order
|
||||
tries = self.retries
|
||||
pair = symbol
|
||||
while tries>0:
|
||||
try:
|
||||
return self.exchange.fetch_order(id,symbol=pair)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_order: {e}",1,symbol)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
tries -=1
|
||||
return self.empty_order
|
||||
|
||||
|
||||
def fetch_market(self,symbol,no_retries=False):
|
||||
'''
|
||||
Returns the market.
|
||||
:param symbol: The symbol of the market.
|
||||
:param no_retries: If True, the function will not retry if an exception occurs.
|
||||
:return: The market information.
|
||||
'''
|
||||
tries = self.retries
|
||||
pair = symbol
|
||||
while tries>0:
|
||||
try:
|
||||
return self.exchange.market(pair)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in fetch_market: {e}",1,symbol)
|
||||
if no_retries:
|
||||
break
|
||||
time.sleep(self.wait_time)
|
||||
tries-=1
|
||||
return None
|
||||
|
||||
|
||||
def get_ticker(self,symbol,no_retries=False):
|
||||
'''
|
||||
Returns the ticker information.
|
||||
:param symbol: The trading pair.
|
||||
:param no_retries: If True, the function will not retry if an exception occurs.
|
||||
:return: The ticker information.
|
||||
'''
|
||||
tries = self.retries
|
||||
pair = symbol
|
||||
while tries>0:
|
||||
try:
|
||||
return self.exchange.fetch_ticker(pair)
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_ticker: {e}")
|
||||
if no_retries:
|
||||
break
|
||||
tries-=1
|
||||
time.sleep(self.wait_time)
|
||||
return None
|
||||
|
||||
|
||||
def get_min_base_size(self,pair):
|
||||
'''
|
||||
Returns the minimum order base size that the exchange supports.
|
||||
|
||||
:param pair: pair
|
||||
:return: minimum order base size
|
||||
'''
|
||||
|
||||
market = self.fetch_market(pair)
|
||||
if market is None:
|
||||
return None
|
||||
if self.get_exchange_name() in ["okex","kucoin"]:
|
||||
return float(market["limits"]["amount"]["min"])
|
||||
elif self.get_exchange_name() in ["gateio"]:
|
||||
return (float(market["limits"]["cost"]["min"])+1)/self.get_ticker_price(pair)
|
||||
elif self.get_exchange_name()=="binance":
|
||||
for line in market["info"]["filters"]:
|
||||
if line["filterType"] == "NOTIONAL":
|
||||
return (float(line["minNotional"])+1)/self.get_ticker_price(pair)
|
||||
return None
|
||||
|
||||
|
||||
def get_min_quote_size(self,pair):
|
||||
'''
|
||||
Returns the minimum order size in quote currency.
|
||||
|
||||
:param pair: pair
|
||||
:return: minimum order quote size
|
||||
'''
|
||||
|
||||
market = self.fetch_market(pair)
|
||||
if market is None:
|
||||
return None
|
||||
if self.get_exchange_name()=="binance":
|
||||
for line in market["info"]["filters"]:
|
||||
if line["filterType"] == "NOTIONAL":
|
||||
#return self.broker.amount_to_precision(pair,(float(line["minNotional"])))
|
||||
return float(line["minNotional"])
|
||||
elif self.get_exchange_name()=="gateio":
|
||||
#return self.cost_to_precision(pair,float(market["info"]["min_base_amount"])*self.broker.get_mid_price(pair))
|
||||
return float(market["limits"]["cost"]["min"])
|
||||
elif self.get_exchange_name() in ["okex","kucoin"]:
|
||||
return self.cost_to_precision(pair,float(market["limits"]["amount"]["min"])*self.get_ticker_price(pair))
|
||||
return None
|
||||
|
||||
|
||||
def get_step_size(self,pair):
|
||||
'''
|
||||
Returns the step size of the market
|
||||
|
||||
:param pair: pair
|
||||
:return: step size
|
||||
|
||||
'''
|
||||
market = self.fetch_market(pair)
|
||||
if market is None:
|
||||
return None
|
||||
try:
|
||||
if self.get_exchange_name()=="binance":
|
||||
for filter in market["info"]["filters"]:
|
||||
if filter["filterType"]=="LOT_SIZE":
|
||||
return float(filter["stepSize"])
|
||||
elif self.get_exchange_name()=="kucoin":
|
||||
return float(market["info"]["baseIncrement"])
|
||||
elif self.get_exchange_name() in ["gateio","okex"]:
|
||||
return float(market["precision"]["amount"])
|
||||
except Exception as e:
|
||||
self.logger.log_this(f"Exception in get_step_size: {e}",1,pair)
|
||||
return None
|
||||
|
||||
|
||||
class logger:
|
||||
def __init__(self,broker_config):
|
||||
self.broker_config = broker_config
|
||||
self.exchange_name = self.broker_config["exchange"]
|
||||
self.tg_credentials = credentials.get_credentials("telegram")
|
||||
|
||||
|
||||
def set_telegram_notifications(self, toggle):
|
||||
try:
|
||||
self.broker_config["telegram"] = bool(toggle)
|
||||
except Exception as e:
|
||||
self.log_this(f"Error in set_telegram_notifications",1)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def send_tg_message(self,message,ignore_config=False):
|
||||
'''
|
||||
Sends a Telegram message
|
||||
'''
|
||||
tg_credentials = credentials.get_credentials("telegram")
|
||||
send_text = f"https://api.telegram.org/bot{tg_credentials['token']}/sendMessage?chat_id={tg_credentials['chatid']}&parse_mode=Markdown&text={message}"
|
||||
output = None
|
||||
if self.broker_config["telegram"] or ignore_config:
|
||||
output = requests.get(send_text,timeout=5).json() #5 seconds timeout. This could also be a tunable.
|
||||
if not output["ok"]:
|
||||
self.log_this(f"Error in send_tg_message: {output}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def log_this(self,message,level=2,pair=None):
|
||||
'''
|
||||
Level 0: Screen, log file and Telegram
|
||||
Level 1: Screen and log file
|
||||
Level 2: Screen only
|
||||
'''
|
||||
|
||||
pair_data = "" if pair is None else f"{pair} | "
|
||||
text = time.strftime(f"[%Y/%m/%d %H:%M:%S] | {pair_data}{message}")
|
||||
|
||||
print(text)
|
||||
|
||||
if level<2:
|
||||
try:
|
||||
with open(f"logs/{self.exchange_name}.log","a") as log_file:
|
||||
log_file.write(text+"\n")
|
||||
log_file.close()
|
||||
except Exception as e:
|
||||
print("Can't write log file")
|
||||
print(e)
|
||||
|
||||
if level<1:
|
||||
self.send_tg_message(f"{self.broker_config['exchange'].capitalize()} | {pair_data}{message}",ignore_config=level==-1)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
#def log_this_API_fail(self,message):
|
||||
# '''
|
||||
# Records the message object to a log file.
|
||||
# '''
|
||||
#
|
||||
# try:
|
||||
# with open(f"logs/{self.exchange_name}.api_errors.log","a") as log_file:
|
||||
# log_file.write(message+"\n")
|
||||
# except Exception as e:
|
||||
# print("Can't write API log file")
|
||||
# print(e)
|
||||
#
|
||||
# return 0
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[2024/07/15 14:40:54] | Empty log file
|
||||
[2024/07/15 14:40:54] | Empty log file
|
||||
[2024/07/15 14:40:54] | Empty log file
|
||||
[2024/07/15 14:40:54] | Empty log file
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[2024/07/15 14:40:47] | Empty log file
|
||||
[2024/07/15 14:40:47] | Empty log file
|
||||
[2024/07/15 14:40:47] | Empty log file
|
||||
[2024/07/15 14:40:47] | Empty log file
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[2024/07/15 14:40:47] | Empty log file
|
||||
[2024/07/15 14:40:47] | Empty log file
|
||||
[2024/07/15 14:40:47] | Empty log file
|
||||
[2024/07/15 14:40:47] | Empty log file
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[2024/07/15 14:40:51] | Empty log file
|
||||
[2024/07/15 14:40:51] | Empty log file
|
||||
[2024/07/15 14:40:51] | Empty log file
|
||||
[2024/07/15 14:40:51] | Empty log file
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import time
|
||||
import sys
|
||||
|
||||
text_string = time.strftime(f"[%Y/%m/%d %H:%M:%S] | Empty log file")
|
||||
|
||||
files_to_reset = ["binance", "gateio", "kucoin","okex"]
|
||||
|
||||
if len(sys.argv)>1 and sys.argv[1] in files_to_reset:
|
||||
files_to_reset = [sys.argv[1]]
|
||||
|
||||
for file in files_to_reset:
|
||||
with open(f"{file}.log","w") as log_file:
|
||||
print(f"Resetting {file}")
|
||||
log_file.write(text_string+"\n")
|
||||
log_file.write(text_string+"\n")
|
||||
log_file.write(text_string+"\n")
|
||||
log_file.write(text_string+"\n")
|
||||
log_file.write(text_string+"\n")
|
||||
|
||||
print("Done")
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import sqlite3
|
||||
import time
|
||||
import json
|
||||
|
||||
# Connect to the SQLite database
|
||||
conn = sqlite3.connect('profits/profits_database.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Execute a SELECT query to retrieve data from the database
|
||||
cursor.execute('SELECT * FROM profits_table')
|
||||
|
||||
# Fetch all rows from the result set
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Process the fetched rows
|
||||
#for row in rows:
|
||||
#human_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(row[0])))
|
||||
#print(human_time,row[1],round(row[2],2),row[3]) # Or do whatever processing you need
|
||||
|
||||
data = json.loads(rows[-3][-1])
|
||||
print(data[-1])
|
||||
print(f"Number of entries: {len(rows)}")
|
||||
|
||||
# Close the connection
|
||||
conn.close()
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
'''
|
||||
Format to dump: (pair,timestamp,profit,exchange_name,order_id,order_history)
|
||||
'''
|
||||
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
|
||||
#Make a dictionary with the format "pair":"exchange" instead of this garbage
|
||||
if sys.argv[1]=="--testnet":
|
||||
exchanges = ["binance_testnet"]
|
||||
else:
|
||||
exchanges = ["binance","gateio","kucoin","okex"]
|
||||
recognized_pairs = {}
|
||||
profits_path = "../profits/"
|
||||
configs_path = "../configs/"
|
||||
results = []
|
||||
dates_already_present = []
|
||||
order_id_list = []
|
||||
amount_of_files_to_process = 0
|
||||
|
||||
#Load pair information from the exchange(s) configuration file(s)
|
||||
for exchange_file in exchanges:
|
||||
with open(f"{configs_path}{exchange_file}.json") as f:
|
||||
coins_in_exchange_config = json.load(f)["pairs"]
|
||||
for coin in coins_in_exchange_config:
|
||||
recognized_pairs[coin] = exchange_file
|
||||
|
||||
#Connect to db
|
||||
connection = sqlite3.connect('profits_database.db')
|
||||
cursor = connection.cursor()
|
||||
|
||||
#Extract order_id data from db
|
||||
cursor.execute('SELECT * FROM profits_table')
|
||||
rows = cursor.fetchall()
|
||||
for row in rows:
|
||||
order_id_list.append(row[4])
|
||||
|
||||
|
||||
for archivo in os.listdir(profits_path):
|
||||
if archivo.endswith(".profits"):
|
||||
amount_of_files_to_process+=1
|
||||
|
||||
#Format data
|
||||
count = 1
|
||||
for archivo in os.listdir(profits_path):
|
||||
if archivo.endswith(".profits"):
|
||||
coin = archivo.split(".")[0]
|
||||
coin = f"{coin[-50:-4]}/{coin[-4:]}"
|
||||
print(f"Processing {count} out of {amount_of_files_to_process} files ({coin})")
|
||||
with open(f"{profits_path}{archivo}", "r") as csvfile:
|
||||
for x in csvfile:
|
||||
date,amount,order_id = x.split(",")
|
||||
unix_time = int(datetime.strptime(date,"%Y-%m-%d").timestamp())
|
||||
while unix_time in dates_already_present:
|
||||
unix_time+=1
|
||||
dates_already_present.append(unix_time)
|
||||
exchange = recognized_pairs[coin] if coin in recognized_pairs else "unknown"
|
||||
stripped_order_id = order_id.strip()
|
||||
|
||||
complete_line = (unix_time,coin,float(amount),exchange,stripped_order_id,"")
|
||||
if stripped_order_id not in order_id_list:
|
||||
results.append(complete_line)
|
||||
count+=1
|
||||
|
||||
|
||||
#Dump data to db
|
||||
#for row in results:
|
||||
# cursor.execute('INSERT INTO profits_table VALUES(?, ?, ?, ?, ?, ?)', row)
|
||||
#connection.commit()
|
||||
#connection.close()
|
||||
|
||||
print(f"Done. Added {len(results)} rows")
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import sys
|
||||
import time
|
||||
import sqlite3
|
||||
|
||||
try:
|
||||
amount_of_deals = sys.argv[1]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Usage: python3 last_n_deals.py int")
|
||||
sys.exit()
|
||||
|
||||
connection = sqlite3.connect('profits_database.db')
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(f'SELECT * FROM profits_table ORDER BY timestamp DESC LIMIT {amount_of_deals}')
|
||||
deals = cursor.fetchall()
|
||||
connection.close()
|
||||
|
||||
|
||||
for line in deals[::-1]:
|
||||
human_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(line[0])))
|
||||
print(human_time,line[1],round(line[2],2),line[3])
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
'''
|
||||
Usage: python3 monthly_details.py YYYY-MM
|
||||
|
||||
Generates a csv file per exchange with details of profit per pair in the format "pair,profit"
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import csv
|
||||
import sys
|
||||
|
||||
try:
|
||||
month = sys.argv[1]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Usage: python3 monthly_details.py YYYY-MM")
|
||||
sys.exit()
|
||||
|
||||
result = {"binance":[],"gateio":[],"kucoin":[],"okex":[]}
|
||||
|
||||
#Connect to db
|
||||
connection = sqlite3.connect("profits_database.db")
|
||||
cursor = connection.cursor()
|
||||
|
||||
query = """SELECT exchange_name, pair, strftime('%Y-%m', datetime(timestamp, 'unixepoch')) AS month, SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
GROUP BY exchange_name, pair, month;"""
|
||||
print("Querying the database")
|
||||
cursor.execute(query)
|
||||
by_exchange = cursor.fetchall()
|
||||
|
||||
to_binance = []
|
||||
to_gateio = []
|
||||
to_kucoin = []
|
||||
to_okex = []
|
||||
|
||||
print("Generating reports")
|
||||
for item in by_exchange:
|
||||
if item[0] in result and item[2]==month:
|
||||
result[item[0]].append((item[1],item[3]))
|
||||
|
||||
#Descending order
|
||||
for item in result.copy():
|
||||
result[item].sort(key=lambda tup: tup[1], reverse=True)
|
||||
|
||||
print("Writing reports to disk")
|
||||
for item in result:
|
||||
with open(f"{item}_{month}_report.csv","w",newline="") as csv_file:
|
||||
csv_writer = csv.writer(csv_file)
|
||||
csv_writer.writerows(result[item])
|
||||
|
||||
print("done")
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import os, sys, json
|
||||
|
||||
total = 0
|
||||
|
||||
try:
|
||||
month = sys.argv[1]
|
||||
year = sys.argv[2]
|
||||
except:
|
||||
print ("Wrong syntax. 'python3 monthlypair.py MM YY'")
|
||||
sys.exit()
|
||||
|
||||
with open(year + month+".csv","w",newline="") as f:
|
||||
for archivo in os.listdir():
|
||||
if archivo.endswith(".profits"):
|
||||
with open(archivo, "r") as csvfile:
|
||||
for x in csvfile:
|
||||
date,amount,_ = x.split(",")
|
||||
if date.split("-")[1]==month and date.split("-")[0]==year:
|
||||
total += float(amount)
|
||||
#result.append([archivo[:-8],total])
|
||||
if total != 0:
|
||||
print(archivo[:-8]+","+str(total))
|
||||
f.write(archivo[:-8]+","+str(total)+"\n")
|
||||
total = 0
|
||||
|
||||
# Per-dollar section:
|
||||
|
||||
with open(year+month+"_per_dollar.csv","w",newline="") as f:
|
||||
for archivo in os.listdir():
|
||||
if archivo.endswith(".profits"):
|
||||
with open(archivo, "r") as csvfile:
|
||||
for x in csvfile:
|
||||
date,amount,_ = x.split(",")
|
||||
if date.split("-")[1]==month and date.split("-")[0]==year:
|
||||
total += float(amount)
|
||||
#result.append([archivo[:-8],total])
|
||||
if total != 0:
|
||||
#Here load the order size from the config file
|
||||
file_name = archivo.split(".")[0]
|
||||
with open("/home/nsanc/DCA2_live/configs/"+file_name+".json") as g:
|
||||
read_config = json.load(g)
|
||||
print(archivo[:-8]+","+str(round(total/read_config["order_size"],2)))
|
||||
f.write(archivo[:-8]+","+str(round(total/read_config["order_size"],2))+"\n")
|
||||
total = 0
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import os, sys, json
|
||||
|
||||
total = 0
|
||||
|
||||
try:
|
||||
month = sys.argv[1]
|
||||
year = sys.argv[2]
|
||||
except Exception as e:
|
||||
print ("Wrong syntax. 'python3 per_exchange.py MM YY'")
|
||||
print(e)
|
||||
sys.exit()
|
||||
|
||||
binance_filename = f"binance{year}{month}.csv"
|
||||
kucoin_filename = f"kucoin{year}{month}.csv"
|
||||
okex_filename = f"okex{year}{month}.csv"
|
||||
gateio_filename = f"gateio{year}{month}.csv"
|
||||
|
||||
with open(binance_filename,"w") as binance:
|
||||
pass
|
||||
with open(kucoin_filename,"w") as binance:
|
||||
pass
|
||||
with open(okex_filename,"w") as binance:
|
||||
pass
|
||||
with open(gateio_filename,"w") as binance:
|
||||
pass
|
||||
|
||||
#hitbtc_filename = f"hitbtc{year}{month}.csv"
|
||||
#poloniex_filename = f"poloniex{year}{month}.csv"
|
||||
|
||||
with open("../configs/binance.json") as r:
|
||||
binance_config = json.load(r)
|
||||
with open("../configs/kucoin.json") as r:
|
||||
kucoin_config = json.load(r)
|
||||
with open("../configs/okex.json") as r:
|
||||
okex_config = json.load(r)
|
||||
with open("../configs/gateio.json") as r:
|
||||
gateio_config = json.load(r)
|
||||
#with open("../configs/hitbtc.json") as r:
|
||||
# hitbtc_config = json.load(r)
|
||||
#with open("../configs/poloniex.json") as r:
|
||||
# poloniex_config = json.load(r)
|
||||
|
||||
|
||||
binance_sum = 0
|
||||
kucoin_sum = 0
|
||||
gateio_sum = 0
|
||||
okex_sum = 0
|
||||
|
||||
for archivo in os.listdir():
|
||||
if archivo.endswith(".profits"):
|
||||
with open(archivo, "r") as csvfile:
|
||||
for x in csvfile:
|
||||
date,amount,_ = x.split(",")
|
||||
if date[:4]==year and date[5:7]==month:
|
||||
total += float(amount)
|
||||
pair = archivo.split(".")[0]
|
||||
if total!=0:
|
||||
if pair in binance_config["pairs"]:
|
||||
binance_sum+=total
|
||||
with open(binance_filename,"a") as binance:
|
||||
binance.write(f"{pair},{round(total,2)}\n")
|
||||
elif pair in kucoin_config["pairs"]:
|
||||
with open(kucoin_filename,"a") as kucoin:
|
||||
kucoin.write(f"{pair},{round(total,2)}\n")
|
||||
kucoin_sum+=total
|
||||
elif pair in okex_config["pairs"]:
|
||||
with open(okex_filename,"a") as okex:
|
||||
okex.write(f"{pair},{round(total,2)}\n")
|
||||
okex_sum+=total
|
||||
elif pair in gateio_config["pairs"]:
|
||||
with open(gateio_filename,"a") as gateio:
|
||||
gateio.write(f"{pair},{round(total,2)}\n")
|
||||
gateio_sum+=total
|
||||
total = 0
|
||||
|
||||
#print("Totals:")
|
||||
total_sum = binance_sum+gateio_sum+kucoin_sum+okex_sum
|
||||
print("===========================")
|
||||
print(f"Binance: {round(binance_sum,2)} USDT ({round(binance_sum/total_sum*100,2)}%)")
|
||||
print(f"Gate.io: {round(gateio_sum,2)} USDT ({round(gateio_sum/total_sum*100,2)}%)")
|
||||
print(f"KuCoin: {round(kucoin_sum,2)} USDT ({round(kucoin_sum/total_sum*100,2)}%)")
|
||||
print(f"OKX: {round(okex_sum,2)} USDT ({round(okex_sum/total_sum*100,2)}%)")
|
||||
print("===========================")
|
||||
|
||||
try:
|
||||
if sys.argv[3]=="--withdrawals":
|
||||
print("With Gate.io:")
|
||||
total_to_withdraw = 1200
|
||||
print(f"Binance: {int(total_to_withdraw*binance_sum/total_sum)} USDT")
|
||||
print(f"Gate.io: {int(total_to_withdraw*gateio_sum/total_sum)} USDT")
|
||||
print(f"KuCoin: {int(total_to_withdraw*kucoin_sum/total_sum)} USDT")
|
||||
print(f"OKX: {int(total_to_withdraw*okex_sum/total_sum)} USDT")
|
||||
print("===========================")
|
||||
print("Without Gate.io:")
|
||||
partial_sum = binance_sum+kucoin_sum+okex_sum
|
||||
total_to_withdraw = 1200
|
||||
print(f"Binance: {int(total_to_withdraw*binance_sum/partial_sum)} USDT")
|
||||
print(f"KuCoin: {int(total_to_withdraw*kucoin_sum/partial_sum)} USDT")
|
||||
print(f"OKX: {int(total_to_withdraw*okex_sum/partial_sum)} USDT")
|
||||
print("===========================")
|
||||
except Exception as e:
|
||||
pass #prograem
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
'''
|
||||
Display last three months of profits grouped by exchange
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import datetime
|
||||
|
||||
linewidth = 40
|
||||
exchanges = ["binance","gateio","kucoin","okex"]
|
||||
|
||||
#Connect to db
|
||||
connection = sqlite3.connect("profits_database.db")
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.execute("""SELECT
|
||||
exchange_name,
|
||||
CASE
|
||||
WHEN strftime('%Y-%m', timestamp, 'unixepoch', '-3 hours') = strftime('%Y-%m', 'now', 'localtime') THEN strftime('%Y-%m', 'now', 'localtime')
|
||||
WHEN strftime('%Y-%m', timestamp, 'unixepoch', '-3 hours') = strftime('%Y-%m', 'now', 'localtime', '-1 month') THEN strftime('%Y-%m', 'now', 'localtime', '-1 month')
|
||||
WHEN strftime('%Y-%m', timestamp, 'unixepoch', '-3 hours') = strftime('%Y-%m', 'now', 'localtime', '-2 month') THEN strftime('%Y-%m', 'now', 'localtime', '-2 month')
|
||||
ELSE 'Other Months'
|
||||
END AS month_group,
|
||||
SUM(amount) AS total_amount
|
||||
FROM
|
||||
profits_table
|
||||
WHERE
|
||||
strftime('%s', 'now') - timestamp <= 365 * 24 * 60 * 60 -- 365 days in seconds
|
||||
GROUP BY
|
||||
exchange_name, month_group
|
||||
ORDER BY
|
||||
exchange_name, month_group;""")
|
||||
by_exchange = cursor.fetchall()
|
||||
|
||||
|
||||
# Get the current date
|
||||
current_date = datetime.date.today()
|
||||
|
||||
# Get the current month in YYYY-MM format
|
||||
current_month = current_date.strftime("%Y-%m")
|
||||
|
||||
# Get the last three months
|
||||
last_three_months = [current_month]
|
||||
for i in range(3):
|
||||
# Calculate the date for the previous month
|
||||
previous_month = current_date.replace(day=1) - datetime.timedelta(days=current_date.day)
|
||||
previous_month = previous_month.replace(month=previous_month.month - i)
|
||||
|
||||
# Format the previous month to YYYY-MM format
|
||||
previous_month_str = previous_month.strftime("%Y-%m")
|
||||
|
||||
# Append the previous month to the list
|
||||
last_three_months.append(previous_month_str)
|
||||
|
||||
|
||||
#Now we got the month list and the db data, let's present it in a readable fashion
|
||||
print("Revenue per month:")
|
||||
print("="*linewidth)
|
||||
for yearmonth in last_three_months[:3][::-1]:
|
||||
print(f"{yearmonth}:")
|
||||
total = 0
|
||||
for exchange in exchanges:
|
||||
if exchange=="binance":
|
||||
for item in by_exchange:
|
||||
if item[0]=="binance" and item[1]==yearmonth:
|
||||
binance_total = item[2]
|
||||
total+=item[2]
|
||||
elif exchange=="gateio":
|
||||
for item in by_exchange:
|
||||
if item[0]=="gateio" and item[1]==yearmonth:
|
||||
gateio_total = item[2]
|
||||
total+=item[2]
|
||||
elif exchange=="kucoin":
|
||||
for item in by_exchange:
|
||||
if item[0]=="kucoin" and item[1]==yearmonth:
|
||||
kucoin_total = item[2]
|
||||
total+=item[2]
|
||||
elif exchange=="okex":
|
||||
for item in by_exchange:
|
||||
if item[0]=="okex" and item[1]==yearmonth:
|
||||
okex_total = item[2]
|
||||
total+=item[2]
|
||||
print(f"Binance: {round(binance_total,2)} ({round(binance_total/total*100,2)}%)")
|
||||
print(f"Gate.io: {round(gateio_total,2)} ({round(gateio_total/total*100,2)}%)")
|
||||
print(f"KuCoin: {round(kucoin_total,2)} ({round(kucoin_total/total*100,2)}%)")
|
||||
print(f"OKX: {round(okex_total,2)} ({round(okex_total/total*100,2)}%)")
|
||||
print("-"*linewidth)
|
||||
print(f"Total: {round(total,2)}")
|
||||
print("="*linewidth)
|
||||
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
'''
|
||||
Profits report from db
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import calendar
|
||||
import datetime
|
||||
|
||||
|
||||
exchanges = ["binance","gateio","kucoin","okex"]
|
||||
line_width = 40
|
||||
|
||||
#Connect to db
|
||||
connection = sqlite3.connect("profits_database.db")
|
||||
cursor = connection.cursor()
|
||||
|
||||
#Last 60 days query
|
||||
cursor.execute("""SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3,
|
||||
SUM(amount) AS total_amount
|
||||
FROM profits_table
|
||||
WHERE strftime('%s', 'now') - timestamp <= 60 * 24 * 60 * 60 -- 60 days in seconds
|
||||
GROUP BY day_utc3
|
||||
ORDER BY day_utc3;""")
|
||||
last_60_days_rows = cursor.fetchall()
|
||||
|
||||
#Last 30 days query
|
||||
#cursor.execute("""SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3,
|
||||
cursor.execute("""SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3,
|
||||
SUM(amount) AS total_amount
|
||||
FROM profits_table
|
||||
WHERE strftime('%s', 'now') - timestamp <= 30 * 24 * 60 * 60 -- 30 days in seconds;""")
|
||||
last_30_days = cursor.fetchall()
|
||||
|
||||
#Last 7 days query
|
||||
cursor.execute("""SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3,
|
||||
SUM(amount) AS total_amount
|
||||
FROM profits_table
|
||||
WHERE strftime('%s', 'now') - timestamp <= 7 * 24 * 60 * 60 -- 7 days in seconds;""")
|
||||
last_7_days = cursor.fetchall()
|
||||
|
||||
#Last n months query
|
||||
cursor.execute("""SELECT strftime('%Y-%m', timestamp, 'unixepoch', '-3 hours') AS year_month_utc3,
|
||||
SUM(amount) AS total_amount
|
||||
FROM profits_table
|
||||
WHERE strftime('%s', 'now') - timestamp <= 18 * 30 * 24 * 60 * 60 -- 18 months in seconds
|
||||
GROUP BY year_month_utc3
|
||||
ORDER BY year_month_utc3;""")
|
||||
last_n_months_rows = cursor.fetchall()
|
||||
|
||||
#Yearly totals
|
||||
cursor.execute("""SELECT strftime('%Y', timestamp, 'unixepoch', '-3 hours') AS year_utc3,
|
||||
SUM(amount) AS total_amount
|
||||
FROM profits_table
|
||||
WHERE strftime('%s', 'now') - timestamp <= 24 * 365 * 60 * 60 -- 365 days in seconds
|
||||
GROUP BY year_utc3
|
||||
ORDER BY year_utc3;""")
|
||||
yearly_totals = cursor.fetchall()
|
||||
|
||||
print("="*line_width)
|
||||
print("Last 60 days:")
|
||||
print("-"*line_width)
|
||||
for row in last_60_days_rows:
|
||||
print(f"{row[0]}: {round(row[1],2)}")
|
||||
print("="*line_width)
|
||||
print("Last 18 months:")
|
||||
print("-"*line_width)
|
||||
for row in last_n_months_rows[1:]:
|
||||
print(f"{row[0]}: {round(row[1],2)}")
|
||||
print("-"*line_width)
|
||||
print(f"Last 30 days average: {round(last_30_days[0][1]/30,2)}")
|
||||
print(f"Last 7 days average: {round(last_7_days[0][1]/7,2)}")
|
||||
cursor.execute("""SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3,
|
||||
SUM(amount) AS total_amount
|
||||
FROM profits_table
|
||||
WHERE timestamp > strftime('%s', 'now', 'start of month', 'utc')
|
||||
GROUP BY day_utc3;""")
|
||||
last_month = cursor.fetchall()
|
||||
|
||||
#The projection calculation is: the amount of profit so far in the month + the averages of the last 30 days and the last 7 days times the number of days left in the month.
|
||||
days_in_month = calendar.monthrange(datetime.date.today().year, datetime.date.today().month)[1]
|
||||
daily_combined_media = (last_30_days[0][1]/30+last_7_days[0][1]/7)/2
|
||||
current_amount = last_n_months_rows[-1][1]
|
||||
days_past_this_month = int(last_60_days_rows[-1][0][8:10])
|
||||
projection_calculation = current_amount + daily_combined_media*(days_in_month-days_past_this_month)
|
||||
|
||||
print(f"This month projection: {round(projection_calculation,2)}")
|
||||
print("="*line_width)
|
||||
print("Per exchange:")
|
||||
print("-"*line_width)
|
||||
cursor.execute("""SELECT
|
||||
exchange_name,
|
||||
CASE
|
||||
WHEN strftime('%Y-%m', timestamp, 'unixepoch', '-3 hours') = strftime('%Y-%m', 'now', 'localtime') THEN 'This Month'
|
||||
WHEN strftime('%Y-%m', timestamp, 'unixepoch', '-3 hours') = strftime('%Y-%m', 'now', 'localtime', '-1 month') THEN 'Last Month'
|
||||
ELSE 'Other Months'
|
||||
END AS month_group,
|
||||
SUM(amount) AS total_amount
|
||||
FROM
|
||||
profits_table
|
||||
WHERE
|
||||
strftime('%s', 'now') - timestamp <= 60 * 24 * 60 * 60 -- 60 days in seconds
|
||||
GROUP BY
|
||||
exchange_name, month_group
|
||||
ORDER BY
|
||||
exchange_name, month_group;""")
|
||||
|
||||
#So type checking goes away
|
||||
binance_amount = 0
|
||||
gateio_amount = 0
|
||||
kucoin_amount = 0
|
||||
okex_amount = 0
|
||||
|
||||
by_exchange = cursor.fetchall()
|
||||
for row in by_exchange:
|
||||
if row[0]=="binance":
|
||||
if row[1]=="This Month":
|
||||
binance_amount = row[2]
|
||||
elif row[0]=="gateio":
|
||||
if row[1]=="This Month":
|
||||
gateio_amount = row[2]
|
||||
elif row[0]=="kucoin":
|
||||
if row[1]=="This Month":
|
||||
kucoin_amount = row[2]
|
||||
elif row[0]=="okex":
|
||||
if row[1]=="This Month":
|
||||
okex_amount = row[2]
|
||||
|
||||
total_amount = binance_amount+gateio_amount+kucoin_amount+okex_amount
|
||||
|
||||
print(f"Binance: {round(binance_amount,2)} USDT ({round(binance_amount/total_amount*100,2)}%)")
|
||||
print(f"Gate.io: {round(gateio_amount,2)} USDT ({round(gateio_amount/total_amount*100,2)}%)")
|
||||
print(f"KuCoin: {round(kucoin_amount,2)} USDT ({round(kucoin_amount/total_amount*100,2)}%)")
|
||||
print(f"OKX: {round(okex_amount,2)} USDT ({round(okex_amount/total_amount*100,2)}%)")
|
||||
print("="*line_width)
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import os, sys, json, datetime
|
||||
|
||||
pair = sys.argv[1]
|
||||
|
||||
def load_old_long(pair):
|
||||
#Load old_long info
|
||||
try:
|
||||
with open(f"../status/{pair}.oldlong") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def load_old_long_from_status(pair):
|
||||
#Load old_long info
|
||||
try:
|
||||
with open(f"../status/{pair}.status") as f:
|
||||
return json.load(f)["old_long"]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
old_long = load_old_long(pair)
|
||||
if old_long is None:
|
||||
old_long = load_old_long_from_status(pair)
|
||||
if old_long is None:
|
||||
sys.exit(0)
|
||||
|
||||
#Get time of switch to unix time
|
||||
old_date = old_long["datetime"][1:11]
|
||||
time_of_switch = datetime.datetime.strptime(old_date,"%Y/%m/%d").timestamp()
|
||||
|
||||
#Calculate profits
|
||||
total = 0
|
||||
with open(f"{pair}.profits") as csvfile:
|
||||
for x in csvfile:
|
||||
[date,amount,_] = x.split(",")
|
||||
time_of_profit = datetime.datetime.strptime(date,"%Y-%m-%d").timestamp()
|
||||
if time_of_profit>=time_of_switch:
|
||||
total += float(amount)
|
||||
|
||||
print(f"Profits since {old_date}: {round(total,2)}")
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
'''
|
||||
Usage: python3 profits_since_date.py BASE/QUOTE YYYY-MM-DD
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
try:
|
||||
pair = sys.argv[1].replace("/","")
|
||||
since_date = sys.argv[2]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Usage: python3 profits_since_date.py BASE/QUOTE YYYY-MM-DD")
|
||||
sys.exit()
|
||||
|
||||
#Connect to db
|
||||
connection = sqlite3.connect("../profits/profits_database.db")
|
||||
cursor = connection.cursor()
|
||||
|
||||
linux_time = datetime.datetime.strptime(since_date,"%Y-%m-%d").timestamp()
|
||||
|
||||
#Query database
|
||||
query = f"""SELECT pair, SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
WHERE timestamp >= '{linux_time}'
|
||||
GROUP BY pair;"""
|
||||
cursor.execute(query)
|
||||
query_result = cursor.fetchall()
|
||||
|
||||
#Calculate profits
|
||||
total = 0
|
||||
for item in query_result:
|
||||
if item[0].replace("/","")==pair:
|
||||
total = item[1]
|
||||
|
||||
print(f"Profits since {since_date}: {round(total,2)}")
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
'''
|
||||
Returns the profits of a certain day, grouped by pair.
|
||||
|
||||
Usage: python3 profits_that_day.py YYYY-MM-DD
|
||||
'''
|
||||
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
try:
|
||||
date = sys.argv[1]
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("Usage: python3 profits_that_day.py YYYY-MM-DD")
|
||||
sys.exit()
|
||||
|
||||
#Connect to db
|
||||
connection = sqlite3.connect("profits_database.db")
|
||||
cursor = connection.cursor()
|
||||
|
||||
query = f"SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3, SUM(amount) AS total_amount FROM profits_table GROUP BY day_utc3 ORDER BY day_utc3;"
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
|
||||
for item in result:
|
||||
if item[0]==date:
|
||||
print(f"Profits on {date}: {round(item[1],2)}")
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import ccxt
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
with open(f"../configs/{sys.argv[1]}.json") as k:
|
||||
config = json.load(k)
|
||||
|
||||
#Load exchange keys
|
||||
pwd = config["password"] if "password" in config else ""
|
||||
exch = sys.argv[1]
|
||||
if exch=="okex":
|
||||
exch="okex5"
|
||||
exchange_class = getattr(ccxt, sys.argv[1])
|
||||
exchange = exchange_class({
|
||||
"apiKey": config["key"],
|
||||
"secret": config["secret"],
|
||||
"password": pwd,
|
||||
"timeout": 30000,
|
||||
"enableRateLimit": True,
|
||||
})
|
||||
exchange.load_markets()
|
||||
|
||||
open_orders = []
|
||||
pairs = {}
|
||||
def add_pair_to_dict(pair):
|
||||
if pair not in pairs:
|
||||
pairs[pair] = 1
|
||||
else:
|
||||
pairs[pair] += 1
|
||||
|
||||
if sys.argv[1]=="binance":
|
||||
for p in config["pairs"]:
|
||||
print(f"Fetching {p}")
|
||||
orders = exchange.fetch_open_orders(symbol=p)
|
||||
time.sleep(.5)
|
||||
open_orders.extend(orders)
|
||||
else:
|
||||
open_orders = exchange.fetch_open_orders()
|
||||
for order in open_orders:
|
||||
if order["side"]=="buy":
|
||||
add_pair_to_dict(order["symbol"])
|
||||
|
||||
found = False
|
||||
for x in pairs:
|
||||
if pairs[x]>1:
|
||||
print(x,pairs[x])
|
||||
orders = exchange.fetch_open_orders(symbol=x)
|
||||
for order in orders:
|
||||
if order["side"]=="buy":
|
||||
print(order)
|
||||
found = True
|
||||
if not found:
|
||||
print("No duplicates found")
|
||||
|
|
@ -0,0 +1,591 @@
|
|||
import requests
|
||||
import sys
|
||||
import json
|
||||
import credentials
|
||||
|
||||
try:
|
||||
if sys.argv[1]=="--testnet":
|
||||
is_testnet = True
|
||||
string_to_add = "TESTNET "
|
||||
api_key = credentials.get_credentials("testnet_api_key")["key"]
|
||||
base_url = credentials.get_url("testnet") #type: ignore
|
||||
exchanges = {"Binance":"/binance"}
|
||||
elif sys.argv[1]=="--mainnet":
|
||||
is_testnet = False
|
||||
string_to_add = "MAINNET "
|
||||
api_key = credentials.get_credentials("mainnet_api_key")["key"]
|
||||
base_url = credentials.get_url("mainnet") #type: ignore
|
||||
exchanges = {"binance":"/binance", "gate.io":"/gateio", "kucoin":"/kucoin", "okx":"/okex"}
|
||||
else:
|
||||
print(f"Unrecognized parameter {sys.argv[1]}")
|
||||
sys.exit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
sys.exit()
|
||||
|
||||
headers = {'X-API-KEY': api_key}
|
||||
|
||||
command_list = f'''{string_to_add}COMMANDS:
|
||||
|
||||
INSTANCE
|
||||
1) global_status 2) missing_pairs 3) server_time
|
||||
4) trader_time 5) toggle_restart 6) toggle_telegram
|
||||
7) mod_global_tp_level 8) global_last_call 9) edit_loop_wait_time
|
||||
10) edit_call_wait_time 11) reload_markets 12) fetch_full_log
|
||||
13) paused_traders 14) fetch_log
|
||||
|
||||
TRADERS
|
||||
51) worker_status 52) get_all_worker_status
|
||||
53) add_pair 54) remove_pair 55) restart_pair
|
||||
56) import_pair 57) switch_to_short 58) switch_to_long
|
||||
59) load_old_long 60) add_so 61) add_quote
|
||||
62) mod_tp_level 63) last_call 64) deferred_last_call
|
||||
65) toggle_pause 66) toggle_cleanup 67) toggle_autoswitch
|
||||
68) toggle_check_old_long_price 69) switch_quote_currency
|
||||
70) reload_safety_order 71) view_old_long
|
||||
|
||||
98) Change broker 99) Exit
|
||||
'''
|
||||
|
||||
def validate_pair(trading_pair):
|
||||
return "/" in trading_pair and len(trading_pair)>3
|
||||
|
||||
def validate_float_or_int(number):
|
||||
'''
|
||||
Validates if the number can be interpreted as a float or an int
|
||||
'''
|
||||
try:
|
||||
number = str(float(number))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def validate_int(number):
|
||||
'''
|
||||
Validates if the number can be interpreted as an integer
|
||||
'''
|
||||
try:
|
||||
new_number = int(number)
|
||||
if str(new_number) == str(number):
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def select_exchange(exchanges):
|
||||
'''
|
||||
Selects the exchange to use
|
||||
'''
|
||||
|
||||
selection = input("Enter exchange: (Binance, Gate.io, KuCoin, OKX) ").lower()
|
||||
for item in exchanges:
|
||||
if selection in item.lower():
|
||||
return item
|
||||
print("Invalid input")
|
||||
sys.exit()
|
||||
|
||||
if __name__=="__main__":
|
||||
|
||||
if len(exchanges)==1:
|
||||
selection = list(exchanges.keys())[0]
|
||||
else:
|
||||
selection = select_exchange(exchanges)
|
||||
#selection = input("Enter exchange: (Binance, Gate.io, KuCoin, OKX) ").lower()
|
||||
#for item in exchanges:
|
||||
# if selection in item.lower():
|
||||
# selection = item
|
||||
# break
|
||||
#print("Invalid input")
|
||||
#sys.exit()
|
||||
port = exchanges[selection]
|
||||
|
||||
|
||||
print("DCAv2 COMMANDER")
|
||||
if not is_testnet:
|
||||
print("WARNING: RUNNING ON MAINNET")
|
||||
|
||||
while True:
|
||||
print("="*80)
|
||||
print(f"Exchange: {selection}")
|
||||
print(command_list)
|
||||
|
||||
|
||||
#When entering the command, it shows a brief description and requests for parameters.
|
||||
command = input("Your input: ")
|
||||
|
||||
try:
|
||||
command = int(command)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
if command==99:
|
||||
print("Goodbye")
|
||||
sys.exit()
|
||||
|
||||
elif command==98:
|
||||
#while True:
|
||||
# selection = input("Enter exchange: (Binance, Gate.io, KuCoin, OKX) ").lower()
|
||||
# if selection not in exchanges:
|
||||
# print("Invalid input")
|
||||
# port = exchanges[selection]
|
||||
# break
|
||||
selection = select_exchange(exchanges)
|
||||
port = exchanges[selection]
|
||||
print(f"New exchange selected: {selection}")
|
||||
|
||||
|
||||
######################
|
||||
###### INSTANCE ######
|
||||
######################
|
||||
|
||||
elif command==1:
|
||||
print("global_status returns a dictionary of the global status of the instance")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/global_status"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==2:
|
||||
print("missing_pairs returns a list of pairs that are in the config file of the instance")
|
||||
print("but are not running.")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/missing_pairs"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==3:
|
||||
print("server_time returns the linux time of the server")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/server_time"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==4:
|
||||
print("trader_time return the last time of the traders was active")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/trader_time"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==5:
|
||||
print("toggle_restart controls if the instance will attempt to restart failed traders or not")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/toggle_restart"
|
||||
print(json.loads(requests.post(url, headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==6:
|
||||
print("toggle_telegram turns on or off the Telegram notifications")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/toggle_telegram"
|
||||
print(json.loads(requests.post(url, headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==7:
|
||||
print("mod_global_tp_level modifies the percentage of profit of all the traders")
|
||||
print("Example: 1.02 is equal to 2% profit")
|
||||
new_profit_level = input("Desired profit level: ")
|
||||
if not validate_float_or_int(new_profit_level):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/mod_global_tp_level"
|
||||
parameters = {"amount": new_profit_level}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==8:
|
||||
print("global_last_call signals all traders to cease operation when profit is reached")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/global_last_call"
|
||||
print(json.loads(requests.post(url, headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==9:
|
||||
print("edit_loop_wait_time modifies the pause the instance takes after processing the open orders")
|
||||
print("instance fetch the orders -> instance sends the orders to the traders ->")
|
||||
print("instance waits for the traders to complete their tasks -> instance waits <loop_wait_time> seconds")
|
||||
print("The input value can be an integer or a float")
|
||||
new_wait_time = input("Desired wait time: ")
|
||||
if not validate_float_or_int(new_wait_time):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/edit_loop_wait_time"
|
||||
parameters = {"wait_time": new_wait_time}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==10:
|
||||
print("edit_call_wait_time modifies the pause that the traders take between some API calls")
|
||||
print("This aims to reduce the load on the API endpoints of the broker.")
|
||||
print("The input value can be an integer or a float")
|
||||
new_wait_time = input("Desired call wait time: ")
|
||||
if not validate_float_or_int(new_wait_time):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/edit_call_wait_time"
|
||||
parameters = {"wait_time": new_wait_time}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==11:
|
||||
print("reload_markets forces CCXT to renew all the market information")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/reload_markets"
|
||||
print(json.loads(requests.post(url, headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==12:
|
||||
print("fetch_full_log displays the log of an instance.")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}/statistics_server/fetch_full_log?exchange_name={port[1:]}&width={100}"
|
||||
for item in json.loads(requests.get(url, headers=headers).content)["line"]:
|
||||
print(item)
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==13:
|
||||
print("paused_traders returns a list of paused traders.")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/paused_traders"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==14:
|
||||
print("fetch_log displays the last n log entries of an instance.")
|
||||
amount = input("Amount of lines? ")
|
||||
if not validate_float_or_int(amount):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}/statistics_server/fetch_log?exchange_name={port[1:]}&width={100}&amount={amount}"
|
||||
for item in json.loads(requests.get(url, headers=headers).content)["line"]:
|
||||
print(item)
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
|
||||
######################
|
||||
####### TRADER #######
|
||||
######################
|
||||
|
||||
elif command==51:
|
||||
print("worker_status return the status dictionary of the trader")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
base,quote = trading_pair.split("/")
|
||||
url = f"{base_url}{port}/worker_status?base={base}"e={quote}"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==52:
|
||||
print("get_all_worker_status returns a dictionary of all the status dictionaries of all active trader")
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/get_all_worker_status"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==53:
|
||||
print("add_pair add a trader to the instance.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/add_pair"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==54:
|
||||
print("remove_pair terminates a running trader from the instance.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/remove_pair"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==55:
|
||||
print("restart_pair terminates and restarts a trader from the instance.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/restart_pair"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==56:
|
||||
print("import_pair imports a trader to the instance.")
|
||||
print("In order for the importing to be successful, a status file must exist in the status directory ")
|
||||
print("and the take profit order must be open.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/import_pair"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==57:
|
||||
print("switch_to_short changes the mode of operation of a trader from long mode (the default one) to short mode.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/switch_to_short"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==58:
|
||||
print("switch_to_long changes the mode of operation of a trader from short mode to the default long mode")
|
||||
print("It takes an extra parameter flag: 0 to ignore the profit calculation from the switch and 1 to do that calculation")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
calculation = input("Profit calculation? 0: ignore, 1: calculate ")
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if int(calculation) not in [0,1]:
|
||||
print("The input for the calculation flag is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/switch_to_long"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote,
|
||||
"calculate_profits": calculation}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==59:
|
||||
print("load_old_long load to the status dictionary the contents of an old_long file")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/load_old_long"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==60:
|
||||
print("add_so extends the safety order limit of a trader")
|
||||
print("You can also use negative numbers to substract to that limit")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
amount = input("Amount of safety orders to add/remove: ")
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if not validate_int(amount):
|
||||
print("The amount entered is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/add_so"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote,
|
||||
"amount": amount}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==61:
|
||||
print("add_quote adds a lump sum of quote currency to the deal.")
|
||||
print("This is not possible to do on a short trader")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
amount = input("Amount of quote to add: ")
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if not validate_float_or_int(amount):
|
||||
print("The amount entered is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/add_quote"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote,
|
||||
"amount": amount}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==62:
|
||||
print("mod_tp_level modifies the profit percentage of a trader")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
new_profit_level = input("Desired profit level: ")
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if not validate_float_or_int(new_profit_level):
|
||||
print("The amount entered is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/mod_tp_level"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote,
|
||||
"amount": new_profit_level}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==63:
|
||||
print("last_call signals a trader to cease operation when profit is reached")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/last_call"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==64:
|
||||
print("deferred_last_call signals a trader to cease operation when profit is reached after certain date")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
yyyymmdd = input("Input date (YYYYMMDD) ")
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if len(yyyymmdd)!=8:
|
||||
print("Date format is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/deferred_last_call"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote,
|
||||
"yyyymmdd": yyyymmdd}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==65:
|
||||
print("toggle_pause pauses or unpauses a trader")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/toggle_pause"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==66:
|
||||
print("toggle_cleanup enables or disables the cleanup routine of a trader")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/toggle_cleanup"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==67:
|
||||
print("toggle_autoswitch enables or disables the automatic switch to long of a short trader once certain conditions are met.")
|
||||
print("This is only valid in a short trader, of course.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/toggle_autoswitch"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==68:
|
||||
print("toggle_check_old_long_price enables or disables the verification of the current price exceeding the old long price.")
|
||||
print("This is only valid in a short trader, of course.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/toggle_check_old_long_price"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==69:
|
||||
print("switch_quote_currency changes the quote currency of a running trader.")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
new_quote = input("Input new quote currency: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if len(new_quote)==0:
|
||||
print("The quote currency is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/switch_quote_currency"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote,
|
||||
"new_quote": new_quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==70:
|
||||
print("reload_safety_order reloads the safety order to the reader using the order id present in the status dictionary")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
url = f"{base_url}{port}/reload_safety_order"
|
||||
base,quote = trading_pair.split("/")
|
||||
parameters = {"base": base,
|
||||
"quote": quote}
|
||||
print(json.loads(requests.post(url, headers=headers, json=parameters).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
||||
elif command==71:
|
||||
print("Views the old_long information")
|
||||
trading_pair = input("Input trader in the format BASE/QUOTE: ").upper()
|
||||
if not validate_pair(trading_pair):
|
||||
print("The input is invalid")
|
||||
break
|
||||
from_file = 0 if input("From file? (y/N) ") in ["N","n",""] else 1
|
||||
if input("Proceed? (Y/n) ") in ["Y","y",""]:
|
||||
base,quote = trading_pair.split("/")
|
||||
url = f"{base_url}{port}/view_old_long?base={base}"e={quote}&from_file={from_file}"
|
||||
print(json.loads(requests.get(url,headers=headers).content))
|
||||
input("Press ENTER to continue ")
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import uuid
|
||||
import sqlite3
|
||||
|
||||
|
||||
users = ["user1", "user2", "user3", "user4"]
|
||||
keys = []
|
||||
|
||||
for user in users:
|
||||
keys.append([user,str(uuid.uuid4())])
|
||||
|
||||
|
||||
database_connection = sqlite3.connect("../api_credentials.db")
|
||||
database_cursor = database_connection.cursor()
|
||||
database_cursor.execute("CREATE TABLE IF NOT EXISTS credentials_table (user TEXT, key TEXT)")
|
||||
|
||||
for pair in keys:
|
||||
database_cursor.execute('INSERT INTO credentials_table VALUES(?, ?)', pair)
|
||||
database_connection.commit()
|
||||
database_connection.close()
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import sys
|
||||
import json
|
||||
|
||||
exchanges = ["binance","okex","kucoin","gateio"]
|
||||
pais = []
|
||||
for exchange in exchanges:
|
||||
with open(f"../configs/{exchange}.json") as f:
|
||||
pairs = json.load(f)["pairs"]
|
||||
for pair in pairs:
|
||||
if pair in [sys.argv[1],sys.argv[1].replace("/","")]:
|
||||
print(f"{pair} is already running on {exchange}")
|
||||
sys.exit(1)
|
||||
print("Pair is not running")
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import sqlite3
|
||||
|
||||
valid_keys = []
|
||||
|
||||
database_connection = sqlite3.connect("../api_credentials.db")
|
||||
database_cursor = database_connection.cursor()
|
||||
|
||||
database_cursor.execute("SELECT * FROM credentials_table")
|
||||
data = database_cursor.fetchall()
|
||||
|
||||
|
||||
for line in data:
|
||||
valid_keys.append(line[1])
|
||||
|
||||
print(valid_keys)
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
'''
|
||||
If the broker cancelled all your orders just because (THANK YOU KUCOIN), this script will resend all the orders and change the status dictionary values accordingly.
|
||||
Note: since version 2024.07.08, both the safety and the take profit order are included in the status file; this will make resending the orders easier.
|
||||
'''
|
||||
|
||||
|
||||
import ccxt
|
||||
import json
|
||||
import sys
|
||||
|
||||
def gib_so_size(starting_order_size: float, so_number: int, scaling_factor: float) -> float:
|
||||
'''
|
||||
Returns the correct safety order size depending on the number
|
||||
Scaling factor example: 5% = 0.0105
|
||||
'''
|
||||
order_size = starting_order_size
|
||||
for _ in range(so_number):
|
||||
order_size = order_size*scaling_factor*100
|
||||
return order_size
|
||||
|
||||
with open(f"../configs/kucoin.json") as k:
|
||||
config_file = json.load(k)
|
||||
|
||||
exchange_class = getattr(ccxt, "kucoin")
|
||||
exchange = exchange_class({
|
||||
"apiKey": config_file["key"],
|
||||
"secret": config_file["secret"],
|
||||
"password": config_file["password"],
|
||||
"timeout": 30000,
|
||||
"enableRateLimit": True
|
||||
})
|
||||
|
||||
pair = sys.argv[1]
|
||||
|
||||
with open(f"../status/{pair}.status") as f:
|
||||
status_contents = json.loads(f.read())
|
||||
|
||||
with open(f"../configs/{pair}.json") as c:
|
||||
config = json.loads(c.read())
|
||||
|
||||
|
||||
if not config["is_short"]:
|
||||
buy_order_amount = gib_so_size(status_contents["order_size"],status_contents["so_amount"]+1,config["safety_order_scale"]) #Next safety order
|
||||
buy_order_price = status_contents["next_so_price"] #Next safety order price
|
||||
|
||||
sell_order_amount = status_contents["base_bought"] #Take profit order
|
||||
sell_order_price = status_contents["take_profit_price"] #Take profit order price
|
||||
else:
|
||||
sell_order_amount = gib_so_size(status_contents["order_size"],status_contents["so_amount"]+1,config["safety_order_scale"]) #Next safety order
|
||||
sell_order_price = status_contents["next_so_price"] #Next safety order price
|
||||
|
||||
buy_order_amount = status_contents["base_bought"] #Take profit order
|
||||
buy_order_price = status_contents["take_profit_price"] #Take profit order price
|
||||
|
||||
print(f"Pair: {pair}")
|
||||
pair_mode = "Short" if config["is_short"] else "Long"
|
||||
print(f"Mode: {pair_mode}")
|
||||
print(f"Buy order amount: {buy_order_amount}")
|
||||
print(f"Buy order price: {buy_order_price}")
|
||||
print(f"Sell order amount: {sell_order_amount}")
|
||||
print(f"Sell order price: {sell_order_price}")
|
||||
|
||||
input("Proceed? ")
|
||||
|
||||
print("Sending buy order")
|
||||
buy_order = exchange.create_order(config["pair"],"limit","buy",buy_order_amount,buy_order_price)
|
||||
print(f"Buy order id: {buy_order['id']}")
|
||||
|
||||
print("Sending sell order")
|
||||
sell_order = exchange.create_order(config["pair"],"limit","sell",sell_order_amount,sell_order_price)
|
||||
print(f"Sell order id: {sell_order['id']}")
|
||||
|
||||
if not config["is_short"]:
|
||||
status_contents["tp_order_id"] = sell_order["id"]
|
||||
status_contents["so_order_id"] = buy_order["id"]
|
||||
else:
|
||||
status_contents["tp_order_id"] = buy_order["id"]
|
||||
status_contents["so_order_id"] = sell_order["id"]
|
||||
|
||||
json_object = json.dumps(status_contents, indent=4)
|
||||
with open(f"../status/{pair}.status","w") as f:
|
||||
f.write(json_object)
|
||||
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import datetime
|
||||
|
||||
pair = sys.argv[1].replace("/","")
|
||||
|
||||
#Connect to db
|
||||
connection = sqlite3.connect("../profits/profits_database.db")
|
||||
cursor = connection.cursor()
|
||||
|
||||
#Load old_long info
|
||||
try:
|
||||
with open(f"../status/{pair}.oldlong") as f:
|
||||
old_long = json.load(f)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("No old_long file")
|
||||
os._exit(0)
|
||||
|
||||
#Get time of switch to unix time
|
||||
old_date = old_long["datetime"][1:11]
|
||||
linux_time = datetime.datetime.strptime(old_date,"%Y/%m/%d").timestamp()
|
||||
|
||||
#Query database
|
||||
query = f"""SELECT pair, SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
WHERE timestamp >= '{linux_time}'
|
||||
GROUP BY pair;"""
|
||||
cursor.execute(query)
|
||||
query_result = cursor.fetchall()
|
||||
|
||||
#Calculate profits
|
||||
total = 0
|
||||
for item in query_result:
|
||||
if item[0].replace("/","")==pair:
|
||||
total = item[1]
|
||||
|
||||
print(f"Profits since switch ({old_date}): {round(total,2)}")
|
||||
print(f"Profit needed to cover expenses: {round(old_long['quote_spent'],2)}")
|
||||
print(f"Difference: {round(old_long['quote_spent']-total,2)}")
|
||||
|
||||
try:
|
||||
with open(f"../status/{pair}.status") as f:
|
||||
status_file = json.load(f)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("No status file")
|
||||
os._exit(0)
|
||||
|
||||
print(f"Old long price: {old_long['tp_price']}")
|
||||
print(f"Current price: {status_file['price']}")
|
||||
|
|
@ -0,0 +1,473 @@
|
|||
import sqlite3
|
||||
import sys
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
|
||||
'''
|
||||
In case the certificate's permissions suddenly change (in auto renewal, for example), reset them this way:
|
||||
/ sudo su
|
||||
# chmod -R 755 /etc/letsencrypt/live/
|
||||
# chmod -R 755 /etc/letsencrypt/archive/
|
||||
# ll /etc/letsencrypt/ (to verify permissions)
|
||||
'''
|
||||
|
||||
cache_requests = False
|
||||
if len(sys.argv)>1 and sys.argv[1]=="--cache_requests":
|
||||
cache_requests = True
|
||||
|
||||
|
||||
profits_database = "../profits/profits_database.db"
|
||||
hashes_db = {"fetch_last_n_deals":0,
|
||||
"fetch_last_n_deals_without_history":0,
|
||||
"fetch_full_log":0,
|
||||
"fetch_log":0,
|
||||
"daily_totals":0,
|
||||
"daily_totals_by_pair":0,
|
||||
"monthly_totals":0,
|
||||
"monthly_totals_by_pair":0,
|
||||
"get_averages":0,
|
||||
"total_profit":0,
|
||||
"total_profit_by_pair":0}
|
||||
|
||||
|
||||
def load_keys_from_db(file_name):
|
||||
#valid_keys = []
|
||||
|
||||
connection = sqlite3.connect(file_name)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute("SELECT * FROM credentials_table")
|
||||
data = cursor.fetchall()
|
||||
connection.close()
|
||||
|
||||
valid_keys = [line[1] for line in data]
|
||||
#for line in data:
|
||||
# valid_keys.append(line[1])
|
||||
|
||||
return valid_keys
|
||||
|
||||
|
||||
def query_total_profit(pair=None):
|
||||
'''
|
||||
Returns total profit of the trading pair.
|
||||
If no pair specified, returns the grand total of all pairs.
|
||||
'''
|
||||
connection = sqlite3.connect(profits_database)
|
||||
cursor = connection.cursor()
|
||||
|
||||
if pair is None:
|
||||
query = "SELECT SUM(amount) AS total_profit FROM profits_table"
|
||||
cursor.execute(query)
|
||||
connection.close()
|
||||
query_result = cursor.fetchall()
|
||||
return query_result[0][0]
|
||||
else:
|
||||
query = """SELECT pair, SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
GROUP BY pair;"""
|
||||
cursor.execute(query)
|
||||
connection.close()
|
||||
query_result = cursor.fetchall()
|
||||
for item in query_result:
|
||||
if item[0].replace("/","")==pair:
|
||||
return item[1]
|
||||
return 0
|
||||
|
||||
|
||||
def query_daily_totals(pair=None):
|
||||
'''
|
||||
Returns a dictionary of daily totals of the trading pair.
|
||||
If no pair specified, returns the totals of all pairs.
|
||||
'''
|
||||
#Connect to db
|
||||
connection = sqlite3.connect(profits_database)
|
||||
cursor = connection.cursor()
|
||||
|
||||
result = {}
|
||||
|
||||
if pair is None:
|
||||
query = """SELECT strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3,
|
||||
SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
GROUP BY day_utc3;"""
|
||||
cursor.execute(query)
|
||||
query_result = cursor.fetchall()
|
||||
connection.close()
|
||||
for item in query_result:
|
||||
result[item[0]] = item[1]
|
||||
else:
|
||||
query = """SELECT pair, strftime('%Y-%m-%d', timestamp, 'unixepoch', '-3 hours') AS day_utc3,
|
||||
SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
GROUP BY pair, day_utc3;"""
|
||||
cursor.execute(query)
|
||||
query_result = cursor.fetchall()
|
||||
connection.close()
|
||||
for item in query_result:
|
||||
if item[0].replace("/","")==pair:
|
||||
result[item[1]] = item[2]
|
||||
return result
|
||||
|
||||
|
||||
def query_monthly_totals(pair=None):
|
||||
'''
|
||||
Returns a dictionary of monthly totals of the trading pair.
|
||||
If no pair specified, returns the totals of all pairs.
|
||||
'''
|
||||
#Connect to db
|
||||
connection = sqlite3.connect(profits_database)
|
||||
cursor = connection.cursor()
|
||||
|
||||
result = {}
|
||||
|
||||
if pair is None:
|
||||
query = """SELECT strftime('%Y-%m', datetime(timestamp, 'unixepoch', '-3 hours')) AS month,
|
||||
SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
GROUP BY month;"""
|
||||
cursor.execute(query)
|
||||
query_result = cursor.fetchall()
|
||||
connection.close()
|
||||
for item in query_result:
|
||||
result[item[0]] = item[1]
|
||||
else:
|
||||
query = f"""SELECT pair, strftime('%Y-%m', datetime(timestamp, 'unixepoch', '-3 hours')) AS month,
|
||||
SUM(amount) AS total_profit
|
||||
FROM profits_table
|
||||
GROUP BY pair, month;"""
|
||||
cursor.execute(query)
|
||||
query_result = cursor.fetchall()
|
||||
connection.close()
|
||||
for item in query_result:
|
||||
if item[0].replace("/","")==pair:
|
||||
result[item[1]] = item[2]
|
||||
return result
|
||||
|
||||
|
||||
def last_n_deals(n):
|
||||
'''
|
||||
Returns a list of the latest n deals
|
||||
'''
|
||||
connection = sqlite3.connect(profits_database)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(f"SELECT * FROM profits_table ORDER BY timestamp DESC LIMIT {n}")
|
||||
result = cursor.fetchall()
|
||||
connection.close()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def last_n_deals_without_history(n):
|
||||
'''
|
||||
Like last_n_deals, but without returning the order history. Useful in bandwidth-restricted scenarios.
|
||||
'''
|
||||
|
||||
return [(row[0],row[1],row[2],row[3],row[4],"") for row in last_n_deals(n)]
|
||||
|
||||
|
||||
def last_n_lines(file_name,width,amount=4,full_log=False):
|
||||
|
||||
file_contents = []
|
||||
result = []
|
||||
|
||||
with open(file_name) as f:
|
||||
file_contents = f.readlines()
|
||||
|
||||
if full_log:
|
||||
for line in file_contents:
|
||||
result.append(line.strip())
|
||||
return result,len(file_contents)
|
||||
|
||||
for line in file_contents[::-1][:amount]:
|
||||
#trimmed = f"{line[0]}{line[12:21]}{line[23:]}".strip()
|
||||
#result.append(trimmed[:width])
|
||||
trimmed = line.strip()
|
||||
result.append(trimmed[:width])
|
||||
if len(trimmed)>width:
|
||||
result.append(trimmed[width:width*2])
|
||||
return result[:amount],len(file_contents)
|
||||
|
||||
|
||||
stats_api = Flask(__name__)
|
||||
|
||||
|
||||
@stats_api.route("/clear_caches")
|
||||
def clear_hashes():
|
||||
global hashes_db
|
||||
|
||||
'''
|
||||
GET request
|
||||
'''
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
hashes_db = {"fetch_last_n_deals":0,
|
||||
"fetch_last_n_deals_without_history":0,
|
||||
"fetch_full_log":0,
|
||||
"fetch_log":0,
|
||||
"daily_totals":0,
|
||||
"daily_totals_by_pair":0,
|
||||
"monthly_totals":0,
|
||||
"monthly_totals_by_pair":0,
|
||||
"get_averages":0,
|
||||
"total_profit":0,
|
||||
"total_profit_by_pair":0}
|
||||
return jsonify({"Done":0})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/fetch_last_n_deals")
|
||||
def fetch_last_n_deals():
|
||||
'''
|
||||
GET request
|
||||
Parameter: 'amount_of_deals' -> int
|
||||
'''
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
try:
|
||||
parameter = request.args.get("amount_of_deals")
|
||||
response_value = last_n_deals(parameter)
|
||||
if not cache_requests:
|
||||
return jsonify({"last_deals": response_value})
|
||||
response_hash = hash(str({"last_deals": response_value}))
|
||||
if hashes_db["fetch_last_n_deals"]!=response_hash:
|
||||
hashes_db["fetch_last_n_deals"] = response_hash
|
||||
return jsonify({"last_deals": response_value})
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({"last_deals":""})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/fetch_last_n_deals_without_history")
|
||||
def fetch_last_n_deals_without_history():
|
||||
'''
|
||||
GET request
|
||||
Parameter: 'amount_of_deals' -> int
|
||||
'''
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
try:
|
||||
parameter = request.args.get("amount_of_deals")
|
||||
#return jsonify({"last_deals": last_n_deals_without_history(parameter)})
|
||||
response_value = last_n_deals_without_history(parameter)
|
||||
if not cache_requests:
|
||||
return jsonify({"last_deals": response_value})
|
||||
response_hash = hash(str({"last_deals": response_value}))
|
||||
if hashes_db["fetch_last_n_deals_without_history"]!=response_hash:
|
||||
hashes_db["fetch_last_n_deals_without_history"] = response_hash
|
||||
return jsonify({"last_deals": response_value})
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({"last_deals":""})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/fetch_full_log")
|
||||
def fetch_full_log():
|
||||
'''
|
||||
GET request
|
||||
Parameters: 'exchange_name" -> string
|
||||
'''
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
try:
|
||||
exchange_name = request.args.get("exchange_name")
|
||||
width = 0
|
||||
last_lines,amount_of_lines = last_n_lines(f"../logs/{exchange_name}.log",width,0,full_log=True)
|
||||
if not cache_requests:
|
||||
return jsonify({"line": last_lines, "amount_of_lines": amount_of_lines})
|
||||
response_hash = hash(str({"line": last_lines, "amount_of_lines": amount_of_lines}))
|
||||
if hashes_db["fetch_full_log"]!=response_hash:
|
||||
hashes_db["fetch_full_log"] = response_hash
|
||||
return jsonify({"line": last_lines, "amount_of_lines": amount_of_lines})
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {"line": [""]*width,"amount_of_lines": 0}
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/fetch_log")
|
||||
def fetch_log():
|
||||
'''
|
||||
GET request
|
||||
Parameters: 'exchange_name" -> string
|
||||
'width' -> int
|
||||
'amount' -> int
|
||||
'''
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
try:
|
||||
exchange_name = request.args.get("exchange_name")
|
||||
width = int(request.args.get("width")) # type: ignore
|
||||
amount = int(request.args.get("amount")) # type: ignore
|
||||
last_lines,total_amount_of_lines = last_n_lines(f"../logs/{exchange_name}.log",width,amount)
|
||||
if not cache_requests:
|
||||
return jsonify({"line": last_lines, "amount_of_lines": total_amount_of_lines})
|
||||
response_hash = hash(str({"line": last_lines, "amount_of_lines": total_amount_of_lines}))
|
||||
if hashes_db["fetch_log"]!=response_hash:
|
||||
hashes_db["fetch_log"] = response_hash
|
||||
return jsonify({"line": last_lines, "amount_of_lines": total_amount_of_lines})
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return {"line": [""]*10,"amount_of_lines": 0}
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/daily_totals")
|
||||
def get_daily_totals():
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
daily_totals = query_daily_totals()
|
||||
if not cache_requests:
|
||||
return jsonify(daily_totals)
|
||||
response_hash = hash(str(daily_totals))
|
||||
if hashes_db["daily_totals"]!=response_hash:
|
||||
hashes_db["daily_totals"] = response_hash
|
||||
return jsonify(daily_totals)
|
||||
return jsonify({"no_changes": True})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/daily_totals_by_pair")
|
||||
def get_daily_totals_by_pair():
|
||||
'''
|
||||
GET request
|
||||
Parameters: 'base' -> string
|
||||
'quote' -> string
|
||||
'''
|
||||
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")
|
||||
daily_totals = query_daily_totals(f"{base}{quote}")
|
||||
if not cache_requests:
|
||||
return jsonify(daily_totals)
|
||||
response_hash = hash(str(daily_totals))
|
||||
if hashes_db["daily_totals_by_pair"]!=response_hash:
|
||||
hashes_db["daily_totals_by_pair"] = response_hash
|
||||
return jsonify(daily_totals)
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'Error': 'Halp'})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/monthly_totals")
|
||||
def get_monthly_totals():
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
monthly_totals = query_monthly_totals()
|
||||
if not cache_requests:
|
||||
return jsonify(monthly_totals)
|
||||
response_hash = hash(str(monthly_totals))
|
||||
if hashes_db["monthly_totals"]!=response_hash:
|
||||
hashes_db["monthly_totals"] = response_hash
|
||||
return jsonify(monthly_totals)
|
||||
return jsonify({"no_changes": True})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/monthly_totals_by_pair")
|
||||
def get_monthly_totals_by_pair():
|
||||
'''
|
||||
GET request
|
||||
Parameters: 'base' -> string
|
||||
'quote' -> string
|
||||
'''
|
||||
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")
|
||||
monthly_totals = query_monthly_totals(f"{base}{quote}")
|
||||
if not cache_requests:
|
||||
return jsonify(monthly_totals)
|
||||
response_hash = hash(str(monthly_totals))
|
||||
if hashes_db["monthly_totals_by_pair"]!=response_hash:
|
||||
hashes_db["monthly_totals_by_pair"] = response_hash
|
||||
return jsonify(monthly_totals)
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'Error': 'Halp'})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/get_averages")
|
||||
def get_averages():
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
try:
|
||||
daily_totals = query_daily_totals()
|
||||
val_30 = 0
|
||||
val_7 = 0
|
||||
acc_30 = []
|
||||
acc_7 = []
|
||||
for x in sorted(daily_totals):
|
||||
acc_30.append(daily_totals[x])
|
||||
acc_7.append(daily_totals[x])
|
||||
length_30 = min(30,len(acc_30)) #Last 30 days
|
||||
length_7 = min(7,len(acc_7)) #Last 7 days
|
||||
for _ in range(length_30):
|
||||
val_30 += acc_30.pop()
|
||||
for _ in range(length_7):
|
||||
val_7 += acc_7.pop()
|
||||
if not cache_requests:
|
||||
return jsonify({"30_day": val_30/length_30, "7_day": val_7/length_7})
|
||||
response_hash = hash(str({"30_day": val_30/length_30, "7_day": val_7/length_7}))
|
||||
if hashes_db["get_averages"]!=response_hash:
|
||||
hashes_db["get_averages"] = response_hash
|
||||
return jsonify({"30_day": val_30/length_30, "7_day": val_7/length_7})
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'Error': 'Halp'})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/total_profit")
|
||||
def total_profit():
|
||||
if "X-API-KEY" in request.headers and request.headers.get("X-API-KEY") in valid_keys:
|
||||
total = query_total_profit()
|
||||
if not cache_requests:
|
||||
return jsonify({"Total profit": total})
|
||||
response_hash = hash(str({"Total profit": total}))
|
||||
if hashes_db["total_profit"]!=response_hash:
|
||||
hashes_db["total_profit"] = response_hash
|
||||
return jsonify({"Total profit": total})
|
||||
return jsonify({"no_changes": True})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
@stats_api.route("/total_profit_by_pair")
|
||||
def total_profit_by_pair():
|
||||
'''
|
||||
GET request
|
||||
Parameters: 'base' -> string
|
||||
'quote' -> string
|
||||
'''
|
||||
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")
|
||||
total = query_total_profit(f"{base}{quote}")
|
||||
if not cache_requests:
|
||||
return jsonify({"Total profit": total})
|
||||
response_hash = hash(str({"Total profit": total}))
|
||||
if hashes_db["total_profit_by_pair"]!=response_hash:
|
||||
hashes_db["total_profit_by_pair"] = response_hash
|
||||
return jsonify({"Total profit": total})
|
||||
return jsonify({"no_changes": True})
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return jsonify({'Error': 'Halp'})
|
||||
return jsonify({'Error': 'API key invalid'}), 401
|
||||
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
|
||||
# Load valid keys from database
|
||||
valid_keys = load_keys_from_db("api_credentials.db")
|
||||
|
||||
#Waitress
|
||||
#serve(stats_api,host="0.0.0.0",port=5010)
|
||||
|
||||
#Dev server
|
||||
stats_api.run(host="0.0.0.0",port=5010)
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
Mandatory:
|
||||
=========
|
||||
0. Mobile app.
|
||||
1. Stats webpage.
|
||||
2. Instead of giving a list of order_ids to each trader, give a list of the open orders and that's it (for easier future development, partial order fills for example)
|
||||
3. Deploying script, both for testnet and for mainnet.
|
||||
|
||||
|
||||
Would be nice to have:
|
||||
=====================
|
||||
0. Trader order: alphabetical; by uptime; by safety orders, by percentage_to_completion. (Although this may be more suitable for the web and mobile apps)
|
||||
1. Local implementation of amount_to_precision, cost_to_precision and price_to_precision. (Unless the plan is to continue to use CCXT forever)
|
||||
2. Instead of cancelling and resending the take profit order, you could just edit it (Kucoin only supports editing on high frequency orders)
|
||||
|
||||
|
||||
Maybe it's a good idea?:
|
||||
=======================
|
||||
0. A fraction of the take profit order is taken as the first order of the next deal.
|
||||
* Starting at the second or third safety order?
|
||||
* This would address the issue of big spreads making the first order very expensive.
|
||||
* Can be problematic in parabolic runs, unless the take-profit order is not x% above the buy order, but above the current price.
|
||||
1. DUSTERS. If a trade is interrupted but the take profit sell order is still open, open another trader (different type, dust-trader, it remembers the original trade)
|
||||
that follows that order; when it closes, it takes note of the profit.
|
||||
a. The ability to import the dusters after an interruption will be key.
|
||||
b. The duster uses the order id as duster id
|
||||
c. Order on screen: BASE/QUOTE | order_id followed | current_price | deal_close_price | total_volume_on_close | pct_to_profit | uptime
|
||||
d. In status bar: Total funds to be released.
|
||||
e. Change main screen: x traders online | y dusters online
|
||||
f. Since they only need to monitor if one order is filled and the data is already locally available, the extra API load will be negligible.
|
||||
Loading…
Reference in New Issue