DCAv2/exchange_wrapper.py

1105 lines
42 KiB
Python
Executable File

import json
import time
import requests
import credentials
import sqlite3
from copy import deepcopy
class Broker:
def __init__(self,exchange,broker_config,config_filename):
self.config_filename = config_filename
self.broker_config = broker_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.broker_config:
self.cooldown_multiplier = self.broker_config["cooldown_multiplier"]
self.wait_before_new_safety_order = 1
if "wait_before_new_safety_order" in self.broker_config:
self.wait_before_new_safety_order = self.broker_config["wait_before_new_safety_order"]
self.empty_order = {"id": "", "status": "", "filled": 0, "remaining": 0, "price": 0, "cost": 0, "fees": [], "symbol": ""}
self.retries = self.broker_config["retries"] if "retries" in self.broker_config else 5
self.slippage_default_threshold = self.broker_config["slippage_default_threshold"] if "slippage_default_threshold" in self.broker_config else .03
self.logger = Logger(self.broker_config)
self.write_order_history = True #This should be a toggle in config_file
#Initialize database
self.profits_database_filename = "profits/profits_database.db"
self.database_connection = sqlite3.connect(self.profits_database_filename)
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()
#Load markets
self.markets = self.exchange.load_markets()
#Populates deals cache
self.deals_cache_length = 10
self.deals_list = self.preload_deals(amount_to_preload=self.deals_cache_length)
def preload_deals(self,amount_to_preload=10):
'''
Reads the last n deals from the database and returns them in a list
'''
connection = sqlite3.connect(self.profits_database_filename)
cursor = connection.cursor()
cursor.execute(f"SELECT * FROM profits_table WHERE exchange_name = ? ORDER BY timestamp DESC LIMIT ?", (self.get_exchange_name(), amount_to_preload))
result = cursor.fetchall()
connection.close()
return [(row[0],row[1],row[2],row[3],row[4],"") for row in result]
def get_deals_cache(self):
return self.deals_list
def get_symbol(self,pair):
if "/" in pair:
return pair
for item in self.markets:
if f"{self.markets[item]['base']}{self.markets[item]['quote']}"==pair:
return self.markets[item]["symbol"]
return "Error"
def all_markets(self,no_retries=False):
retries = self.retries
while retries>0:
try:
self.markets = self.exchange.load_markets()
return self.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 validate_market(self,symbol):
'''
Checks that the market for the symbol exists, that it's a spot market and that it's active.
Returns True if the market is valid, False otherwise.
'''
if symbol not in self.markets:
self.logger.log_this(f"Market {symbol} not found in the exchange")
return False
if self.markets[symbol]['spot'] == False:
self.logger.log_this(f"Market {symbol} is not a spot market")
return False
if self.markets[symbol]['active'] == False:
self.logger.log_this(f"Market {symbol} is not active")
return False
return True
def reload_markets(self):
try:
self.markets = 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 get_trades_timestamps(self,pair,timespan,no_retries=False):
'''
Returns the timestamps of the last trades from the database for the boosting algorithm
'''
retries = self.retries
while retries>0:
try:
database_connection = sqlite3.connect(self.profits_database_filename)
database_cursor = database_connection.cursor()
database_cursor.execute(f"SELECT * FROM profits_table WHERE timestamp >= {time.time()-timespan} ORDER BY timestamp")
rows = database_cursor.fetchall()
return [item[0] for item in rows if item[1]==pair]
except Exception as e:
self.logger.log_this(f"Exception in preload_timestamps: {e}")
if no_retries:
break
retries-=1
time.sleep(self.wait_time)
return []
def write_profit_to_cache(self,dataset):
'''
dataset format: (timestamp,pair,amount,exchange_name,order_id,order_history)
'''
self.deals_list.insert(0,(dataset[0],dataset[1],dataset[2],dataset[3],dataset[4],""))
self.deals_list = self.deals_list[:self.deals_cache_length]
return 0
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(self.profits_database_filename)
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(self.profits_database_filename)
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_wait_before_new_safety_order(self):
return self.wait_before_new_safety_order
def set_wait_before_new_safety_order(self,value:float):
self.wait_before_new_safety_order = value
return 0
def get_default_order_size(self):
return self.broker_config["default_order_size"]
def set_default_order_size(self,size):
try:
self.broker_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.broker_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.broker_config)
def set_config(self,new_config):
self.broker_config = deepcopy(new_config)
return 0
def reload_config_file(self):
try:
with open(self.config_filename) as f:
self.broker_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.broker_config["pairs"]:
self.broker_config["pairs"].append(pair)
return 0
return 1
def remove_pair_from_config(self,pair):
try:
if pair in self.broker_config["pairs"]:
self.broker_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.broker_config["pairs"]
def clear_pairs(self):
self.broker_config["pairs"].clear()
return 0
def get_lap_time(self):
return self.broker_config["lap_time"]
def set_lap_time(self,new_lap_time):
try:
self.broker_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, backup=False):
try:
if backup:
with open(f"{self.exchange}.bak","w") as c:
c.write(json.dumps(self.broker_config, indent=4))
with open(f"{self.config_filename}","w") as f:
f.write(json.dumps(self.broker_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.get_exchange_name()=="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.get_exchange_name()=="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.get_exchange_name()=="binance":
return self.get_opened_orders_binance(pairs)
return self.get_opened_orders()
#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.broker_config and self.broker_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.get_exchange_name()=="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.broker_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:
to_buy = float(size)/self.get_top_ask_price(pair)
amount = self.amount_to_precision(pair,to_buy)
else:
amount = self.amount_to_precision(pair,size) #Market sell orders are always nominated in base currency
order_to_send = self.exchange.create_order(pair,"market",side,amount)
time.sleep(self.wait_time)
# Wait a bit more when dealing with Kucoin
return self.get_order(order_to_send["id"],pair)
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 price_too_high_error(self, error_object):
'''
Checks if the error is a price too high error.
Receives an error object.
Returns True if the error is a price too high error, False otherwise.
:param error_object: The error object.
:return: Boolean value
'''
return "the highest price limit for buy orders is" in str(error_object).lower()
def price_too_low_error(self, error_object):
'''
Checks if the error is a price too low error.
Receives an error object.
Returns True if the error is a price too low error, False otherwise.
:param error_object: The error object.
:return: Boolean value
'''
return "the lowest price limit for sell orders is" in str(error_object).lower()
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
elif self.price_too_high_error(e): #In high volatility moments, OKX sometimes just needs a bit of extra thinking time.
time.sleep(self.wait_time)
elif self.price_too_low_error(e):
time.sleep(self.wait_time)
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","bybit"]:
return float(market["limits"]["amount"]["min"])
elif self.get_exchange_name() in ["kucoin"]:
return (float(market["limits"]["cost"]["min"])+.1)/self.get_ticker_price(pair)
elif self.get_exchange_name() in ["gateio"]:
return (float(market["limits"]["cost"]["min"])+.25)/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"])+.5)/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() in ["gateio", "bybit"]:
#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", "bybit"]:
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")
self.log_list_max_length = 10
self.log_list = self.preload_logs()
def preload_logs(self):
try:
with open(f"logs/{self.exchange_name}.log","r") as f:
self.log_list = f.readlines()
return self.log_list[-self.log_list_max_length:]
except Exception as e:
print(e)
return []
def set_log_list_max_length(self, amount):
self.log_list_max_length = amount
return self.log_list_max_length
def get_log_list(self):
return self.log_list
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 -1: Force Telegram only
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==-1:
self.send_tg_message(message,ignore_config=True)
return 0
if level<2:
try:
#Write to log file
with open(f"logs/{self.exchange_name}.log","a") as log_file:
log_file.write(text+"\n")
log_file.close()
#Append to log list
self.log_list.append(text)
#Trim log list
self.log_list = self.log_list[-self.log_list_max_length:]
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