1144 lines
42 KiB
Python
Executable File
1144 lines
42 KiB
Python
Executable File
import collections
|
|
import time
|
|
import credentials
|
|
import sqlite3
|
|
from contextlib import contextmanager
|
|
from requests import get as requests_get
|
|
from json import load, dumps
|
|
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 = .5 #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.follow_order_history = False #This should be a toggle in config_file
|
|
self.write_order_history = False #This should be a toggle in config_file
|
|
|
|
#Initialize database
|
|
self.profits_database_filename = "profits/profits_database.db"
|
|
|
|
self._db = sqlite3.connect(self.profits_database_filename,
|
|
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
|
|
check_same_thread=False)
|
|
self._db.row_factory = sqlite3.Row
|
|
with self._db:
|
|
self._db.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
|
|
)
|
|
''')
|
|
|
|
#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)
|
|
|
|
|
|
@contextmanager
|
|
def _cur(self):
|
|
'''
|
|
Database cursor
|
|
'''
|
|
cur = self._db.cursor()
|
|
try:
|
|
yield cur
|
|
finally:
|
|
cur.close()
|
|
|
|
|
|
def preload_deals(self,amount_to_preload=10):
|
|
'''
|
|
Reads the last n deals from the database and returns them in a list
|
|
'''
|
|
query = "SELECT * FROM profits_table WHERE exchange_name = ? ORDER BY timestamp DESC LIMIT ?"
|
|
with self._cur() as cur:
|
|
cur.execute(query, (self.get_exchange_name(), amount_to_preload))
|
|
result = cur.fetchall()
|
|
|
|
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
|
|
'''
|
|
|
|
limit = time.time()-timespan
|
|
query = "SELECT * FROM profits_table WHERE timestamp >= ? ORDER BY timestamp"
|
|
|
|
with self._cur() as cur:
|
|
cur.execute(query,(limit,))
|
|
rows = cur.fetchall()
|
|
return [item[0] for item in rows if item[1]==pair]
|
|
|
|
|
|
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)
|
|
'''
|
|
|
|
query = "INSERT INTO profits_table VALUES(?, ?, ?, ?, ?, ?)"
|
|
with self._db:
|
|
self._db.execute(query, dataset)
|
|
return 0
|
|
|
|
|
|
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.
|
|
'''
|
|
|
|
query = f"SELECT * FROM profits_table WHERE pair = ? ORDER BY timestamp DESC LIMIT 1;"
|
|
with self._cur() as cur:
|
|
cur.execute(query, (order['symbol'],))
|
|
result = cur.fetchone()
|
|
if result is None:
|
|
return False
|
|
return order["id"]==result[4]
|
|
|
|
|
|
def get_write_order_history(self):
|
|
return self.write_order_history
|
|
|
|
def get_follow_order_history(self):
|
|
return self.follow_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 = 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(dumps(self.broker_config, indent=4))
|
|
with open(f"{self.config_filename}","w") as f:
|
|
f.write(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()
|
|
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:
|
|
self.last_price = self.exchange.fetch_ticker(symbol)["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 all open orders on the exchange
|
|
|
|
:param pairs: list of pairs to get opened orders
|
|
:return: list of all open orders
|
|
'''
|
|
|
|
if pairs is None:
|
|
pairs = []
|
|
try:
|
|
if self.get_exchange_name()=="binance":
|
|
return self.get_opened_orders_binance(pairs)
|
|
return self.get_opened_orders()
|
|
except Exception as e:
|
|
self.logger.log_this(f"Exception in fetch_open_orders: {e}",2)
|
|
return []
|
|
|
|
|
|
def fetch_closed_orders(self,pairs=None) -> list:
|
|
'''
|
|
Returns a list of all closed orders on the exchange
|
|
|
|
:param pairs: list of pairs to get opened orders
|
|
:return: list of all open orders
|
|
'''
|
|
|
|
if pairs is None:
|
|
pairs = []
|
|
try:
|
|
if self.get_exchange_name()=="binance":
|
|
return self.get_closed_orders_binance(pairs)
|
|
return self.get_closed_orders()
|
|
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 open 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_closed_orders(self,pair=None,no_retries=False): #It should return a list of all opened orders
|
|
'''
|
|
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
|
|
'''
|
|
|
|
retries = self.retries
|
|
while retries>0:
|
|
try:
|
|
return self.exchange.fetch_closed_orders(pair)
|
|
except Exception as e:
|
|
self.logger.log_this(f"Exception in get_closed_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 get_closed_orders_binance(self,pairs):
|
|
'''
|
|
Returns a list of all the closed orders on the exchange
|
|
|
|
:param pairs: list of pairs
|
|
:return: list of all the closed 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_closed_orders()
|
|
result = []
|
|
for pair in pairs:
|
|
a = self.exchange.fetch_closed_orders(pair)
|
|
result.extend(iter(a))
|
|
return result
|
|
except Exception as e:
|
|
self.logger.log_this(f"Exception in get_closed_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
|
|
'''
|
|
|
|
tries = self.retries//2
|
|
while tries>0:
|
|
try:
|
|
while self.get_order(id,symbol)["status"]=="open":
|
|
self.exchange.cancel_order(id,symbol)
|
|
time.sleep(self.wait_time)
|
|
return 0
|
|
except Exception as e:
|
|
if self.get_order(id,symbol)["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
|
|
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(symbol, 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(symbol,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(symbol,"limit",side,base_amount,price)
|
|
time.sleep(self.wait_time)
|
|
return self.get_order(new_order["id"],symbol)
|
|
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
|
|
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(symbol)
|
|
amount = self.amount_to_precision(symbol,to_buy)
|
|
else:
|
|
amount = self.amount_to_precision(symbol,size) #Market sell orders are always nominated in base currency
|
|
|
|
order_to_send = self.exchange.create_order(symbol,"market",side,amount)
|
|
time.sleep(self.wait_time)
|
|
# Wait a bit more when dealing with Kucoin
|
|
|
|
return self.get_order(order_to_send["id"],symbol)
|
|
except Exception as e:
|
|
self.logger.log_this(f"Exception in new_market_order: {e}",1,symbol)
|
|
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
|
|
while tries>=0:
|
|
try:
|
|
order_to_send = self.exchange.create_order(symbol,"limit",side,self.amount_to_precision(symbol,size),price)
|
|
time.sleep(self.wait_time)
|
|
return self.get_order(order_to_send["id"],symbol)
|
|
#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(symbol,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
|
|
while tries>0:
|
|
try:
|
|
return self.exchange.fetch_order(id,symbol)
|
|
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
|
|
while tries>0:
|
|
try:
|
|
return self.exchange.market(symbol)
|
|
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
|
|
while tries>0:
|
|
try:
|
|
return self.exchange.fetch_ticker(symbol)
|
|
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 = collections.deque(maxlen=self.log_list_max_length)
|
|
self.preload_logs()
|
|
|
|
|
|
def preload_logs(self):
|
|
try:
|
|
with open(f"logs/{self.exchange_name}.log","r") as f:
|
|
for line in f:
|
|
self.log_list.append(line.rstrip("\n"))
|
|
return 0
|
|
except Exception as e:
|
|
print(e)
|
|
return 1
|
|
|
|
|
|
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 list(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
|
|
'''
|
|
try:
|
|
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
|
|
except Exception as e:
|
|
self.log_this(f"Error in send_tg_message: {e}",1)
|
|
return 1
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|