974 lines
37 KiB
Python
Executable File
974 lines
37 KiB
Python
Executable File
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 #Default cooldown multiplier value
|
||
if "cooldown_multiplier" in self.read_config:
|
||
self.cooldown_multiplier = self.read_config["cooldown_multiplier"]
|
||
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_top_ask_price(self,symbol):
|
||
'''
|
||
Returns the top ask price for the given symbol
|
||
|
||
Parameters:
|
||
symbol: the symbol to get the top ask price for
|
||
|
||
Returns:
|
||
the top ask price for the given symbol
|
||
'''
|
||
|
||
orderbook = self.get_order_book(symbol)
|
||
if orderbook=={}:
|
||
self.logger.log_this("Can't fetch orderbook (from get_top_ask_price)",1,symbol)
|
||
return self.get_ticker_price(symbol)
|
||
try:
|
||
return orderbook["asks"][0][0]
|
||
except Exception as e:
|
||
self.logger.log_this(f"Exception getting top ask price: {e}",1,symbol)
|
||
return self.get_ticker_price(symbol)
|
||
|
||
|
||
def get_top_bid_price(self,symbol):
|
||
'''
|
||
Returns the top bid price for the given symbol
|
||
|
||
Parameters:
|
||
symbol: the symbol to get the top bid price for
|
||
|
||
Returns:
|
||
the top bid price for the given symbol
|
||
'''
|
||
|
||
orderbook = self.get_order_book(symbol)
|
||
if orderbook=={}:
|
||
self.logger.log_this("Can't fetch orderbook (from get_top_bid_price)",1,symbol)
|
||
return self.get_ticker_price(symbol)
|
||
try:
|
||
return orderbook["bids"][0][0]
|
||
except Exception as e:
|
||
self.logger.log_this(f"Exception getting top mid price: {e}",1,symbol)
|
||
return self.get_ticker_price(symbol)
|
||
|
||
|
||
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
|
||
|