Initial commit

This commit is contained in:
Nicolás Sánchez 2024-10-24 23:33:47 -03:00
commit a8daf9c8b1
33 changed files with 7847 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -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

1144
changelog.txt Executable file

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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
}

217
duster.py Normal file
View File

@ -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))

928
exchange_wrapper.py Executable file
View File

@ -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

4
logs/binance.log Normal file
View File

@ -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

4
logs/gateio.log Normal file
View 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

4
logs/kucoin.log Normal file
View 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

4
logs/okex.log Normal file
View 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

20
logs/reset_log_files.py Normal file
View 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")

1674
main.py Normal file

File diff suppressed because it is too large Load Diff

25
profits/db_read.py Normal file
View File

@ -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()

View File

@ -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")

21
profits/last_n_deals.py Normal file
View File

@ -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])

View File

@ -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")

44
profits/monthlypair.py Executable file
View File

@ -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

102
profits/per_exchange.py Executable file
View File

@ -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

View File

@ -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)

136
profits/profit_report.py Normal file
View File

@ -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)

42
profits/profit_since_short.py Executable file
View File

@ -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)}")

View File

@ -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)}")

View File

@ -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)}")

1806
trader.py Executable file

File diff suppressed because it is too large Load Diff

54
utils/check_for_duplicates.py Executable file
View File

@ -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")

591
utils/commander.py Normal file
View File

@ -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}&quote={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}&quote={quote}&from_file={from_file}"
print(json.loads(requests.get(url,headers=headers).content))
input("Press ENTER to continue ")

19
utils/generate_keys.py Normal file
View File

@ -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()

13
utils/is_it_running.py Executable file
View File

@ -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")

15
utils/read_key_db.py Normal file
View File

@ -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)

83
utils/recreate_orders.py Normal file
View File

@ -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)

53
utils/short_report.py Normal file
View File

@ -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']}")

View File

@ -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)

29
utils/todo.txt Executable file
View File

@ -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.