diff --git a/Lista5/apis.json b/Lista5/apis.json new file mode 100644 index 0000000..6f57f23 --- /dev/null +++ b/Lista5/apis.json @@ -0,0 +1,80 @@ +{ + "API": { + "bitbay": { + "name": "bitbay", + "url_orderbook": "https://bitbay.net/API/Public/", + "url_markets": "https://api.bitbay.net/rest/trading/ticker", + "orderbook_ending": "/orderbook.json", + "taker_fee": 0.0043 + }, + "bitrex": { + "name": "bitrex", + "url_orderbook": "https://api.bittrex.com/api/v1.1/public/getorderbook?market=", + "url_markets": "https://api.bittrex.com/api/v1.1/public/getmarkets", + "url_currencies": "https://api.bittrex.com/api/v1.1/public/getcurrencies", + "orderbook_ending": "&type=both", + "orderbook_separator": "-", + "taker_fee": 0.0075 + }, + "nbp": { + "name": "nbp", + "url_exchange": "http://api.nbp.pl/api/exchangerates/rates/a/", + "json_format": "?format=json" + }, + "stooq" : { + "name": "stooq", + "url_stock" : "https://stooq.pl/q/?s=" + } + }, + "FEES": { + "bitbay_fees": { + "AAVE": 0.54, + "ALG": 426, + "AMLT": 1743, + "BAT": 156, + "BCC": 0.001, + "BCP": 1237, + "BOB": 11645, + "BSV": 0.003, + "BTC": 0.0005, + "BTG": 0.001, + "COMP": 0.1, + "DAI": 81.0, + "DASH": 0.01, + "DOT": 0.1, + "EOS": 0.1, + "ETH": 0.006, + "EXY": 520, + "GAME": 479, + "GGC": 112, + "GNT": 403, + "GRT": 84, + "LINK": 2.7, + "LML": 1500, + "LSK": 0.3, + "LTC": 0.001, + "LUNA": 0.02, + "MANA": 100, + "MKR": 0.025, + "NEU": 572, + "NPXS": 17229.0, + "OMG": 14, + "PAY": 1523, + "QARK": 1019, + "REP": 3.2, + "SRN": 5717, + "SUSHI": 8.8, + "TRX": 1.0, + "UNI": 2.5, + "USDC": 125, + "USDT": 190, + "XBX": 5508, + "XIN": 5, + "XLM": 0.005, + "XRP": 0.1, + "XTZ": 0.1, + "ZEC": 0.004, + "ZRX": 56 + } + } +} \ No newline at end of file diff --git a/Lista5/apisUtilities.py b/Lista5/apisUtilities.py new file mode 100644 index 0000000..95ae586 --- /dev/null +++ b/Lista5/apisUtilities.py @@ -0,0 +1,194 @@ +import time +import requests +import yfinance as yf +from bs4 import BeautifulSoup + + +NORMALIZED_OPERATIONS = ['bids', 'asks'] +WAITING_TIME = 2 + + +def get_api_response(url): + response = requests.get(url) + if response.status_code == 200: + return response.json() + else: + # print("Request not successful: " + response.reason) + return None + + +def get_transfer_fees(stockFst, stockSnd, pairs): + fees = {stockFst.get_name(): {}, stockSnd.get_name(): {}} + fstStockFees = stockFst.get_withdrawal_fees() + sndStockFees = stockSnd.get_withdrawal_fees() + for pair in pairs: + fees[stockFst.get_name()][pair[0]] = fstStockFees[pair[0]] + fees[stockSnd.get_name()][pair[0]] = sndStockFees[pair[0]] + return fees + + +def find_common_currencies_pairs(fstStockPairs, sndStockPairs): + common = [] + for pair in fstStockPairs: + if pair in sndStockPairs: + common.append(pair) + return common + + +def exchange_base_currency(sourceCurrency, targetCurrency, quantity, apiInfo): + exchangeRate = get_currency_exchange_rate(sourceCurrency, targetCurrency, apiInfo) + return exchangeRate * quantity + + +def get_currency_exchange_rate(sourceCurrency, targetCurrency, apiInfo): + if sourceCurrency == targetCurrency: + return 1 + if sourceCurrency == 'PLN': + url = f"{apiInfo['API']['nbp']['url_exchange']}{targetCurrency}/{apiInfo['API']['nbp']['json_format']}" + response = get_api_response(url) + if response is not None: + return 1 / float(response['rates'][0]['mid']) + else: + return None + elif targetCurrency == 'PLN': + url = f"{apiInfo['API']['nbp']['url_exchange']}{sourceCurrency}/{apiInfo['API']['nbp']['json_format']}" + response = get_api_response(url) + if response is not None: + return float(response['rates'][0]['mid']) + else: + return None + + plnValue = get_currency_exchange_rate(sourceCurrency, 'PLN', apiInfo) + targetValue = get_currency_exchange_rate('PLN', targetCurrency, apiInfo) + if plnValue and targetValue: + return plnValue * targetValue + else: + return None + + +def get_current_foreign_stock_price(stock, baseCurrency, apiInfo): + # foreign stock prices are checked using yahoo finance API replacement + try: + tickerInfo = yf.Ticker(stock).info + currency = tickerInfo['currency'] if tickerInfo['currency'] else "USD" + return exchange_base_currency(currency, baseCurrency, + (float(tickerInfo['dayLow']) + float(tickerInfo['dayHigh'])) / 2, apiInfo) + except Exception: + raise ValueError(f'Wrong symbol: {stock}') + + +def get_foreign_exchange(stock): + # foreign market name is checked using yahoo finance API replacement + try: + tickerInfo = yf.Ticker(stock).info + return tickerInfo['exchange'] + + except Exception: + raise ValueError(f'Wrong symbol: {stock}') + + +def get_current_pl_stock_price(stock, baseCurrency, apiInfo): + # due to lack of free API for checking pl stock prices is resolved using HTML scrapping + stooqUrl = apiInfo["API"]["stooq"]["url_stock"] + try: + response = requests.get(f"{stooqUrl}{stock.lower()}").text + soup = BeautifulSoup(response, 'html.parser') + price = float(soup.find(id="t1").find(id='f13').find('span').get_text()) + return exchange_base_currency("PLN", baseCurrency, price, apiInfo) + + except Exception: + raise ValueError(f'Wrong symbol: {stock}') + + +def calculate_percentage_difference(order1, order2): + return round(((order1 - order2) / order1) * 100, 2) + + +def include_taker_fee(cost, stock, operation): + if operation == NORMALIZED_OPERATIONS[1]: + return cost * (1 + stock.get_taker_fee()) + elif operation == NORMALIZED_OPERATIONS[0]: + return cost * (1 - stock.get_taker_fee()) + + +def calculate_order_value(price, amount): + return price * amount + + +def calculate_arbitrage(sourceStock, targetStock, currency, baseCurrency, fees): + offersToBuyFrom = sourceStock.get_offers(currency, baseCurrency)[NORMALIZED_OPERATIONS[1]] + offersToSellTo = targetStock.get_offers(currency, baseCurrency)[NORMALIZED_OPERATIONS[0]] + volume = 0.0 + spentMoney = 0.0 + gainedMoney = 0.0 + transferFee = fees[sourceStock.get_name()][currency] + transferFeePaidNumber = 0 + operationNumber = 0 + + while offersToBuyFrom and offersToSellTo and offersToBuyFrom[0][0] < offersToSellTo[0][0]: + operationNumber += 1 + buyVolume = min(offersToBuyFrom[0][1], offersToSellTo[0][1]) + sellVolume = buyVolume + if transferFee > 0: + if sellVolume > transferFee: + sellVolume -= transferFee + transferFee = 0 + transferFeePaidNumber = operationNumber + else: + transferFee -= buyVolume + sellVolume = 0 + buyValue = calculate_order_value(offersToBuyFrom[0][0], buyVolume) + buyCost = include_taker_fee(buyValue, sourceStock, NORMALIZED_OPERATIONS[1]) + sellValue = calculate_order_value(offersToSellTo[0][0], sellVolume) + sellGain = include_taker_fee(sellValue, targetStock, NORMALIZED_OPERATIONS[0]) + + if buyCost < sellGain or transferFee > 0 or (transferFee == 0.0 and operationNumber == transferFeePaidNumber): + volume += buyVolume + spentMoney += buyCost + gainedMoney += sellGain + + offersToSellTo[0][1] -= sellVolume + if offersToSellTo[0][1] == 0: + del offersToSellTo[0] + offersToBuyFrom[0][1] -= buyVolume + if offersToBuyFrom[0][1] == 0: + del offersToBuyFrom[0] + + else: + break + + if gainedMoney < spentMoney: + gainedMoney = spentMoney = 0 + if spentMoney == 0 and gainedMoney == 0: + profitability = 0 + else: + profitability = round(((gainedMoney - spentMoney) / spentMoney) * 100, 2) + + return {'volume': volume, 'profitability': profitability, 'profit': round(gainedMoney - spentMoney, 5)} + + +def zad6(fstStock, sndStock, transferFees, currencyPairs): + resultsFromFstToSnd = [] + resultsFromSndToFst = [] + for currency in currencyPairs: + crypto = currency[0] + baseCurrency = currency[1] + print(f"Checking arbitrage for Cryptocurrency: {crypto}, base currency: {baseCurrency}") + offer1 = fstStock.get_offers(crypto, baseCurrency) + offer2 = sndStock.get_offers(crypto, baseCurrency) + if offer1 is not None and offer2 is not None: + if offer1.get(NORMALIZED_OPERATIONS[0], None) and offer1.get(NORMALIZED_OPERATIONS[1], None) \ + and offer2.get(NORMALIZED_OPERATIONS[0], None) and offer2.get(NORMALIZED_OPERATIONS[1], None): + resultFrom1To2 = calculate_arbitrage(fstStock, sndStock, crypto, baseCurrency, transferFees) + resultsFromFstToSnd.append((fstStock.get_name(), + sndStock.get_name(), crypto, baseCurrency, resultFrom1To2)) + resultFrom2To1 = calculate_arbitrage(sndStock, fstStock, crypto, baseCurrency, transferFees) + resultsFromSndToFst.append((sndStock.get_name(), + fstStock.get_name(), crypto, baseCurrency, resultFrom2To1)) + else: + print("Orderbooks do not contain buying and selling prices!") + else: + print("Something gone wrong during data acquisition") + time.sleep(WAITING_TIME) + print() + return resultsFromFstToSnd + resultsFromSndToFst diff --git a/Lista5/cryptoApis/Bitbay.py b/Lista5/cryptoApis/Bitbay.py new file mode 100644 index 0000000..c9c15cc --- /dev/null +++ b/Lista5/cryptoApis/Bitbay.py @@ -0,0 +1,39 @@ +from jsonUtilities import load_data_from_json +from apisUtilities import get_api_response +from cryptoApis.CryptoApi import CryptoApiInterface + + +def parse_bitbay_currencies(jsonData): + result = [] + if jsonData.get("items", None): + items = jsonData['items'] + for entry in items.keys(): + pair = entry.split('-') + result.append((pair[0], pair[1])) + return result + + +class Bitbay(CryptoApiInterface): + def __init__(self): + self.__data = load_data_from_json("apis.json")['API']['bitbay'] + self.__fees = load_data_from_json("apis.json")['FEES']["bitbay_fees"] + + def get_offers(self, currency, baseCurrency): + offers = get_api_response(f'{self.__data["url_orderbook"]}{currency}' + f'{baseCurrency}{self.__data["orderbook_ending"]}') + if offers is not None: + return offers + + def get_taker_fee(self): + return self.__data['taker_fee'] + + def get_markets(self): + marketInfo = get_api_response(self.__data['url_markets']) + if marketInfo is not None: + return parse_bitbay_currencies(marketInfo) + + def get_withdrawal_fees(self): + return self.__fees + + def get_name(self): + return self.__data['name'] diff --git a/Lista5/cryptoApis/Bitrex.py b/Lista5/cryptoApis/Bitrex.py new file mode 100644 index 0000000..37cee78 --- /dev/null +++ b/Lista5/cryptoApis/Bitrex.py @@ -0,0 +1,71 @@ +from apisUtilities import NORMALIZED_OPERATIONS, get_api_response +from cryptoApis.CryptoApi import CryptoApiInterface +from jsonUtilities import load_data_from_json + + +def parse_bitrex_orderbook(jsonData): + resultDictionary = {} + if jsonData.get("result", None): + table = [] + if jsonData["result"].get("buy", None): + pair = [] + for dictionary in jsonData["result"]["buy"]: + pair.append(dictionary["Rate"]) + pair.append(dictionary["Quantity"]) + table.append(pair.copy()) + pair.clear() + resultDictionary[NORMALIZED_OPERATIONS[0]] = table.copy() + table.clear() + if jsonData["result"].get("sell", None): + pair = [] + for dictionary in jsonData["result"]["sell"]: + pair.append(dictionary["Rate"]) + pair.append(dictionary["Quantity"]) + table.append(pair.copy()) + pair.clear() + resultDictionary[NORMALIZED_OPERATIONS[1]] = table.copy() + table.clear() + return resultDictionary + + +def parse_bitrex_currencies(jsonData): + result = [] + if jsonData.get("result", None): + for entry in jsonData['result']: + if entry.get("MarketCurrency", None) and entry.get("BaseCurrency", None): + result.append((entry['MarketCurrency'], entry['BaseCurrency'])) + return result + + +class Bitrex(CryptoApiInterface): + def __init__(self): + self.__data = load_data_from_json("apis.json")['API']['bitrex'] + + def get_offers(self, currency, baseCurrency): + offers = get_api_response(f'{self.__data["url_orderbook"]}{baseCurrency}' + f'{self.__data["orderbook_separator"]}' + f'{currency}{self.__data["orderbook_ending"]}') + if offers is not None: + return parse_bitrex_orderbook(offers) + + def get_taker_fee(self): + return self.__data['taker_fee'] + + def get_markets(self): + marketInfo = get_api_response(self.__data['url_markets']) + if marketInfo is not None: + return parse_bitrex_currencies(marketInfo) + + def get_withdrawal_fees(self): + fees = get_api_response(self.__data['url_currencies']) + dictionary = {} + if fees.get("result", None): + items = fees['result'] + for entry in items: + if entry.get('Currency', None) and entry.get('TxFee', None): + dictionary[entry['Currency']] = entry["TxFee"] + + return dictionary + + def get_name(self): + return self.__data['name'] diff --git a/Lista5/cryptoApis/CryptoApi.py b/Lista5/cryptoApis/CryptoApi.py new file mode 100644 index 0000000..241db58 --- /dev/null +++ b/Lista5/cryptoApis/CryptoApi.py @@ -0,0 +1,15 @@ +class CryptoApiInterface: + def get_offers(self, currency, baseCurrency): + raise NotImplementedError + + def get_taker_fee(self): + raise NotImplementedError + + def get_markets(self): + raise NotImplementedError + + def get_withdrawal_fees(self): + raise NotImplementedError + + def get_name(self): + raise NotImplementedError diff --git a/Lista5/jsonUtilities.py b/Lista5/jsonUtilities.py new file mode 100644 index 0000000..3c0e59b --- /dev/null +++ b/Lista5/jsonUtilities.py @@ -0,0 +1,16 @@ +import json + + +def load_data_from_json(path): + try: + with open(path, 'r') as data: + result = dict(json.load(data)) + return result + except json.decoder.JSONDecodeError: + return dict() + + +def save_data_to_json(path, data): + file = open(path, "w") + json.dump(data, file, indent=4) + file.close() diff --git a/Lista5/tasks.py b/Lista5/tasks.py new file mode 100644 index 0000000..63a87f5 --- /dev/null +++ b/Lista5/tasks.py @@ -0,0 +1,216 @@ +from tabulate import tabulate +from cryptoApis.Bitbay import Bitbay +from cryptoApis.Bitrex import Bitrex +from jsonUtilities import load_data_from_json, save_data_to_json +from apisUtilities import find_common_currencies_pairs, get_current_foreign_stock_price, get_current_pl_stock_price, \ + get_currency_exchange_rate, NORMALIZED_OPERATIONS, include_taker_fee, zad6, get_transfer_fees, get_foreign_exchange + +CATEGORIES = ["foreignStock", "plStock", "crypto", "currencies"] +DATA_PROCESSING_ERROR_MESSAGE = "Could not process provided data" +TAX = 0.19 +CRYPTO_APIS = [Bitbay(), Bitrex()] + + +def add_item_to_wallet(category, name, quantity, avpPrice, walletData): + entry = {'symbol': name, 'quantity': quantity, 'avgPrice': avpPrice} + if category in CATEGORIES: + print(f"Successfully added {name} to wallet") + walletData[category].append(entry) + else: + print(f"Invalid category: {category}") + + +def print_wallet(walletData): + print("----------------------------------------") + print("Wallet content:") + print(f"Base currency: {walletData['baseCurrency']}") + for cat in CATEGORIES: + print(cat) + for entry in walletData[cat]: + print(f"\tSymbol: {entry['symbol']}") + print(f"\tQuantity: {entry['quantity']}") + print(f"\tAverage buying price: {entry['avgPrice']}\n") + + +def include_tax(profit): + if profit > 0: + return (1 - TAX) * profit + else: + return profit + + +def find_best_crypto_market(crypto, quantity, baseCurrency): + results = [] + for api in CRYPTO_APIS: + leftToSell = quantity + sumGain = 0 + offers = api.get_offers(crypto, baseCurrency) + + if offers is not None and bool(offers) and offers.get(NORMALIZED_OPERATIONS[0], False): + offersToSellTo = offers[NORMALIZED_OPERATIONS[0]] + while leftToSell > 0: + offer = offersToSellTo[0] + if leftToSell >= offer[1]: + leftToSell -= offer[1] + gain = include_taker_fee(offer[1] * offer[0], api, NORMALIZED_OPERATIONS[0]) + sumGain += gain + else: + gain = include_taker_fee(leftToSell * offer[0], api, NORMALIZED_OPERATIONS[0]) + sumGain += gain + leftToSell = 0 + + del offersToSellTo[0] + + results.append((sumGain / quantity, api.get_name().upper())) + + res = sorted(results, reverse=True) + return res[0] + + +def find_pairs_with_currency(currency, pairs): + result = [] + for pair in pairs: + if pair[0] == currency: + result.append(pair) + return result + + +def analyze_portfolio(portfolioData, depth, baseCurrency): + returnData = [] + sumRow = ["Sum", "----", "----", 0, 0, 0, "----", "----"] + commonMarkets = find_common_currencies_pairs(CRYPTO_APIS[0].get_markets(), CRYPTO_APIS[1].get_markets()) + apiInfo = load_data_from_json("apis.json") + + for cat in CATEGORIES: + for entry in portfolioData[cat]: + currentRow = ["", 0, 0, 0, 0, 0, "----", "----"] + currentRow[0] = entry["symbol"] + try: + quantity = entry['quantity'] * depth + except ValueError: + quantity = DATA_PROCESSING_ERROR_MESSAGE + currentRow[1] = quantity + price = 0 + + if cat == CATEGORIES[0]: + try: + price = get_current_foreign_stock_price(entry['symbol'], baseCurrency, apiInfo) + exchange = get_foreign_exchange(entry['symbol']) + except ValueError: + price = DATA_PROCESSING_ERROR_MESSAGE + exchange = DATA_PROCESSING_ERROR_MESSAGE + + result = find_best_crypto_market(entry['symbol'], quantity, "USD") + if result is not None and price != DATA_PROCESSING_ERROR_MESSAGE: + basePrice = get_currency_exchange_rate("USD", baseCurrency, apiInfo) * result[0] + if basePrice > price: + price = basePrice + exchange = CRYPTO_APIS[1].get_name().upper() + + currentRow[2] = price + currentRow[6] = exchange + + elif cat == CATEGORIES[1]: + try: + price = get_current_pl_stock_price(entry['symbol'], baseCurrency, apiInfo) + except ValueError: + price = DATA_PROCESSING_ERROR_MESSAGE + currentRow[2] = price + if price != DATA_PROCESSING_ERROR_MESSAGE: + currentRow[6] = "Warsaw.SE" + + elif cat == CATEGORIES[2]: + result = find_best_crypto_market(entry['symbol'], quantity, baseCurrency) + if result is not None: + currentRow[2] = result[0] + currentRow[6] = result[1] + price = result[0] + else: + currentRow[2] = DATA_PROCESSING_ERROR_MESSAGE + currentRow[6] = DATA_PROCESSING_ERROR_MESSAGE + + pairs = find_pairs_with_currency(entry['symbol'], commonMarkets) + arbitrageList = zad6(CRYPTO_APIS[0], CRYPTO_APIS[1], get_transfer_fees(CRYPTO_APIS[0], + CRYPTO_APIS[1], pairs), pairs) + result = sorted(arbitrageList, key=lambda x: x[4]['profitability'], reverse=True) + if result[0]: + if result[0][4]['profit'] > 0: + currentRow[7] = f"Stocks: {result[0][0]} -> {result[0][1]}, " \ + f"pair: {result[0][2]}-{result[0][3]}, " \ + f"profit: {result[0][4]['profit']} {result[0][3]}" + else: + currentRow[7] = "There is no profitable arbitrage available for this currency" + else: + currentRow[7] = "There is no arbitrage available for this currency" + + elif cat == CATEGORIES[3]: + price = get_currency_exchange_rate(entry['symbol'], baseCurrency, apiInfo) + if price is None: + price = DATA_PROCESSING_ERROR_MESSAGE + currentRow[2] = price + + try: + value = round(price * quantity, 3) + except TypeError: + value = DATA_PROCESSING_ERROR_MESSAGE + currentRow[3] = value + + try: + profit = round(value - entry['avgPrice'] * quantity, 3) + except TypeError: + profit = DATA_PROCESSING_ERROR_MESSAGE + currentRow[4] = profit + + try: + netProfit = round(include_tax(profit), 3) + except TypeError: + netProfit = DATA_PROCESSING_ERROR_MESSAGE + currentRow[5] = netProfit + + for i in range(3, 6): + if currentRow[i] != DATA_PROCESSING_ERROR_MESSAGE: + sumRow[i] += currentRow[i] + + returnData.append(currentRow.copy()) + + returnData.append(sumRow.copy()) + + return returnData + + +def show_portfolio(portfolioData, depth): + depth = depth / 100 + baseCurrency = portfolioData["baseCurrency"] + print(f'Base currency: {baseCurrency}') + + headers = ["Symbol", "Quantity", "Weighted average selling price", "Value", "Profit", + "Net profit", "Selling market", "Arbitrage"] + + data_to_print = analyze_portfolio(portfolioData, depth, baseCurrency) + print(tabulate(data_to_print, headers=headers)) + + +if __name__ == '__main__': + + data = load_data_from_json("wallet.json") + print_wallet(data) + add_item_to_wallet("Banana", "ACT", 345.89, 23.56, data) + add_item_to_wallet("currencies", "NANA", 5.1, 101.11, data) + print_wallet(data) + + part = input("Enter percentage value of your wallet content to be sold (eg. 25): ") + try: + partValue = float(part) + except ValueError: + print(f'Wrong value {part}, assuming 10%') + partValue = 10 + if partValue <= 0 or partValue > 100: + print(f'Wrong value {part}, assuming 10%') + partValue = 10 + + print("Selling whole wallet") + show_portfolio(data, 100) + print(f"\nSelling {partValue}% of wallet") + show_portfolio(data, partValue) + + save_data_to_json("wallet.json", data) diff --git a/Lista5/wallet.json b/Lista5/wallet.json new file mode 100644 index 0000000..d05cf39 --- /dev/null +++ b/Lista5/wallet.json @@ -0,0 +1,36 @@ +{ + "baseCurrency": "EUR", + "foreignStock": [ + { + "symbol": "AAPL", + "quantity": 21, + "avgPrice": 102.35 + } + ], + "plStock": [ + { + "symbol": "KGH", + "quantity": 56, + "avgPrice": 40.66 + } + ], + "crypto": [ + { + "symbol": "LTC", + "quantity": 150.654, + "avgPrice": 140.16 + }, + { + "symbol": "BTC", + "quantity": 0.45356, + "avgPrice": 31141.56 + } + ], + "currencies": [ + { + "symbol": "CHF", + "quantity": 2500, + "avgPrice": 0.92 + } + ] +} \ No newline at end of file