├── Framework ├── out │ └── build │ │ └── .hide ├── .gitattributes ├── Scratch │ ├── requirments.txt │ ├── SpotTickerLink.txt │ └── timing.txt ├── ReadMEPics │ ├── Outline.png │ ├── Arb_Example.png │ └── Parallel_Example.png ├── CMakeLists.txt ├── create_user_settings.sh ├── shell_driver.sh ├── Header_Files │ ├── combinations.hpp │ ├── graph.hpp │ ├── amount_optimization.hpp │ ├── bellmon_ford.hpp │ ├── arbitrage_finder.hpp │ └── exchange_api_pull.hpp ├── Symbol_Data_Files │ └── request_coingecko_api.py ├── main.cpp └── generate_user_settings.py ├── .DS_Store ├── .vscode ├── c_cpp_properties.json └── settings.json ├── LICENSE └── README.md /Framework/out/build/.hide: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Framework/.gitattributes: -------------------------------------------------------------------------------- 1 | out/** linguist-vendored 2 | CMakeFiles/** linguist-vendored -------------------------------------------------------------------------------- /Framework/Scratch/requirments.txt: -------------------------------------------------------------------------------- 1 | libcurl 7.79.1 2 | nlohmann-json 3.11.2 3 | cmake 3.25.2 -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrbjrb1212/Cpp-Crypto-Arbitrage-Framework/HEAD/.DS_Store -------------------------------------------------------------------------------- /Framework/ReadMEPics/Outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrbjrb1212/Cpp-Crypto-Arbitrage-Framework/HEAD/Framework/ReadMEPics/Outline.png -------------------------------------------------------------------------------- /Framework/ReadMEPics/Arb_Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrbjrb1212/Cpp-Crypto-Arbitrage-Framework/HEAD/Framework/ReadMEPics/Arb_Example.png -------------------------------------------------------------------------------- /Framework/ReadMEPics/Parallel_Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrbjrb1212/Cpp-Crypto-Arbitrage-Framework/HEAD/Framework/ReadMEPics/Parallel_Example.png -------------------------------------------------------------------------------- /Framework/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") 4 | 5 | project(Crypto) 6 | 7 | find_package(nlohmann_json REQUIRED) 8 | include_directories(${nlohmann_json_INCLUDE_DIRS}) 9 | 10 | find_package(CURL REQUIRED) 11 | include_directories(${CURL_INCLUDE_DIRS}) 12 | 13 | add_executable(${PROJECT_NAME} main.cpp) 14 | target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json curl) -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "${workspaceFolder}/**" 7 | ], 8 | "defines": [], 9 | "macFrameworkPath": [ 10 | "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks" 11 | ], 12 | "compilerPath": "/usr/bin/clang", 13 | "cStandard": "c17", 14 | "cppStandard": "c++98", 15 | "intelliSenseMode": "macos-clang-x64", 16 | "configurationProvider": "ms-vscode.makefile-tools" 17 | } 18 | ], 19 | "version": 4 20 | } -------------------------------------------------------------------------------- /Framework/Scratch/SpotTickerLink.txt: -------------------------------------------------------------------------------- 1 | Binance base - https://api.binance.com 2 | - /api/v3/ticker/price for all trading pairs and curr price 3 | 4 | Huobi base - https://api.huobi.pro/ 5 | /market/tickers - for a list of all currencies trading pairs and information about it 6 | - Want to parse for the "ask" to get the price for my graph 7 | /v1/common/currencys - for a list of all currencies 8 | 9 | Kucoin base - https://api.kucoin.com 10 | /api/v1/market/allTickers - for a list of all currencies trading pairs and information about it 11 | - Want to parse for the "buy" to get the price for my graph 12 | 13 | BitMart base - https://api-cloud.bitmart.com 14 | /spot/v2/ticker - for a list of all currencies trading pairs and information about it 15 | 16 | Bitget base - https://api.bitget.com/ 17 | /api/spot/v1/market/tickers - for a list of all currencies trading pairs and information about it 18 | 19 | Gate.io base - https://api.gateio.ws/api/v4 20 | /spot/tickers - for a list of all currencies trading pairs and information about it 21 | 22 | -------------------------------------------------------------------------------- /Framework/create_user_settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ############################################# 3 | # # 4 | # Script to generate user setting via the # 5 | # generate_user_setting.py program. # 6 | # Result of this script is a txt with user # 7 | # defined settings and a generated txt # 8 | # of trading pairs. # 9 | # # 10 | ############################################# 11 | 12 | filename="user_settings.txt" 13 | 14 | python3 generate_user_settings.py $filename 15 | cp $filename out/build/ 16 | contents=$(cat "$filename") 17 | coinReq=$(echo "$contents" | awk -F '[= ]' '/coinReq=/{print $2}') 18 | volReq=$(echo "$contents" | awk -F '[= ]' '/volReq=/{print $2}') 19 | python3 Symbol_Data_Files/request_coingecko_api.py $coinReq $volReq 20 | mv Viable_Trading_Pairs.txt Symbol_Data_Files/ 21 | 22 | if [ -e "$filename" ] 23 | then 24 | # clear 25 | echo "user_settings are generated" 26 | echo "building framework" 27 | cmake -S . -B out/build/ 28 | fi 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 John Billos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Framework/shell_driver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ################################################# 3 | # # 4 | # Shell script to run the arbitrage framework # 5 | # - A single execution of this will facilite # 6 | # user setting creation and execute # 7 | # iterations of the frame work # 8 | # # 9 | ################################################# 10 | 11 | 12 | 13 | filename="user_settings.txt" 14 | fileCopyLoc="out/build/user_settings.txt" 15 | 16 | if [ -e "$filename" ] 17 | then 18 | if diff "$filename" "$fileCopyLoc" > /dev/null 19 | then 20 | # build the framework 21 | echo "user_settings are already generated" 22 | echo "building framework" 23 | echo "" 24 | cmake -S . -B out/build/ 25 | else 26 | bash create_user_settings.sh 27 | fi 28 | 29 | else 30 | # create user_settings from console input 31 | bash create_user_settings.sh 32 | fi 33 | 34 | clear 35 | cd out/build/ 36 | # compile the frame work 37 | make 38 | echo "" 39 | echo "finished framework build" 40 | ./Crypto -------------------------------------------------------------------------------- /Framework/Header_Files/combinations.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | using namespace std; 7 | 8 | /* 9 | * 10 | * Standalone function that generates combinations from strings 11 | * 12 | */ 13 | void generateCombinations(int N, string currentCombo, vector& combos, vector& letters) { 14 | // base case: we've reached the desired length of the combination 15 | if (currentCombo.length() == N) 16 | { 17 | cout << currentCombo << endl; 18 | } 19 | else 20 | { 21 | // recursive case: generate all possible combinations by appending "A" or "B" 22 | for(string letter : letters) 23 | { 24 | generateCombinations(N, currentCombo + letter, combos, letters); 25 | } 26 | } 27 | } 28 | 29 | 30 | /* 31 | * 32 | * Standalone function that generates combinations from characters 33 | * 34 | */ 35 | void generateCombinations(int N, string currentCombo, vector& combos, vector& letters) { 36 | // base case: we've reached the desired length of the combination 37 | if (currentCombo.length() == N) 38 | { 39 | combos.push_back(currentCombo); 40 | } 41 | else 42 | { 43 | for(char letter : letters) 44 | { 45 | generateCombinations(N, currentCombo + letter, combos, letters); 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Framework/Scratch/timing.txt: -------------------------------------------------------------------------------- 1 | Pull Ticker, ArbDetect and Pull Orderbook (From USDT): 2 | 3512495 microseconds 3 | 3555046 microseconds 4 | 3708147 microseconds 5 | 6 | 5-Path serial arbirtage detector (USDT): 7 | 1135686 microseconds 8 | 1126452 microseconds 9 | 1126452 microseconds 10 | 11 | 5-Path slightly parallel arb detector (USDT): 12 | 332959 microseconds 13 | 251890 microseconds 14 | 255589 microseconds 15 | 188364 microseconds 16 | 123132 microseconds 17 | 18 | 4-Path serial arbirtage detector (USDT): 19 | 65227 microseconds 20 | 63268 microseconds 21 | 64293 microseconds 22 | 23 | 4-Path parallel arb detector (USDT): 24 | 9707 microseconds 25 | 8456 microseconds 26 | 9933 microseconds 27 | 28 | 3-Path serial arbritage detector (USDT): 29 | 771 microseconds 30 | 802 microseconds 31 | 778 microseconds 32 | 33 | 3-Path parallel arb detector (USDT): 34 | 4708 microseconds 35 | 3397 microseconds 36 | 8529 microseconds 37 | 38 | Destroy and Recreate Graph gateio time: 39 | 2244115 microseconds 40 | 2192950 microseconds 41 | 2250641 microseconds 42 | 43 | 44 | Update the graph gateio time: 45 | 2127464 microseconds 46 | 2197382 microseconds 47 | 1998020 microseconds 48 | 49 | 50 | Destroy and Recreate Graph all exchanges time (10 iterations): 51 | 19805107 microseconds 52 | 20957517 microseconds 53 | 21514038 microseconds 54 | 55 | 56 | Update the graph all exchanges time (10 iterations): 57 | 22304184 58 | 22949285 59 | 24215379 60 | 61 | Update the graph all exchanges time (10 iterations; exclude set graph time): 62 | 22429368 63 | 21704841 64 | 21748287 65 | 66 | 67 | 68 | 69 | From Parse to add to graph time: 70 | 969 microseconds 71 | 1004 microseconds 72 | 1032 microseconds 73 | 74 | Curl time: 75 | 1346582 microseconds 76 | 1341916 microseconds 77 | 1330167 microseconds 78 | 79 | 80 | JSON_pare time: 81 | 34491 microseconds 82 | 17585 microseconds 83 | 16379 microseconds 84 | 25111 microseconds 85 | 86 | 87 | 88 | 89 | sequential API pull/parse/update times: 90 | 9.626 seconds 91 | 9.648 seconds 92 | 9.711 seconds 93 | 9.377 seconds 94 | 9.228 seconds 95 | 96 | Parallel API pull/parse/update times: 97 | 2.164 seconds 98 | 2.010 seconds 99 | 1.990 seconds 100 | 2.106 seconds 101 | 2.166 seconds -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.js": "javascriptreact", 4 | "__bit_reference": "cpp", 5 | "__bits": "cpp", 6 | "__config": "cpp", 7 | "__debug": "cpp", 8 | "__errc": "cpp", 9 | "__hash_table": "cpp", 10 | "__locale": "cpp", 11 | "__mutex_base": "cpp", 12 | "__node_handle": "cpp", 13 | "__nullptr": "cpp", 14 | "__split_buffer": "cpp", 15 | "__string": "cpp", 16 | "__threading_support": "cpp", 17 | "__tuple": "cpp", 18 | "array": "cpp", 19 | "atomic": "cpp", 20 | "bitset": "cpp", 21 | "cctype": "cpp", 22 | "chrono": "cpp", 23 | "clocale": "cpp", 24 | "cmath": "cpp", 25 | "compare": "cpp", 26 | "complex": "cpp", 27 | "concepts": "cpp", 28 | "cstdarg": "cpp", 29 | "cstddef": "cpp", 30 | "cstdint": "cpp", 31 | "cstdio": "cpp", 32 | "cstdlib": "cpp", 33 | "cstring": "cpp", 34 | "ctime": "cpp", 35 | "cwchar": "cpp", 36 | "cwctype": "cpp", 37 | "exception": "cpp", 38 | "initializer_list": "cpp", 39 | "ios": "cpp", 40 | "iosfwd": "cpp", 41 | "iostream": "cpp", 42 | "istream": "cpp", 43 | "limits": "cpp", 44 | "locale": "cpp", 45 | "memory": "cpp", 46 | "mutex": "cpp", 47 | "new": "cpp", 48 | "optional": "cpp", 49 | "ostream": "cpp", 50 | "ratio": "cpp", 51 | "sstream": "cpp", 52 | "stdexcept": "cpp", 53 | "streambuf": "cpp", 54 | "string": "cpp", 55 | "string_view": "cpp", 56 | "system_error": "cpp", 57 | "tuple": "cpp", 58 | "type_traits": "cpp", 59 | "typeinfo": "cpp", 60 | "unordered_map": "cpp", 61 | "variant": "cpp", 62 | "vector": "cpp", 63 | "algorithm": "cpp", 64 | "__tree": "cpp", 65 | "any": "cpp", 66 | "deque": "cpp", 67 | "forward_list": "cpp", 68 | "fstream": "cpp", 69 | "iomanip": "cpp", 70 | "map": "cpp", 71 | "numeric": "cpp", 72 | "ranges": "cpp", 73 | "span": "cpp", 74 | "stack": "cpp", 75 | "unordered_set": "cpp", 76 | "valarray": "cpp", 77 | "filesystem": "cpp", 78 | "utility": "cpp" 79 | }, 80 | "cmake.configureOnOpen": false, 81 | "C_Cpp.errorSquiggles": "disabled", 82 | "cSpell.words": [ 83 | "Customizability", 84 | "datastrucutre" 85 | ] 86 | } -------------------------------------------------------------------------------- /Framework/Header_Files/graph.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace std; 10 | 11 | 12 | /* 13 | * 14 | * Defines an edge in the graph 15 | * 16 | */ 17 | struct Edge 18 | { 19 | string to; 20 | double exPrice; 21 | double fee; 22 | string exchange; 23 | string bidOrAsk; 24 | }; 25 | 26 | 27 | /* 28 | * 29 | * Adjacency list graph custom data 30 | * structure that defines trade relations 31 | * across all exchanges that the arbitrage bot 32 | * pulls active ticker data from. 33 | * 34 | */ 35 | class Graph 36 | { 37 | private: 38 | int m_graphEdges = 0; 39 | 40 | public: 41 | Graph() {} 42 | unordered_map > adjacencyList; 43 | 44 | // Expand the graph by adding an edge 45 | // - Usually only done during the 1st set of GET requests 46 | void addEdge(string from, string to, double fee, string exchange) 47 | { 48 | adjacencyList[from].push_back({to, 0.0, log(1-fee), exchange, ""}); 49 | adjacencyList[to].push_back({from, 0.0, log(1-fee), exchange, ""}); 50 | m_graphEdges += 2; 51 | } 52 | 53 | // Expand the graph by adding an edge 54 | // - Done when an orderbook is searched but it comes up empty 55 | // thus the edge is illiquid and should not be searched in the 56 | void deleteEdge(string from, string to, string exchange) 57 | { 58 | vector &edges = adjacencyList[from]; 59 | for (int i = 0; i < edges.size(); i++) 60 | { 61 | if ((edges[i].to == to) && (edges[i].exchange == exchange)) 62 | { 63 | edges.erase(edges.begin() + i); 64 | if (edges.size() == 0) 65 | adjacencyList.erase(from); 66 | break; 67 | } 68 | } 69 | edges = adjacencyList[to]; 70 | for (int i = 0; i < edges.size(); i++) 71 | { 72 | if ((edges[i].to == from) && (edges[i].exchange == exchange)) 73 | { 74 | edges.erase(edges.begin() + i); 75 | if (edges.size() == 0) 76 | adjacencyList.erase(to); 77 | break; 78 | } 79 | } 80 | } 81 | 82 | 83 | // Update an edge with new ticker prices 84 | // - Completed each time new ticker data is obtained 85 | void updateEdge(string from, string to, double bidPrice, double askPrice, string exchange) 86 | { 87 | for (Edge& edge : adjacencyList[from]) 88 | { 89 | if ((edge.to == to) && (edge.exchange == exchange)) 90 | { 91 | edge.exPrice = log(bidPrice); 92 | edge.bidOrAsk = "bid"; 93 | break; 94 | } 95 | } 96 | for (Edge& edge : adjacencyList[to]) 97 | { 98 | if ((edge.to == from) && (edge.exchange == exchange)) 99 | { 100 | edge.exPrice = log(1) - log(askPrice); 101 | edge.bidOrAsk = "ask"; 102 | break; 103 | } 104 | } 105 | } 106 | 107 | int getVertexCount() 108 | { 109 | return adjacencyList.size(); 110 | } 111 | 112 | int getEdgeCount() 113 | { 114 | return m_graphEdges; 115 | } 116 | 117 | 118 | // Prints the all data contained within the graph 119 | void printGraph() 120 | { 121 | cout << "\nGraph Print in form of Adjaceny List: " << endl; 122 | for (auto it = adjacencyList.begin(); it != adjacencyList.end(); it++) 123 | { 124 | string vertex = it->first; 125 | vector edges = it->second; 126 | cout << vertex << ": "; 127 | for (Edge edge : edges) 128 | { 129 | cout << "(" << edge.to << ", " << edge.exPrice << ", " << edge.fee << ", " << edge.exchange << ") "; 130 | } 131 | cout << endl; 132 | } 133 | } 134 | 135 | 136 | // Prints all data associated with a particular edge in the graph 137 | void printEdge(string from, string to, string exchange) { 138 | vector edges = adjacencyList[from]; 139 | 140 | // Loop through the edges to find the edge that ends at the specified vertex 141 | for (Edge edge : edges) { 142 | if (edge.to == to && exchange == edge.exchange) { 143 | // Print the edge information 144 | cout << "Edge from " << from << " to " << to << " with exchange " << edge.exchange << ": "; 145 | cout << "ExPrice = " << edge.exPrice << ", Fee = " << edge.fee << endl; 146 | return; 147 | } 148 | } 149 | 150 | // If the loop finishes without finding the edge, print an error message 151 | cout << "Error: no edge found from " << from << " to " << to << endl; 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /Framework/Symbol_Data_Files/request_coingecko_api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import sys 3 | # TODO: consider changing order of the code for readability 4 | 5 | 6 | """ 7 | " 8 | " Method parses coin information from json endpoint data 9 | " if the trading volume is higher than the user defined minimum 10 | " mark the coin as viable for trading 11 | " 12 | """ 13 | def parse_coin_from_json(json_data, viable_coins, min_vol): 14 | # each entry in the json data is a hashmap of information 15 | # about a coin 16 | for coin_data in json_data: 17 | try: 18 | vol = float(coin_data["total_volume"]) 19 | if vol < min_vol: 20 | continue 21 | 22 | # append the coin to the viable_coins list if the min 23 | # volume condition is satisfied 24 | viable_coins.append(coin_data["symbol"].upper()) 25 | except: 26 | continue 27 | 28 | return viable_coins 29 | 30 | 31 | """ 32 | " 33 | " Use requests to get crypto data from 34 | " the coin gecko endpoint and parse it with 35 | " the json library 36 | " 37 | """ 38 | def extract_coin_info(page_URL): 39 | request_res = requests.get(page_URL) 40 | if request_res.status_code == 200: 41 | # Convert the response to JSON format 42 | json_coins_data = request_res.json() 43 | return json_coins_data 44 | 45 | 46 | """ 47 | " 48 | " Method to retrieve general crypto data 49 | " via the coingecko free public API 50 | " data include symbol, volume, price change, etc. 51 | " 52 | """ 53 | def scrape_pages(viable_coin_count, min_vol): 54 | viable_coins = [] 55 | iters, last_count = 1, -1 56 | 57 | # continue to parse more coins while they are more coins to request via api 58 | # and the last iteration of the loop added coins 59 | while (len(viable_coins) < viable_coin_count and last_count != len(viable_coins)): 60 | last_count = len(viable_coins) 61 | page_URL = f"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250&page={iters}&sparkline=false" 62 | json_coins_data = extract_coin_info(page_URL) 63 | parse_coin_from_json(json_coins_data, viable_coins, min_vol) 64 | # print(len(viable_coins)) 65 | 66 | iters += 1 67 | 68 | # truncate out extra coins if they were parsed and added 69 | return viable_coins[:viable_coin_count-1] 70 | 71 | """ 72 | " Method to print all viable coins to a txt file 73 | " a set can be created from file to parse out 74 | " nonviable coins from API Ticker Data 75 | """ 76 | def create_symbol_output_file(viable_coins): 77 | with open("Viable_Trading_Pairs.txt", "w") as f_out: 78 | for i, from_symbol in enumerate(viable_coins): 79 | if from_symbol == "" or "." in from_symbol: 80 | continue 81 | for j, to_symbol in enumerate(viable_coins[i+1:]): 82 | if to_symbol == "" or "." in to_symbol: 83 | continue 84 | str_forward_print = f'\"{from_symbol}/{to_symbol}\"\n' 85 | f_out.write(str_forward_print) 86 | str_backward_print = f'\"{to_symbol}/{from_symbol}\"\n' 87 | f_out.write(str_backward_print) 88 | 89 | 90 | """ 91 | " 92 | " Method for parsing coin volume cli 93 | " argument and user input where needed 94 | " 95 | """ 96 | def cli_vol_parse(): 97 | min_coin_vol = float(sys.argv[2]) 98 | if min_coin_vol <= 0: 99 | print("Please enter a number a real number for minimum 24hr trading volume") 100 | print("CLI Format: python3 scrape_coinmarketcap.py ") 101 | return True 102 | 103 | if min_coin_vol > 400000000: 104 | print("More than 400m in trading volume as it limits your arbitrage opportunities to less than 100 coins") 105 | user_res = input("Would you like to continue? (Y/N) ") 106 | if user_res == "N" or user_res == "n": 107 | return True 108 | 109 | 110 | """ 111 | " 112 | " Method for parsing amount of coins cli 113 | " argument and user input where needed 114 | " 115 | """ 116 | def cli_coins_parse(): 117 | number_coins_req = int(sys.argv[1]) 118 | if number_coins_req <= 0: 119 | print("Please enter a number a real number for amount of coins") 120 | print("CLI Format: python3 scrape_coinmarketcap.py ") 121 | return True 122 | 123 | if number_coins_req > 250: 124 | print("More than 250 coins is not recommend as it may fill the graph with smaller, harder to trade coins") 125 | user_res = input("Would you like to continue? (Y/N) ") 126 | if user_res == "N" or user_res == "n": 127 | return True 128 | 129 | 130 | """ 131 | " 132 | " Method for parsing CLI and directing 133 | " user input where needed 134 | " 135 | """ 136 | def cli_parse(): 137 | if (len(sys.argv) != 3): 138 | print("CLI Format: python3 scrape_coinmarketcap.py ") 139 | return True 140 | 141 | # parse the first CLI argument which is # number of coins 142 | if cli_coins_parse(): 143 | return True 144 | 145 | # parse the second CLI argument which is minimum 24hr trading volume 146 | if cli_vol_parse(): 147 | return True 148 | 149 | 150 | """ 151 | " 152 | " Drive method for parsing CLI 153 | " and calling escarping and formatting 154 | " 155 | """ 156 | def main(): 157 | number_coins_req = int(sys.argv[1]) 158 | min_coin_vol = float(sys.argv[2]) 159 | 160 | # scrape all pages 161 | viable_coins = scrape_pages(number_coins_req, min_coin_vol) 162 | percent_requested = len(viable_coins) / number_coins_req 163 | if percent_requested < 0.50: 164 | print(f"Only {len(viable_coins)} coins were returned, only {percent_requested * 100}% of requested") 165 | user_res = input("Would you like to continue? (Y/N) ") 166 | if user_res == "N" or user_res == "n": 167 | return 168 | 169 | create_symbol_output_file(viable_coins) 170 | return 171 | 172 | if __name__ == "__main__": 173 | main() -------------------------------------------------------------------------------- /Framework/Header_Files/amount_optimization.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "graph.hpp" 4 | #include "arbitrage_finder.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | /* 11 | * 12 | * Algorithm for searching through most recent orderbook information 13 | * and determining profitability from amount of active liquidity with 14 | * consideration for prices beyond bid and ask 15 | * 16 | */ 17 | double orderBookProfit(vector &arbPath, vector>> &orderBookData, 18 | vector> &orderBookSides, double startCapital, int nDepth, 19 | unordered_map &feeMap) 20 | { 21 | double activeTradeAmt = startCapital; 22 | for (int i = 0; i < arbPath.size(); i++) 23 | { 24 | TrackProfit currProfitItem = arbPath[i]; 25 | double conversionAmt = 0; 26 | 27 | // In a must sell situation with the trading pair 28 | // if we are selling we must look at the bid side of the table 29 | if (currProfitItem.from == orderBookSides[i][0]) 30 | { 31 | // looking to sell x units of a coin to a buyer who wants to buy y units of a the same coin 32 | // the buyer in this situation is willing to give me another coin as payment 33 | 34 | // iterating over buy side book data 35 | for (int j = 0; j < nDepth; j++) 36 | { 37 | double unitsCouldBuy = activeTradeAmt; 38 | double unitsAvail = orderBookData[i][1][j]; 39 | double tradeSize = unitsCouldBuy - unitsAvail; 40 | 41 | // more units available on current bid order than I could buy 42 | if (tradeSize <= 0) 43 | { 44 | conversionAmt += unitsCouldBuy * exp(orderBookData[i][0][j]); 45 | activeTradeAmt = 0; 46 | break; 47 | } 48 | 49 | // less units available on the current bid order than I could buy 50 | else 51 | { 52 | conversionAmt += unitsAvail * exp(orderBookData[i][0][j]); 53 | activeTradeAmt -= unitsAvail; 54 | } 55 | 56 | } 57 | if (activeTradeAmt != 0) 58 | { 59 | return -1.0; 60 | } 61 | } 62 | 63 | // In a must buy situation with the trading pair 64 | // if we are buying we must look at the bid side of the table 65 | else 66 | { 67 | // looking to buy x units of a coin from a buyer who wants to sell y units of a the same coin 68 | // the seller in this situation is willing to give me there coin through payment of another coin 69 | 70 | // iterating over sell side book data 71 | for (int j = 0; j < nDepth; j++) 72 | { 73 | 74 | double unitsCouldBuy = activeTradeAmt * (1/exp(orderBookData[i][2][j])); 75 | double unitsAvail = orderBookData[i][3][j]; 76 | double tradeSize = unitsCouldBuy - unitsAvail; 77 | 78 | if (tradeSize <= 0.0) 79 | { 80 | conversionAmt += unitsCouldBuy; 81 | activeTradeAmt = 0; 82 | break; 83 | } 84 | 85 | else 86 | { 87 | conversionAmt += unitsAvail; 88 | activeTradeAmt -= unitsAvail * exp(orderBookData[i][2][j]); 89 | } 90 | } 91 | if (activeTradeAmt != 0) 92 | { 93 | return -1.0; 94 | } 95 | } 96 | activeTradeAmt = (conversionAmt * (1-feeMap[arbPath[i].exchange])); 97 | } 98 | return activeTradeAmt; 99 | } 100 | 101 | 102 | /* 103 | * 104 | * method to check if any of the order books are 105 | * empty implying a closed order book 106 | * if they are closed, delete that from the graph 107 | */ 108 | void CheckClosedOrderBooks(vector>> &orderBookData, Graph &g, vector &arbPath) 109 | { 110 | int d1 = orderBookData.size(); 111 | for (int i = 0; i < d1; i++) 112 | { 113 | if ((orderBookData[i][1][0] == 0.0) || (orderBookData[i][3][0] == 0.0)) 114 | { 115 | g.deleteEdge(arbPath[i].from, arbPath[i].to, arbPath[i].exchange); 116 | } 117 | } 118 | } 119 | 120 | 121 | int DetermineMaxOrderBookBuckets(vector>> &orderBookData) 122 | { 123 | int maxBuckets = INT_MAX, orderBookOrders; 124 | int d1 = orderBookData.size(); 125 | for (int i = 0; i < d1; i++) 126 | { 127 | orderBookOrders = orderBookData[i][0].size(); 128 | maxBuckets = min(maxBuckets, orderBookOrders); 129 | } 130 | return maxBuckets; 131 | } 132 | 133 | 134 | /* 135 | * 136 | * Control flow method for the user choice of 137 | * only executing the main arbitrage method. 138 | * - Bare bones option that has lightweight logging 139 | * 140 | */ 141 | double amountOptControlMain(Graph &g, vector &arbPath, int nDepth, unordered_map &feeMap, double minAmt) 142 | { 143 | // Need to obtain orderbook info for each trade in the arbitrage path 144 | int d1 = arbPath.size(); 145 | // Each orderbook will provide bid prices, bid sizes, ask prices, ask sizes 146 | int d2 = 4; 147 | // Amount of orderbook orders to track 148 | int d3 = nDepth; 149 | vector>> orderBookData(d1,vector>(d2,vector(d3))); 150 | vector> orderBookSides(d1, vector(2)); 151 | 152 | pullAllOrderBook(arbPath, orderBookData, orderBookSides, nDepth); 153 | CheckClosedOrderBooks(orderBookData, g, arbPath); 154 | 155 | int maxBuckets = DetermineMaxOrderBookBuckets(orderBookData); 156 | double returnOnInvestment, capAfterTrades, maxProfitability=0.0; 157 | 158 | vector startCaps {minAmt, minAmt * 1.25, minAmt * 1.5, minAmt * 1.75, minAmt * 2.0, minAmt * 2.5, minAmt * 3.0, minAmt * 4, minAmt * 5.0, minAmt * 10}; 159 | 160 | // Check possible starting capital options for profitability of different levels of a starting trade 161 | for (double startCap : startCaps) 162 | { 163 | capAfterTrades = orderBookProfit(arbPath, orderBookData, orderBookSides, startCap, maxBuckets, feeMap); 164 | returnOnInvestment = (capAfterTrades/startCap - 1); 165 | if (capAfterTrades == -1) 166 | break; 167 | maxProfitability = max(maxProfitability, returnOnInvestment); 168 | } 169 | return maxProfitability * 100; 170 | } 171 | 172 | 173 | /* 174 | * 175 | * Control flow method for the user choice of 176 | * benchmarking code 177 | * - In addition to performing normal ideal amount, 178 | * the code records time it takes to do operations 179 | * 180 | */ 181 | double amountOptControlTime(Graph &g, vector &arbPath, int nDepth, unordered_map &feeMap, double minAmt, vector ×) 182 | { 183 | /// Need to obtain orderbook info for each trade in the arbitrage path 184 | int d1 = arbPath.size(); 185 | // Each orderbook will provide bid prices, bid sizes, ask prices, ask sizes 186 | int d2 = 4; 187 | // Amount of orderbook orders to track 188 | int d3 = nDepth; 189 | vector>> orderBookData(d1,vector>(d2,vector(d3))); 190 | vector> orderBookSides(d1, vector(2)); 191 | 192 | auto start = high_resolution_clock::now(); 193 | pullAllOrderBook(arbPath, orderBookData, orderBookSides, nDepth); 194 | auto end = high_resolution_clock::now(); 195 | auto duration = duration_cast(end - start); 196 | times[2] = (duration.count()); 197 | 198 | start = high_resolution_clock::now(); 199 | 200 | CheckClosedOrderBooks(orderBookData, g, arbPath); 201 | 202 | int maxBuckets = DetermineMaxOrderBookBuckets(orderBookData); 203 | double returnOnInvestment, capAfterTrades, maxProfitability=0.0; 204 | vector startCaps {minAmt, minAmt * 1.25, minAmt * 1.5, minAmt * 1.75, minAmt * 2.0, minAmt * 2.5, minAmt * 3.0, minAmt * 4, minAmt * 5.0, minAmt * 10}; 205 | 206 | for (double startCap : startCaps) 207 | { 208 | capAfterTrades = orderBookProfit(arbPath, orderBookData, orderBookSides, startCap, maxBuckets, feeMap); 209 | returnOnInvestment = (capAfterTrades/startCap - 1); 210 | if (capAfterTrades == -1) 211 | break; 212 | maxProfitability = max(maxProfitability, returnOnInvestment); 213 | } 214 | 215 | end = high_resolution_clock::now(); 216 | duration = duration_cast(end - start); 217 | times[3] = (duration.count() / 1e-3); 218 | return maxProfitability; 219 | } 220 | 221 | 222 | /* 223 | * 224 | * Control flow method for the user choice of 225 | * debugging code 226 | * - Has all bare bones features of ideal algo 227 | * - Additionally has tons of print out information 228 | * 229 | */ 230 | void amountOptControlDebug(Graph &g, vector &arbPath, int nDepth, unordered_map &feeMap, double minAmt) 231 | { 232 | // Need to obtain orderbook info for each trade in the arbitrage path 233 | int d1 = arbPath.size(); 234 | // Each orderbook will provide bid prices, bid sizes, ask prices, ask sizes 235 | int d2 = 4; 236 | // Amount of orderbook orders to track 237 | int d3 = nDepth; 238 | vector>> orderBookData(d1,vector>(d2,vector(d3))); 239 | vector> orderBookSides(d1, vector(2)); 240 | 241 | cout << endl; 242 | pullAllOrderBook(arbPath, orderBookData, orderBookSides, nDepth); 243 | for (int i = 0; i < d1; i++) 244 | { 245 | g.printEdge(arbPath[i].from, arbPath[i].to, arbPath[i].exchange); 246 | cout << "Amounts Unit: " << orderBookSides[i][0] << " Price Unit: " << orderBookSides[i][1] << endl; 247 | cout << "Bids: "; 248 | PrintVector(orderBookData[i][0]); 249 | cout << "Bids size: "; 250 | PrintVector(orderBookData[i][1]); 251 | cout << "Asks: "; 252 | PrintVector(orderBookData[i][2]); 253 | cout << "Asks size: "; 254 | PrintVector(orderBookData[i][3]); 255 | cout << endl; 256 | } 257 | 258 | CheckClosedOrderBooks(orderBookData, g, arbPath); 259 | 260 | int maxBuckets = DetermineMaxOrderBookBuckets(orderBookData); 261 | double returnOnInvestment, capAfterTrades, maxProfitability=0.0; 262 | vector viableAmts; 263 | vector startCaps {minAmt, minAmt * 1.25, minAmt * 1.5, minAmt * 1.75, minAmt * 2.0, minAmt * 2.5, minAmt * 3.0, minAmt * 4, minAmt * 5.0, minAmt * 10}; 264 | 265 | // Check possible starting capital options for profitability of different levels of a starting trade 266 | for (double startCap : startCaps) 267 | { 268 | capAfterTrades = orderBookProfit(arbPath, orderBookData, orderBookSides, startCap, maxBuckets, feeMap); 269 | if (capAfterTrades == -1) 270 | break; 271 | returnOnInvestment = (capAfterTrades/startCap - 1); 272 | if (returnOnInvestment > 0){ 273 | viableAmts.push_back(startCap); 274 | } 275 | maxProfitability = max(maxProfitability, returnOnInvestment); 276 | 277 | cout << "Start capital: " << startCap << " " << arbPath[0].from; 278 | cout << ", Profitability: " << capAfterTrades - startCap << " "; 279 | cout << arbPath[0].from << ", currROI: " << WeightConversion(returnOnInvestment)-1 << "%" << endl; 280 | } 281 | 282 | // Print the amounts that produce positive profit 283 | if (viableAmts.size() > 0) 284 | { 285 | cout << "Viable Amounts: "; 286 | for (int i=0; i 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include "Header_Files/graph.hpp" 19 | #include "Header_Files/arbitrage_finder.hpp" 20 | #include "Header_Files/exchange_api_pull.hpp" 21 | #include "Header_Files/combinations.hpp" 22 | #include "Header_Files/amount_optimization.hpp" 23 | 24 | using namespace std; 25 | 26 | /* 27 | * 28 | * Struct for holding all custom user settings 29 | * that are generated from shell_driver.sh 30 | * 31 | */ 32 | struct UserInput 33 | { 34 | int pathLen; 35 | string startCoin; 36 | double tradeAmt; 37 | string exchangeRemove; 38 | double lowerBound; 39 | int coinReq; 40 | double volReq; 41 | bool debugMode; 42 | bool timeMode; 43 | int orderBookDepth; 44 | }; 45 | 46 | 47 | /* 48 | * 49 | * Print method to print information about parsed 50 | * user info 51 | * 52 | */ 53 | void printUserInput(UserInput userInput) 54 | { 55 | cout << "Arb Path Length: " << userInput.pathLen << endl; 56 | cout << "Start Crypto: " << userInput.startCoin << endl; 57 | cout << "Min Trade Amount: " << userInput.tradeAmt << endl; 58 | cout << "Removed exchanges: " << userInput.exchangeRemove << endl; 59 | cout << "Lower bound profitability: " << userInput.lowerBound << endl; 60 | cout << "Coin amount requested: " << userInput.coinReq << endl; 61 | cout << "Volume requested: " << userInput.volReq << endl; 62 | cout << "Debug mode: " << userInput.debugMode << endl; 63 | cout << "Time mode: " << userInput.timeMode << endl; 64 | cout << "Order book depth: " << userInput.orderBookDepth << endl; 65 | } 66 | 67 | 68 | /* 69 | * 70 | * Parse user settings from a user_settings.txt file 71 | * and fill a UserInput struct 72 | * 73 | */ 74 | void parseUserSettings(UserInput &userInput) 75 | { 76 | ifstream file("../../user_settings.txt"); 77 | unordered_map values; 78 | string line; 79 | while (getline(file, line)) { 80 | istringstream iss(line); 81 | string key, value; 82 | getline(iss, key, '='); 83 | getline(iss, value); 84 | values[key] = value; 85 | } 86 | userInput.pathLen = stoi(values["pathLen"]); 87 | userInput.startCoin = values["startCoin"]; 88 | userInput.tradeAmt = stod(values["tradeAmt"]); 89 | userInput.exchangeRemove = values["exchangeRemove"]; 90 | userInput.lowerBound = stod(values["lowerBound"]); 91 | userInput.coinReq = stoi(values["coinReq"]); 92 | userInput.volReq = stod(values["volReq"]); 93 | if (values["debugMode"].at(0) == '1') 94 | userInput.debugMode = true; 95 | else 96 | userInput.debugMode = false; 97 | if (values["timeMode"].at(0) == '1') 98 | userInput.timeMode = true; 99 | else 100 | userInput.timeMode = false; 101 | userInput.orderBookDepth = stoi(values["orderBookDepth"]); 102 | } 103 | 104 | 105 | unordered_set removeExchanges(string removeExchanges) 106 | { 107 | unordered_set exchangeVec; 108 | 109 | // Use a loop to find each delimiter and extract the substring between the delimiters 110 | size_t startPos = 0; 111 | size_t delimPos = removeExchanges.find('/'); 112 | while (delimPos != string::npos) { 113 | exchangeVec.insert(removeExchanges.substr(startPos, delimPos - startPos)); 114 | startPos = delimPos + 1; 115 | delimPos = removeExchanges.find('/', startPos); 116 | } 117 | 118 | // Add the final substring to the vector 119 | exchangeVec.insert(removeExchanges.substr(startPos)); 120 | 121 | // Print the extracted elements 122 | return exchangeVec; 123 | } 124 | 125 | 126 | /* 127 | * 128 | * main arbitrage running branch 129 | * - Facilitates all features of the framework 130 | * while minimizing the log information 131 | * 132 | */ 133 | void mainArbOnly(UserInput &userInput, Graph &g, unordered_map> &symbolMap, 134 | unordered_set &seenSymbols, unordered_map &feeMap, 135 | unordered_set &exchangeRemove) 136 | { 137 | string startCoin = userInput.startCoin; 138 | int frameworkIterations=0, positiveArbs=0; 139 | int currIterations=0, currArbsFound=0; 140 | int optimalAmount; 141 | double profitability; 142 | vector arbPath; 143 | 144 | // Set the graph 145 | pullAllTicker(symbolMap, g, true, seenSymbols, exchangeRemove); 146 | 147 | // call resize the symbolMap to only hold viable trading pairs 148 | symbolHashMapResize(symbolMap, seenSymbols); 149 | seenSymbols.clear(); 150 | 151 | while (true){ 152 | // update the graph 153 | pullAllTicker(symbolMap, g, false, seenSymbols, exchangeRemove); 154 | 155 | // detect best arbitrage path in the graph 156 | arbPath = ArbDetect(g, startCoin, 1.0 + userInput.lowerBound, 1.10, userInput.pathLen); 157 | frameworkIterations++; currIterations++; 158 | cout << "Iteration " << frameworkIterations << ": "; 159 | if (arbPath.size() > 0) 160 | { 161 | // determine optimal trade amount through orderbook information 162 | profitability = amountOptControlMain(g, arbPath, userInput.orderBookDepth, feeMap, userInput.tradeAmt); 163 | positiveArbs++; currArbsFound++; 164 | } 165 | 166 | LogArbInfo(arbPath, feeMap, userInput.startCoin, profitability); 167 | 168 | CheckPointInfo(frameworkIterations, positiveArbs, currIterations, currArbsFound); 169 | sleep(1); 170 | } 171 | 172 | } 173 | 174 | 175 | /* 176 | * 177 | * debug arbitrage running branch 178 | * - Facilitates all features of the framework 179 | * for exactly one successful arbitrage find 180 | * - Debug mode will print important arbitrage 181 | * log information such as graph size, arbitrage 182 | * path information, orderbook parsing, and 183 | * ideal size finding 184 | */ 185 | void mainDebugMode(UserInput &userInput, Graph &g, unordered_map> &symbolMap, 186 | unordered_set &seenSymbols, unordered_map &feeMap, 187 | unordered_set &exchangeRemove) 188 | { 189 | string startCoin = userInput.startCoin; 190 | vector arbPath; 191 | printStars(); 192 | cout << "UserInput:" << endl; 193 | printUserInput(userInput); 194 | printStars(); 195 | cout << endl; 196 | // Set the graph 197 | pullAllTicker(symbolMap, g, true, seenSymbols, exchangeRemove); 198 | 199 | // call resize the symbolMap to only hold viable trading pairs 200 | symbolHashMapResize(symbolMap, seenSymbols); 201 | seenSymbols.clear(); 202 | 203 | printStars(); 204 | cout << "Graph Stats:" << endl; 205 | cout << "Number of vertices: " << g.getVertexCount() << endl; 206 | cout << "Number of edges: " << g.getEdgeCount() << endl; 207 | printStars(); 208 | cout << endl; 209 | 210 | printStars(); 211 | cout << "Performing Arb Finder from " << startCoin << endl; 212 | printStars(); 213 | 214 | int found = 0, need = 1, iterations = 1; 215 | while (found < need) 216 | { 217 | sleep(2); 218 | // update the graph 219 | pullAllTicker(symbolMap, g, false, seenSymbols, exchangeRemove); 220 | 221 | // detect best arbitrage path in the graph 222 | arbPath = ArbDetect(g, startCoin, 1.0 + userInput.lowerBound, 1.10, userInput.pathLen); 223 | if (arbPath.size() > 0) 224 | { 225 | cout << "Found Arb Path in " << iterations << " iterations" << endl; 226 | printStars(); 227 | cout << endl; 228 | cout << "Arbitrage Path" << endl; 229 | printArbInfo(arbPath, feeMap); 230 | printArbProfitability(arbPath, feeMap); 231 | printStars(); 232 | cout << endl; 233 | 234 | printStars(); 235 | cout << "Amount Optimization Debug Info" << endl; 236 | amountOptControlDebug(g, arbPath, userInput.orderBookDepth, feeMap, userInput.tradeAmt); 237 | printStars(); 238 | 239 | found++; 240 | } 241 | else 242 | { 243 | cout << "Iteration " << iterations << " found no arbitrage path" << endl; 244 | iterations += 1; 245 | } 246 | } 247 | } 248 | 249 | 250 | /* 251 | * 252 | * time arbitrage running branch 253 | * - Facilitates all features of the framework 254 | * to benchmark the time dominant operations 255 | * of the framework 256 | * - These operations include ticker data pulling, 257 | * arbitrage finding, orderbook pulling, and 258 | * ideal amount finding 259 | */ 260 | void mainTimeMode(UserInput &userInput, Graph &g, unordered_map> &symbolMap, 261 | unordered_set &seenSymbols, unordered_map &feeMap, 262 | unordered_set &exchangeRemove) 263 | { 264 | string startCoin = userInput.startCoin; 265 | vector arbPath; 266 | double profitability; 267 | 268 | // Set the graph 269 | pullAllTicker(symbolMap, g, true, seenSymbols, exchangeRemove); 270 | 271 | // call resize the symbolMap to only hold viable trading pairs 272 | symbolHashMapResize(symbolMap, seenSymbols); 273 | seenSymbols.clear(); 274 | 275 | int iterations = 1, foundPaths = 0; 276 | while (true) 277 | { 278 | vector times(4); 279 | // check point print every 280 | if (iterations % 100 == 0) 281 | { 282 | cout << iterations << " Iterations Check Point: "; 283 | cout << foundPaths << " profitable paths found" << endl; 284 | } 285 | 286 | // update the graph 287 | auto start = high_resolution_clock::now(); 288 | pullAllTicker(symbolMap, g, false, seenSymbols, exchangeRemove); 289 | auto end = high_resolution_clock::now(); 290 | auto duration = duration_cast(end - start); 291 | times[0] = (duration.count()); 292 | 293 | // detect best arbitrage path in the graph 294 | start = high_resolution_clock::now(); 295 | arbPath = ArbDetect(g, startCoin, 1.0 + userInput.lowerBound, 1.10, userInput.pathLen); 296 | end = high_resolution_clock::now(); 297 | duration = duration_cast(end - start); 298 | times[1] = (duration.count()); 299 | 300 | if (arbPath.size() > 0) 301 | { 302 | profitability = amountOptControlTime(g, arbPath, userInput.orderBookDepth, feeMap, userInput.tradeAmt, times); 303 | foundPaths++; 304 | } 305 | 306 | // print iteration information 307 | cout << "Iter " << iterations << ": "; 308 | cout << "Ticker_t=" << times[0] << " ms, "; 309 | cout << "ArbFind_t=" << times[1] << " ms, "; 310 | cout << "OrdBook_t=" << times[2] << " ms, "; 311 | cout << "OptAmt_t=" << times[3] << " ms" << endl; 312 | if (arbPath.size() > 0){ 313 | cout << "\t-"; 314 | LogArbInfo(arbPath, feeMap, userInput.startCoin, profitability); 315 | } 316 | iterations++; 317 | sleep(1); 318 | } 319 | } 320 | 321 | 322 | int main(){ 323 | UserInput userInput; 324 | parseUserSettings(userInput); 325 | 326 | unordered_set seenSymbols; 327 | Graph g; 328 | unordered_map> symbolMap = buildSymbolHashMap("../../Symbol_Data_Files/Viable_Trading_Pairs.txt"); 329 | unordered_map feeMap = buildFeeMap(); 330 | unordered_set exchangeRemove = removeExchanges(userInput.exchangeRemove); 331 | 332 | if (userInput.debugMode) 333 | mainDebugMode(userInput, g, symbolMap, seenSymbols, feeMap, exchangeRemove); 334 | else if (userInput.timeMode) 335 | mainTimeMode(userInput, g, symbolMap, seenSymbols, feeMap, exchangeRemove); 336 | else 337 | mainArbOnly(userInput, g, symbolMap, seenSymbols, feeMap, exchangeRemove); 338 | 339 | return 1; 340 | } -------------------------------------------------------------------------------- /Framework/generate_user_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | " User input parsing script to allow user customizability with the 3 | " arbitrage finding frame work 4 | " Currently Supported Customizations: 5 | " - Path length: Determines the length of arbitrage paths that 6 | " the arbitrage finding algorithm searches 7 | " 8 | " - Start Currency: Determines the start and end currency of each 9 | " arbitrage path. Each arbitrage path is a cycle 10 | " 11 | " - Remove Supported Exchanges: Remove any exchange from data pulling and parsing 12 | " that user does not want. Can greatly reduce 13 | " API latency based on user location 14 | " 15 | " - Lower Bound Profitability: Place a lower bound for profitability for 16 | " paths. Removes extra work for paths that are 17 | " profitable at a minute factor (Ex: 0.001%) 18 | " 19 | " - Amount of Coins to Consider: Determines the number of coins the framework will 20 | " consider and compute on. The number of coins is 21 | " equivalent to the top x coins on coingecko.com 22 | " 23 | " - Minimum 24hr Coin Trading Volume: User select minimum 24hr trading volume 24 | " for all coins selected above 25 | " 26 | """ 27 | 28 | 29 | import sys 30 | import os 31 | import requests 32 | 33 | 34 | """ 35 | " 36 | " Get desired arbitrage path length from user 37 | " 38 | """ 39 | def path_len_selection(): 40 | path_len = -1 41 | while (path_len == -1): 42 | try: 43 | in_val = int(input("Please select a desired arbitrage path length from [2,3,4,5]: ")) 44 | if in_val < 2 or in_val > 5: 45 | raise Exception 46 | path_len = in_val 47 | except ValueError as verr: 48 | print("Bad input. Try again") 49 | except Exception as ex: 50 | print("Bad input. Try again") 51 | 52 | print("") 53 | return path_len 54 | 55 | 56 | """ 57 | " 58 | " Get arbitrage path start and end coin from user 59 | " 60 | """ 61 | def start_currency_selection(): 62 | start_coin = None 63 | valid_coins = ["USDT", "BTC", "ETH", "BNB"] 64 | while (start_coin == None): 65 | try: 66 | print("Please select a desired start coin for all arbitrage paths") 67 | in_coin = input("(USDT, BTC, ETH, BNB) are supported: ").upper() 68 | if in_coin not in valid_coins: 69 | raise Exception 70 | start_coin = in_coin 71 | except: 72 | print("Bad input / Non Valid Start Coin") 73 | 74 | print("") 75 | return start_coin 76 | 77 | 78 | """ 79 | " 80 | " Method to determine minimum trade size for a given coin 81 | " As of April 1st, 2023, all exchanges supported by this framework 82 | " have a minimum trade size that is equal to or less than 10 USDT 83 | " 84 | """ 85 | def trading_amount_selection(start_coin): 86 | # min trade size for USDT 87 | min_trade_size = 20 88 | 89 | # determine min trade size if not in USDT 90 | while (start_coin != "USDT"): 91 | try: 92 | print("Requesting Active Trade Prices") 93 | web_req = requests.get("https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=25&page={iters}&sparkline=false") 94 | json_req = web_req.json() 95 | for line in json_req: 96 | if line["symbol"].upper() == start_coin: 97 | USDT_coin_ex_rate = line["current_price"] 98 | min_trade_size = min_trade_size / USDT_coin_ex_rate 99 | start_coin = "USDT" 100 | except: 101 | continue 102 | 103 | return min_trade_size 104 | 105 | 106 | """ 107 | " 108 | " Method to format exchanges the user wishes to remove 109 | " from API data pulling 110 | " 111 | """ 112 | def format_exchanges(to_remove): 113 | to_remove_str = "" 114 | for i in range(len(to_remove)-1): 115 | to_remove_str += to_remove[i] + "/" 116 | 117 | to_remove_str += to_remove[len(to_remove)-1] 118 | return to_remove_str 119 | 120 | 121 | """ 122 | " 123 | " Get exchanges that the user wants to blacklist 124 | " from active pulling 125 | " 126 | """ 127 | def remove_exchanges(): 128 | # current 6 supported exchanges 129 | exchanges = ["binance", "kucoin", "bitmart", "bitget", "gateio", "huobi"] 130 | to_remove = [] 131 | 132 | print("Currently supported exchanges: ") 133 | print("{Binance, KuCoin, BitMart, Bitget, Gateio, Huobi}") 134 | print("Enter any exchanges you wish to remove (comma separated)") 135 | ex_to_remove = input("If none, enter NA ").lower() 136 | ex_to_remove = ex_to_remove.replace(" ", "").split(",") 137 | 138 | for exchange in ex_to_remove: 139 | if exchange in exchanges: 140 | to_remove.append(exchange) 141 | 142 | if to_remove: 143 | to_remove = format_exchanges(to_remove) 144 | else: 145 | print("") 146 | return "None" 147 | 148 | print("") 149 | return to_remove 150 | 151 | 152 | """ 153 | " 154 | " Get the lower bound for a profitable arbitrage path 155 | " user expresses as a percentage 156 | " 157 | """ 158 | def lower_bound_selection(): 159 | lower_bound = -1.0 160 | print("Active arbitrage paths can be as little as 0.001% profitable and extremely profitable such as 10%") 161 | print("Select a percentage such that a path is not deemed as profitable unless it above that lower bound") 162 | while (lower_bound == -1.0): 163 | try: 164 | input_bound = float(input("What is your lower bound %: ")) / 100 165 | if input_bound < 0.0: 166 | raise ValueError 167 | lower_bound = input_bound 168 | except ValueError as verr: 169 | print("Bad input. Try again") 170 | except Exception as ex: 171 | print("Bad input. Try again") 172 | 173 | print("") 174 | return lower_bound 175 | 176 | 177 | """ 178 | " 179 | " Get the minimum cryptocurrencies a user wants to consider 180 | " the cryptocurrencies are retrieved from coingecko.com 181 | " 182 | """ 183 | def min_coins_selection(): 184 | coin_req = -1 185 | print("Please request a number of coins you wish for this framework to consider") 186 | print("Your number request is sent to coingecko.com API and top X coins will be assessed") 187 | while (coin_req == -1): 188 | try: 189 | in_coin_req = int(input("Number of coins (minimum is 25): ")) 190 | if in_coin_req < 25: 191 | raise ValueError 192 | if in_coin_req > 250: 193 | print("More than 250 coins is not recommend as it may fill the framework with smaller, harder to trade coins") 194 | user_res = input("Would you like to continue? (Y/N) ") 195 | if user_res.lower != ("Y"): 196 | continue 197 | coin_req = in_coin_req 198 | 199 | except ValueError as verr: 200 | print("Bad input. Try again") 201 | except Exception as ex: 202 | print("Bad input. Try again") 203 | 204 | print("") 205 | return coin_req 206 | 207 | 208 | """" 209 | " 210 | " Get the 24hr minimum volume that each cryptocurrency must 211 | " have to be deemed as viable for trading 212 | " 213 | """ 214 | def min_vol_selection(): 215 | trade_vol = -1.0 216 | print("Please request a 24hr trade volume for each coin you wish for this framework to consider") 217 | print("Your number request is sent to coingecko.com API and only coins at or above that volume will be considered") 218 | 219 | while(trade_vol == -1.0): 220 | try: 221 | in_vol = float(input("24hr Trading Volume in USDT (minimum is 0.0 USDT): ")) 222 | 223 | if in_vol > 400000000: 224 | print("More than 400m in trading volume as it limits your arbitrage opportunities to less than 100 coins") 225 | user_res = input("Would you like to continue? (Y/N) ") 226 | if user_res.lower != ("Y"): 227 | continue 228 | trade_vol = in_vol 229 | 230 | except ValueError as verr: 231 | print("Bad input. Try again") 232 | except Exception as ex: 233 | print("Bad input. Try again") 234 | 235 | print("") 236 | return trade_vol 237 | 238 | 239 | """" 240 | " 241 | " Get desired orderbook depth to search when determining 242 | " realistic path profitability 243 | " 244 | """ 245 | def order_book_depth_selection(): 246 | order_book_depth = 0 247 | print("This framework retrieves most up to date orderbook information (active market orders)") 248 | print("before terming a series of trades as a profitable arbitrage") 249 | 250 | while (order_book_depth == 0): 251 | try: 252 | in_n_depth = int(input("How many active orders would you like to consider at any given time? (0, 250]: ")) 253 | if in_n_depth < 5: 254 | print(f'{in_n_depth} may be too small with order books of lower liquidity') 255 | in_res = input("Are you sure you want to proceed? (Y/N): ").lower() 256 | if in_res == "y": 257 | order_book_depth = in_n_depth 258 | else: 259 | raise ValueError 260 | if in_n_depth > 250: 261 | print(f'{in_n_depth} is too many orders for most trading pairs. Truncating {in_n_depth} down to 250') 262 | order_book_depth = 250 263 | continue 264 | order_book_depth = in_n_depth 265 | 266 | 267 | except ValueError as verr: 268 | continue 269 | except Exception as ex: 270 | print("Bad input. Try again") 271 | 272 | print("") 273 | return order_book_depth 274 | 275 | 276 | """" 277 | " 278 | " Get debug/test mode activation from user 279 | " 280 | """ 281 | def debug_time_mode(): 282 | print("This framework is presented as easy to use, modify, and adapt") 283 | print("For this reason, there are two modes outside of normal arbitrage find mode") 284 | print(" - Debug Mode: Print all important information about a single framework iteration to learn how it works") 285 | print(" - Time Mode: Used to benchmark speed of the framework, will provide a time breakdown per iteration") 286 | 287 | debug_mode = time_mode = "0" 288 | end_condition = False 289 | while (not end_condition): 290 | try: 291 | in_include = input("Would you like to enable either of these options (Y/N): ").lower() 292 | if in_include == "n": 293 | end_condition = True 294 | elif in_include == "y": 295 | in_debug_mode = input("Would you like to enable debug mode (Y/N): ").lower() 296 | if in_debug_mode == "y": 297 | end_condition = True 298 | debug_mode = "1" 299 | break 300 | if in_debug_mode != "n": 301 | raise ValueError 302 | 303 | in_test_mode = input("Would you like to enable time mode (Y/N): ").lower() 304 | if in_test_mode == "y": 305 | end_condition = True 306 | time_mode = "1" 307 | elif in_test_mode == "n": 308 | end_condition = True 309 | else: 310 | raise ValueError 311 | 312 | else: 313 | raise ValueError 314 | except ValueError as verr: 315 | print("Bad input. Try again") 316 | except Exception as ex: 317 | print("Bad input. Try again") 318 | 319 | 320 | print("") 321 | return debug_mode, time_mode 322 | 323 | 324 | """" 325 | " 326 | " Print current user selections to console 327 | " and ask user to okay them before a user_settings.txt 328 | " is created 329 | " 330 | """ 331 | def print_to_console(selected_user_settings): 332 | for setting in selected_user_settings: 333 | if "tradeAmt" in setting: 334 | continue 335 | if "lowerBound" in setting: 336 | setting = setting.split("=") 337 | percentage = float(setting[1]) * 100 338 | print(f'{setting[0]}={percentage}%') 339 | continue 340 | if "Mode" in setting: 341 | setting = setting.replace("\n", "").split("=") 342 | t_f_str = "True" if setting[1] == "1" else "False" 343 | print(f'{setting[0]}={t_f_str}') 344 | continue 345 | print(setting, end="") 346 | 347 | green_light = input("Is all of this information correct? (Y/N): ") 348 | if green_light.lower() == "y": 349 | return True 350 | else: 351 | os.system("clear") 352 | print("Restarting user questions now") 353 | return False 354 | 355 | 356 | """" 357 | " 358 | " Generate the user setting text file 359 | " 360 | """ 361 | def generate_output_file(user_filepath, user_settings): 362 | with open(user_filepath, 'w') as f_out: 363 | for line in user_settings: 364 | f_out.write(line) 365 | 366 | 367 | """" 368 | " 369 | " Driver code to get user options 370 | " 371 | """ 372 | def main(): 373 | green_light = False 374 | user_settings = [] 375 | 376 | while (not green_light): 377 | # Invoke all user customization methods 378 | path_len = path_len_selection() 379 | start_coin = start_currency_selection() 380 | min_trading_amount = trading_amount_selection(start_coin) 381 | exchanges_remove = remove_exchanges() 382 | profit_lower_bound = lower_bound_selection() 383 | coin_req = min_coins_selection() 384 | vol_req = min_vol_selection() 385 | order_book_depth = order_book_depth_selection() 386 | debug_mode, time_mode = debug_time_mode() 387 | 388 | # Format all user customization choices 389 | path_len_str = f'pathLen={path_len}\n' 390 | start_coin_str = f'startCoin={start_coin}\n' 391 | trading_amount_str = f'tradeAmt={min_trading_amount}\n' 392 | exchanges_remove_str = f'exchangeRemove={exchanges_remove}\n' 393 | lower_bound_str = f'lowerBound={profit_lower_bound}\n' 394 | coin_req_str = f'coinReq={coin_req}\n' 395 | vol_req_str = f'volReq={vol_req}\n' 396 | debug_str = f'debugMode={debug_mode}\n' 397 | time_str = f'timeMode={time_mode}\n' 398 | order_book_depth_str = f'orderBookDepth={order_book_depth}\n' 399 | 400 | user_settings = [path_len_str, start_coin_str, trading_amount_str,\ 401 | exchanges_remove_str, lower_bound_str,\ 402 | coin_req_str, vol_req_str,\ 403 | debug_str, time_str,\ 404 | order_book_depth_str] 405 | os.system("clear") 406 | green_light = print_to_console(user_settings) 407 | 408 | # Once user approves selections, create the user_settings.txt file 409 | generate_output_file(sys.argv[1], user_settings) 410 | 411 | 412 | 413 | if __name__ == "__main__": 414 | main() -------------------------------------------------------------------------------- /Framework/Header_Files/bellmon_ford.hpp: -------------------------------------------------------------------------------- 1 | // CURRENTLY DEPRECATED AND NOT IN USE 2 | 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "graph.hpp" 14 | #include 15 | 16 | using namespace std; 17 | vector maxPath; 18 | // double maxProfit = 0; 19 | 20 | 21 | double weightConversion(double edgeWeight){ 22 | return exp(-1 * (edgeWeight / log(exp(1)))); 23 | } 24 | 25 | void printBellmonFord(unordered_map dists, string source) 26 | { 27 | cout << "\nPrint out of distance array of Bellmon Ford Algo: " << endl; 28 | for (auto access : dists) 29 | { 30 | string vertex = access.first; 31 | double shortestPath = access.second; 32 | cout << "Vertex: " << vertex << " shortest path from " << source << ": " << shortestPath << endl; 33 | } 34 | } 35 | 36 | void PrintCycle(vector cycleVec) 37 | { 38 | cout << "Arbritrage Detected!" << endl; 39 | for (int i = 0; i < cycleVec.size() - 1; i++) 40 | { 41 | cout << cycleVec[i] << " --> "; 42 | } 43 | 44 | cout << cycleVec[cycleVec.size() - 1] << endl; 45 | } 46 | 47 | 48 | /* 49 | * 50 | * Method to calculate the profitablity of a arbritage opportunity 51 | * prints to console the profitability percentage of the trade 52 | # See TODO: 53 | */ 54 | double ProfitCalc(vector cycleVec, unordered_map > adjacency_list) 55 | { 56 | // TODO: this is an inefficent search for the trades needed to calcualte arbritage profit 57 | // may change letter if my adjaceny list strucutre goes from hashmap[string, vectors] to hashmap[string, hashmap] 58 | 59 | double sourceVal = 1; 60 | string fromSymbol = cycleVec[cycleVec.size() - 1]; 61 | for (int i = cycleVec.size() - 2; i >= 0; i--) 62 | { 63 | string toSymbol = cycleVec[i]; 64 | // cout << "Working with " << fromSymbol << " and " << toSymbol << endl; 65 | for (Edge edge : adjacency_list[fromSymbol]) 66 | { 67 | if (edge.to == toSymbol) 68 | { 69 | // cout << "Trade from: " << fromSymbol << " to: " << toSymbol << endl; 70 | // cout << "-ln(symbolTradeVal) aka edge.weight: " << edge.weight << endl; 71 | // cout << "-1*(edge.weight/exp(1) aka e^{...}: " << (-1 * (edge.weight / log(exp(1)))) << endl; 72 | // cout << "Decrypted oringinal trade val: " << exp(-1 * (edge.weight / log(exp(1)))) << endl; 73 | sourceVal = sourceVal * exp(-1 * (edge.weight / log(exp(1)))); 74 | // cout << "SourceVal: " << sourceVal << endl; 75 | } 76 | } 77 | fromSymbol = toSymbol; 78 | } 79 | // Reverse iterate through cycleVec 80 | // Get the cycleVec.size() - 1 trades from my natural log to decmial formula 81 | sourceVal = (sourceVal - 1) * 100; 82 | cout << sourceVal << "% profitability" << endl; 83 | cout << endl; 84 | return sourceVal; 85 | } 86 | 87 | 88 | void printVector(const vector &vec) 89 | { 90 | for (const string &str : vec) 91 | { 92 | cout << str << ", "; 93 | } 94 | cout << endl; 95 | } 96 | 97 | 98 | void printUnorderedSet(const unordered_set &set) 99 | { 100 | for (const string &str : set) 101 | { 102 | cout << str << ", "; 103 | } 104 | cout << endl; 105 | } 106 | 107 | 108 | void printUnorderedMap(const unordered_map &map) 109 | { 110 | for (auto access : map) 111 | { 112 | cout << "From: " << access.first << ", To: " << access.second << endl; 113 | } 114 | cout << endl; 115 | } 116 | 117 | // variable types 118 | // void dfsMaxProfit(unordered_map > adjaceny_list, string source, double currProfit, 119 | // string currVertex, unordered_set seenVertexSet, vector path) 120 | // { 121 | // // base case for getting back to the source start 122 | // if ((currVertex == source)) 123 | // { 124 | // // printVector(path); 125 | // if (currProfit < maxProfit) 126 | // { 127 | // maxProfit = currProfit; 128 | // maxPath = path; 129 | // } 130 | // else if (currProfit == maxProfit) 131 | // { 132 | // maxPath = (path.size() < maxPath.size()) ? path : maxPath; 133 | // } 134 | // return; 135 | // // return currProfit; 136 | // } 137 | // if (seenVertexSet.size() >= 3) 138 | // { 139 | // return; 140 | // // return -1; 141 | // } 142 | 143 | // for (Edge edge : adjaceny_list[currVertex]) 144 | // { 145 | // cout << "Curr Max Profit: " << maxProfit << endl; 146 | // cout << "Curr Profit: " << currProfit << endl; 147 | // cout << "Curr Vertex from: " << currVertex << endl; 148 | // cout << "Curr Edge To: " << edge.to << endl; 149 | // cout << "Curr Path Vector: "; 150 | // printVector(path); 151 | // cout << "Seen set: "; 152 | // printUnorderedSet(seenVertexSet); 153 | // cout << endl; 154 | // // if the desintation of the edge has not be visited yet 155 | // if (seenVertexSet.count(edge.to) == 0) 156 | // { 157 | // path.push_back(edge.to); 158 | // seenVertexSet.insert(edge.to); 159 | // // exp(-1 * (edge.weight / log(exp(1)))); 160 | // currProfit += edge.weight; 161 | // dfsMaxProfit(adjaceny_list, source, currProfit, edge.to, seenVertexSet, path); 162 | // // double dfsProfit = dfsMaxProfit(adjaceny_list, source, currProfit, edge.to, seenVertexSet, path); 163 | // seenVertexSet.erase(edge.to); 164 | // // currProfit = (dfsProfit < currProfit) ? dfsProfit : currProfit; 165 | // path.pop_back(); 166 | // } 167 | // } 168 | 169 | // return; 170 | // // return currProfit; 171 | // } 172 | 173 | /* 174 | * 175 | * Implmenetation of Bellmon Ford Algorithm to detect Currency Arbritage opportunities 176 | * TODO: Implement more efficent or parallel versions of Bellmon Ford 177 | */ 178 | vector BellmonFord(Graph g, string source, double profitThreshold) 179 | { 180 | // const float INF = numeric_limits::max(); 181 | // const int V = g.getVertexCount(); 182 | unordered_map > adjacency_list = g.adjacency_list; 183 | // unordered_map prevVert; 184 | 185 | // // Initialize distance array 186 | // unordered_map dists; 187 | // for (auto access : adjacency_list) 188 | // { 189 | // dists[access.first] = INF; 190 | // } 191 | // dists[source] = 1; 192 | 193 | // // Do V-1 iterations of Bellmon Ford 194 | // for (int i = 0; i < V - 1; i++) 195 | // { 196 | // vector vertexStack{}; 197 | // unordered_set vertexSeenSet{}; 198 | // vertexStack.push_back(source); 199 | 200 | // while (!vertexStack.empty()) 201 | // { 202 | // string currVertex = vertexStack.back(); 203 | // vertexStack.pop_back(); 204 | // vertexSeenSet.insert(currVertex); 205 | 206 | // // traverse all the edges of currVertex 207 | // for (Edge edge : adjacency_list[currVertex]) 208 | // { 209 | // if (vertexSeenSet.count(edge.to) == 0) 210 | // { 211 | // vertexStack.push_back(edge.to); 212 | // } 213 | 214 | // double newCost = dists[currVertex] + edge.weight; 215 | // if (newCost < dists[edge.to]) 216 | // { 217 | // dists[edge.to] = newCost; 218 | // prevVert[edge.to] = currVertex; 219 | // } 220 | // } 221 | // } 222 | // } 223 | 224 | // I only want to detect a negative cycle in which the source coin is invloved 225 | /* 226 | string currVertex = source; 227 | vector vertexStack; 228 | vertexStack.push_back(source); 229 | unordered_set vertexSeenSet; 230 | double max2Profit = 1; 231 | // call recursion here 232 | for (Edge edge : adjacency_list[source]) 233 | { 234 | vector path{source, edge.to}; 235 | vertexSeenSet.insert(edge.to); 236 | double currProfit = edge.weight; 237 | // double dfsProfit = dfsMaxProfit(adjacency_list, source, currProfit, edge.to, vertexSeenSet, path); 238 | dfsMaxProfit(adjacency_list, source, currProfit, edge.to, vertexSeenSet, path); 239 | vertexSeenSet.erase(edge.to); 240 | // if (dfsProfit < max2Profit){ 241 | // max2Profit = dfsProfit; 242 | // } 243 | } 244 | cout << "maxProfit: " << maxProfit << endl; 245 | // check if any trade is profitable 246 | double maxProfitConversion = exp(-1 * (maxProfit / log(exp(1)))); 247 | cout << "MaxProfit Conversion: " << maxProfitConversion << endl; 248 | if (maxProfit >= (1 + profitThreshold)) 249 | { 250 | PrintCycle(maxPath); 251 | cout << (maxProfit - 1) * 100 << "% Profitability" << endl; 252 | } 253 | */ 254 | 255 | /* 256 | * Detect a negative cycle anywhere in the graph 257 | * V-th relax loop to check for negative cycle 258 | */ 259 | 260 | // TODO: modify such that I am only looking for negative cycles that include my selected source Coin 261 | 262 | // TODO: Try to implement this article https://www.geeksforgeeks.org/print-negative-weight-cycle-in-a-directed-graph/ 263 | // or this one https://cp-algorithms.com/graph/finding-negative-cycle-in-graph.html#implementation 264 | // I need to have a way to discover the path that has a negative cycle and calculate if it is profitable 265 | double maxProfit = 0; 266 | double currProfit = 0; 267 | int negCycles = 0; 268 | vector negCyclePath; 269 | 270 | string currVertex = source; 271 | for (Edge fristTradeEdge : adjacency_list[currVertex]){ 272 | currProfit += fristTradeEdge.weight; 273 | 274 | for (Edge secondTradeEdge : adjacency_list[fristTradeEdge.to]){ 275 | if (secondTradeEdge.to == source){ 276 | continue; 277 | } 278 | currProfit += secondTradeEdge.weight; 279 | 280 | for (Edge sourceTradeEdge : adjacency_list[secondTradeEdge.to]){ 281 | if (sourceTradeEdge.to != source){ 282 | continue; 283 | } 284 | currProfit += sourceTradeEdge.weight; 285 | if (currProfit < maxProfit){ 286 | maxProfit = currProfit; 287 | negCyclePath = {source, fristTradeEdge.to, secondTradeEdge.to, source}; 288 | } 289 | currProfit -= sourceTradeEdge.weight; 290 | break; 291 | } 292 | 293 | currProfit -= secondTradeEdge.weight; 294 | } 295 | 296 | currProfit -= fristTradeEdge.weight; 297 | } 298 | 299 | if (maxProfit < 0){ 300 | cout << endl; 301 | cout << "Arbritarge Opportunity Detected!!" << endl; 302 | double maxProfitConversion = weightConversion(maxProfit); 303 | cout << "MaxProfit in -log(x): " << maxProfit << endl; 304 | cout << "MaxProfit in x: " << maxProfitConversion << endl; 305 | cout << ((maxProfitConversion - 1) * 100) << "% profitability" << endl; 306 | PrintCycle(negCyclePath); 307 | cout << endl; 308 | } 309 | 310 | return negCyclePath; 311 | 312 | // For loop to go through all the edges of USD 313 | // For loop to go through all the edges of edges of USD 314 | // get back to USD most likely by iterating through the edges of the previous 315 | 316 | 317 | 318 | // while (!vertexStack.empty()){ 319 | // string currVertex = vertexStack.back(); 320 | // vertexStack.pop_back(); 321 | // vertexSeenSet.insert(currVertex); 322 | // for (Edge edge : adjacency_list[currVertex]) 323 | // { 324 | // if (vertexSeenSet.count(edge.to) == 0) 325 | // { 326 | // vertexStack.push_back(edge.to); 327 | // } 328 | // double newCost = dists[currVertex] + edge.weight; 329 | // if (newCost < dists[edge.to]) 330 | // { 331 | // cout << "Negative Cycle Detected!!!" << endl; 332 | // cout << "Arbritage Opportunity Detected" << endl; 333 | // } 334 | // } 335 | // } 336 | 337 | // if (C != -1) { 338 | // for (int i = 0; i < V; i++) 339 | // C = parent[C]; 340 | // // To store the cycle vertex 341 | // vector cycle; 342 | // for (int v = C;; v = parent[v]) { 343 | // cycle.push_back(v); 344 | // if (v == C 345 | // && cycle.size() > 1) 346 | // break; 347 | // } 348 | 349 | // // Reverse cycle[] 350 | // reverse(cycle.begin(), cycle.end()); 351 | 352 | // // Printing the negative cycle 353 | // for (int v : cycle) 354 | // cout << v << ' '; 355 | // cout << endl; 356 | // return; 357 | // } 358 | 359 | // while (!vertexStack.empty() && vertexStack[0] == source) 360 | // { 361 | // string currVertex = vertexStack.back(); 362 | // vertexStack.pop_back(); 363 | // vertexSeenSet.insert(currVertex); 364 | // for (Edge edge : adjacency_list[currVertex]) 365 | // { 366 | // if (vertexSeenSet.count(edge.to) == 0) 367 | // { 368 | // vertexStack.push_back(edge.to); 369 | // } 370 | 371 | // double newCost = dists[currVertex] + edge.weight; 372 | // if (newCost < dists[edge.to]) 373 | // { 374 | // short i = 0; 375 | // cout << "\nNegative Cycle Detected!!!!" << endl; 376 | // // Go through prevVert to print out the path needed to execute the trade 377 | // vector cycleVec = {edge.to, currVertex}; 378 | // unordered_set vertexSeenSet = {edge.to, currVertex}; 379 | // while (vertexSeenSet.count(prevVert[currVertex]) == 0) 380 | // { 381 | // if (i >= 5) 382 | // { 383 | // break; 384 | // } 385 | // cycleVec.push_back(prevVert[currVertex]); 386 | // currVertex = prevVert[currVertex]; 387 | // i++; 388 | // } 389 | // if (i >= 5) 390 | // { 391 | // break; 392 | // } 393 | // cycleVec.push_back(prevVert[currVertex]); 394 | // printVector(cycleVec); 395 | // // cycleVec = CorrectArbCycle(cycleVec, 1); 396 | // // double profitability = ProfitCalc(cycleVec, adjacency_list); 397 | // return; 398 | // } 399 | // } 400 | // } 401 | 402 | // printBellmonFord(dists, source); 403 | // return dists; 404 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## C++ Crypto Arbitrage Framework 2 | A **C++ dominant framework** for pulling real-time data via public endpoints from six crypto exchanges to **compute a crypto currency arbitrage** that is both mathematically profitable and a proper amount to trade to get realistic profitability. The framework can **detect bilatreal, trilateral, quadliteral, and pentalateral arbitrages** from a single set of crypto currency data. 3 | 4 |

5 | 6 | ## Need for Speed, through C++ and Parallelization 7 | Pulling active ticker data and select orderbook data via public API endpoints from all trading pairs across 6 crypto exchanges is about **2.7 MB of JSON string data per pull**. That is about **46.6 GB** of ticker and orderbook data **per day** from only 6 exchanges. This framework downloads active ticker data, parses active ticker data, performs arbitrage finding, formats the arbitrage path, downloads relevant orderbook information, and determines realistic profitability. Most of these **operations are expensive**, time consuming operations that in the end will net a path that is deemed profitable. **If the path takes too long to compute the profitability/trading opportunity may have disappeared.** 8 | 9 | For the reason that **speed is royalty** in this situation, **C++ is used on all time dominant operations and** all operations that could have benefited from embarrassingly or **data parallelism** were accommodated. On my own hardware, the **time dominant factor** of this framework **is downloading json data** from public endpoints. Over 99% of time is spent simply downloading ticker and orderbook data, which is both good and bad. Operations acting on operable data are efficient, which makes the framework suffers from outside factors that **can not be avoided without additional hardware or data costs**. 10 | 11 | ## To Use Framework 12 | Install all libraries* 13 | ```sh 14 | $ sudo apt-get install make cmake nlohmann-json3-dev curl python3 pip 15 | ``` 16 | ```sh 17 | $ pip install requests bs4 18 | ``` 19 | ```sh 20 | $ cd Framework 21 | ``` 22 | ```sh 23 | $ bash shell_driver.sh 24 | ``` 25 | 26 | 27 | \- **shell_driver.sh** will initializes the framework by building, compiling, and applying user selected options 28 | 29 | \* commands given for Linux Debian environment 30 | 31 | ## Outline 32 | 33 |

34 | 35 | The project is set up into **five** main section crypto ticker data pulling, graph building, arbitrage finding, orderbook data pulling, and trading amount determination 36 | 37 | 38 | ## Arbitrage Finding Algorithm 39 | A Crypto Currency arbitrage refers to the practice of **buying and selling** cryptocurrencies across different exchanges or markets to take advantage of price discrepancies and make a **profit**. 40 | 41 | ### Math Behind the Algorithm 42 | The mathematical formula for an arbitrage is 43 | 44 | $$\text{StartingCapital} \cdot \text{Trade1} \cdot \text{Trade2} \cdot ... \cdot \text{TradeN} > \text{StartingCapital}$$ 45 | 46 | For numerical stability with floating point operations, the formula can be converted to 47 | 48 | $$\ln(\text{StartingCapital}) + \ln(\text{Trade1}) + \ln(\text{Trade2}) + ... + \ln(\text{TradeN}) > \ln(\text{StartingCapital})$$ 49 | 50 | With trading fees included, 51 | 52 | $$\ln(\text{StartingCapital}) + \ln(\text{Trade1}) + \ln(\text{Trade1Fee}) + \ln(\text{Trade2}) + $$ 53 | 54 | $$\ln(\text{Trade2Fee}) + ... + \ln(\text{TradeN}) + \ln(\text{TradeNFee}) > \ln(\text{StartingCapital}) $$ 55 | 56 | 57 | **Every** set of **Trades** that satisfy this inequality is considered a **profitable arbitrage** by purely ticker data. However, there are many sets of trades with a **profitability** of **less than 0.1%**, which may be ideal for a high frequency traders but are not viable for an average trader 58 | 59 | Adding a lower bound profit threshold allows for a certain degree of profitability before a arbitrage path is said to be viable by an individual user, 60 | 61 | 62 | $$\ln(\text{StartingCapital}) + \ln(\text{Trade1}) + \ln(\text{Trade1Fee}) + ... + \ln(\text{TradeN}) + \ln(\text{TradeNFee}) $$ 63 | 64 | $$ \geq \ln(\text{StartingCapital}) + \ln(1 + \text{LowerThreshold})$$ 65 | 66 | - **LowerThreshold** is percentage expressed as a decimal 67 | - An **UpperThreshold** of default 200% profitability is implemented to avoid looping over outdated ticker and orderbook data 68 | 69 | 70 | ### Algorithm and Data Structures Implementation 71 | To find sets of trades that satisfy the equation above, the [arbitrage_finder.hpp](Framework/Header_Files/arbitrage_finder.hpp) implements a brute force path searching algorithm from a given source coin. As every arbitrage path starts and ends with the same coin, **each path is a cycle** within the graph data structure being used. 72 | 73 | Within this graph data structure, **each vertex** is a tradable **crypto currency** and **each edge** away from said vertex is a **trading pair** available on some exchange. There may be **multiple edges from one vertex to another** and that equates to a trading pair offered on multiple exchanges. The **end goal** of discovering an arbitrage path is to **complete the trades** in the path **as soon as possible** on relevant exchanges. To do this, a user will end up overspending the average user by placing bids at or above ask price and asks at or below bid price to create the best chance to for automatic order completion. This structure choice makes edge weights in a trading pair the ask price from the start currency to the end currency and bid price for the vice versa. 74 | 75 | 76 | ## Ideal Trading Amount Algorithm 77 | As the **arbitrage** path finding **algorithm** satisfies the mathematical formula described above based on ticker prices, the path falls prey to **discovering** paths that are profitable by ticker data, but the order book **lacks liquidity** to support a similar profitability. The path may be profitable by ticker data but is fruitless without knowing how much one should trade. 78 | 79 | The [ideal trading amount algorithm](Framework/Header_Files/amount_optimization.hpp) takes the path found by the arbitrage finding algorithm and **determines an upper bound for a trading amount**. The algorithm will peak into the top-N, N is specified by the user, active trades in the orderbook for each trade in the path. Looking into each of these active trades will allow the **algorithm** to **recognize** trading pairs with **low liquidity** on either the bid or ask side. 80 | 81 | The result of this **algorithm will determine a realistic profitability**, the corollary slippage rate from ticker profitability, and the amount of source capital the user should trade. 82 | 83 | 84 | ## Usability and Flexibility 85 | 86 | As seen in [framework usage](#to-use-framework), this framework is driven by a [shell script](Framework/shell_driver.sh) 87 | 88 | This **shell script generates a user setting file** from user input, if one is not already present. Described below are each setting. Once user settings are generated, the **arbitrage framework can be used freely** and these user settings can be updated at any time. A sample user_setting.txt file is provided [here](Framework/Symbol_Data_Files/user_settings.txt) for reference or if the [generate_user_settings.py](Framework/generate_user_settings.py) proves to cause unintended behavior or is hard to use for a user. 89 | 90 | **Parameter: `pathLen`**
91 | **Type: `Int`**
92 | Sets the arbitrage path length the [arbitrage finding algorithm](Framework/Header_Files/arbitrage_finder.hpp) will search. In practical terms, set the number of trading pairs in a given arbitrage sequence the framework is allowed to search through. Choice between length 2, 3, 4, or 5. 93 | 94 | **Parameter: `startCoin`**
95 | **Type: `String`**
96 | Sets the arbitrage path start and end value. Each arbitrage is a cycle in a graph data structure and the startCoin or source coin is the start and end of that cycle. Profitability will be determined based on the starting amount of this coin 97 | 98 | **Parameter: `tradeAmt`**
99 | **Type: `Double`**
100 | Sets the minimum trading amount value of each trade in an arbitrage path in terms of the **startCoin** currency. All of 6 supported exchanges have a minimum trading amount value for each market order, which equates to roughly 10-20 USDT. By default, the **tradeAmt** is set the value of 20 USDT in the **startCoin** currency the user selects. 101 | 102 | **Parameter: `exchangeRemove`**
103 | **Type: `list[String]`**
104 | Sets the exchanges the user wishes to remove from data pulling. It is suggested to remove Binance, if using this framework from a US IP Address. Removing exchanges will speed up all operations, but **will decrease** the amount of **possible arbitrages** and potentially **overall profitability**. 105 | 106 | **Parameter: `lowerBound`**
107 | **Type: `Double`**
108 | Sets the minimum profitability threshold for a arbitrage path to be deemed as profitable. A 0.03 lowerBound refers to a profitability of 3% before an arbitrage path is deemed as profitable. 109 | 110 | **Parameter: `coinReq`**
111 | **Type: `Int`**
112 | Sets the number of coins to request from the [CoinGecko API](https://www.coingecko.com). An API request is made to retrieve the top **coinReq** by 24hr trading volume using the [request_coingecko_api.py](Framework/Symbol_Data_Files/request_coingecko_api.py) script. 113 | 114 | **Parameter: `volReq`**
115 | **Type: `Double`**
116 | Sets the minimum 24hr trading volume to request for each coin requested via the [CoinGecko API](https://www.coingecko.com). An API request is made to retrieve the top **coinReq** by 24hr trading volume using the [request_coingecko_api.py](Framework/Symbol_Data_Files/request_coingecko_api.py) script. 117 | 118 | **Parameter: `debugMode`**
119 | **Type: `Int/Boolean`**
120 | Places the arbitrage framework into debug mode. Facilitates all features of the framework for exactly one successful arbitrage find. Debug mode will print important arbitrage log information such as graph size, arbitrage path information, orderbook parsing, and ideal size finding. 121 | 122 | **Parameter: `timeMode`**
123 | **Type: `Int/Boolean`**
124 | Places the arbitrage framework into time mode. Facilitates all features of the framework to benchmark the time dominant operations of the framework. These operations include ticker data pulling, arbitrage finding, orderbook pulling, and ideal amount finding. 125 | 126 | **Parameter: `orderBookDepth`**
127 | **Type: `Int`**
128 | Sets the depth to request from each trading pair orderbook when determining realistic profitability and price slippage in the [ideal trading amount algorithm](Framework/Header_Files/amount_optimization.hpp). The higher the depth the longer each API request will take to generate and download. It is also unlikely that orderbooks will have a large amount of active trades (250+), if it is not a extremely popular trading pair. It is also worth noting that the deeper one looks into an orderbook for potential trading liquidity the more spread one will deal with. 129 | 130 | ## Exchange Data Pulling 131 |

132 | 133 | Pinging and downloading ticker and orderbook data via public exchange endpoints takes a lot of time that cannot be avoided. Each public endpoint web request will take, between 1 and 3 seconds. To avoid a serial implementation of doing one request after another, the **requests are multithreaded** (assuming multiple threads are available) to reduce the total time to make all endpoint data requests around 3 seconds with the slowest endpoint request being the bottleneck. [API pulling code](Framework/Header_Files/exchange_api_pull.hpp) 134 | 135 | This type of exchange data pulling from public api endpoints is freely available via open-source projects such as [ccxt](https://github.com/ccxt/ccxt) and [freqtrade](https://github.com/freqtrade/freqtrade). **I choose six strong exchanges to allow myself the opportunity to learn** to pull and manipulate trading data. 136 | 137 | 138 | 139 | ## Possible Next Steps or Different Directions 140 | - Format the data into a matrix and **use k matrix multiplies** to maximize the profitability wit an entry in the result or intermediate matrix being the **argMax** of a row from matrixA and column from matrixB 141 | - **Reduces arbitrage algorithm complexity** to $O((k+1)*n^{([2.34, 3])})$, where k is the number of desired trades, for larger arbitrage paths and is quite amendable to parallelization 142 | - **Easy to calculate** but would be **harder to trace** the path from the result 143 | - Along with (k+1) matrix multiplies is the idea of tropical algebra for currency arbitrage outlined in this [paper](https://commons.lib.jmu.edu/cgi/viewcontent.cgi?article=1303&context=honors201019) that uses **more sophisticated mathematical formuli**, such as the kronocker and Karp's Algorithm 144 | 145 | - Another option is [Johnson's algorithm](https://en.wikipedia.org/wiki/Johnson%27s_algorithm) or [Floyd-Warshall](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) to compute an all-pairs shortest path 146 | - Both will **yield the greatest possible profitability** for an arbitrage is fed data correctly 147 | - Both **suffer from tracing profitability** to the actual path as multiple intersecting arbitrage paths in the graph will produce incorrect tracing 148 | - The shortest path from any given vertex to another does not directly result in a arbitrage path, which would **imply additional required computations** 149 | - No guarantee that the shortest path from one vertex to another and back is of reasonable length (could have very long trade paths) 150 | 151 | - A final option is **linear programming** with a linear solver for both improving the computational speed of the arbitrage finder and amount of optimizer 152 | - There are **multiple** providers of free/payed **linear solvers** such as Gurobi, MATLAB, HiGHS, or any [others](https://www.researchgate.net/post/What_is_the_best_software_available_for_linear_programming) 153 | - Constraints could be freely added by used such as certain coins to favor or avoid, path length, minimal or maximal exchanges to make trades on, min or max of trading a coin or set of coins, bias towards coins that have or have not been traded before 154 | - **Objective function would be the maximization profit** from available trades at any given time 155 | - Separate **objective function could be the optimized trading amount** from the parsed orderbook data 156 | 157 | - Additionally **before deploying the framework** as an active trading bot, one should consider hooking up the arbitrage paths that the bot finds to a Twilio text message/email bot. The Twilio application could send a user text messages about discovered arbitrage paths and the user could manually check them to recognize potential strengths and weaknesses of this framework. From there, a user can tweak user_settings customization or my code entirely to feel comfortable with the framework before deploying an active trading bot 158 | 159 | 160 | ## Disclaimer and Drawbacks 161 | - This crypto arbitrage framework was done for **personal interest**. It is possible that this framework will output a **false positive path** that **produces financial loss** because of outdated orderbook data so any outside use of this code is at the users discretion and risk 162 | - **Very few exchanges offer cross exchange trades**, so **keeping balances on all exchanges in use would be required** for the best market order completion speed. Else, one would have transfer funds between exchanges, pay transfer fees, and suffer from a slower execution of an arbitrage path. 163 | - If trading on multiple exchanges with large enough balances, it is possible for a user to parallelize market order submission by trading each pair independently. If the arbitrage path is profitable at order completion, the result overall will be profitable 164 | -------------------------------------------------------------------------------- /Framework/Header_Files/arbitrage_finder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "graph.hpp" 15 | #include "combinations.hpp" 16 | 17 | 18 | using namespace std; 19 | 20 | /* 21 | * 22 | * Struct designed for log data storage and arbitrage validation outside of inital discovery 23 | * 24 | */ 25 | struct TrackProfit 26 | { 27 | string from; 28 | string to; 29 | long double orderPrice; 30 | string bidOrask; 31 | string exchange; 32 | }; 33 | 34 | 35 | /* 36 | * 37 | * Computes from log representation of edge weight to 38 | * double decimal representation of edge weight 39 | * 40 | */ 41 | long double WeightConversion(long double conversionMetric){ 42 | return exp(conversionMetric); 43 | } 44 | 45 | 46 | /* 47 | * 48 | * Helpful method that prints out the trades of an arbitrage opportunity 49 | * 50 | */ 51 | void PrintCycle(vector cycleVec) 52 | { 53 | cout << "Arbitrage Detected!" << endl; 54 | for (int i = 0; i < cycleVec.size() - 1; i++) 55 | { 56 | cout << cycleVec[i].from << " --> "; 57 | } 58 | 59 | cout << cycleVec[cycleVec.size() - 1].from << " --> " << cycleVec[cycleVec.size() - 1].to << endl; 60 | } 61 | 62 | 63 | /* 64 | * 65 | * Helpful method that prints out contents of a string vector 66 | * 67 | */ 68 | void printVector(const vector &vec) 69 | { 70 | for (const string &str : vec) 71 | { 72 | cout << str << ", "; 73 | } 74 | cout << endl; 75 | } 76 | 77 | 78 | /* 79 | * 80 | * Helpful method that prints out contents of a double vector 81 | * 82 | */ 83 | void PrintVector(vector &vec) 84 | { 85 | for (double &num : vec) 86 | { 87 | cout << num << ", "; 88 | } 89 | cout << endl; 90 | } 91 | 92 | 93 | /* 94 | * 95 | * Helpful method that prints out contents of a hash set vector 96 | * 97 | */ 98 | void printUnorderedSet(const unordered_set &set) 99 | { 100 | for (const string &str : set) 101 | { 102 | cout << str << ", "; 103 | } 104 | cout << endl; 105 | } 106 | 107 | 108 | /* 109 | * 110 | * Helpful method that prints out contents of a hash map vector 111 | * 112 | */ 113 | void printUnorderedMap(const unordered_map &map) 114 | { 115 | for (auto access : map) 116 | { 117 | cout << "From: " << access.first << ", To: " << access.second << endl; 118 | } 119 | cout << endl; 120 | } 121 | 122 | 123 | /* 124 | * 125 | * Print method for log message of tracking profitability 126 | * 127 | */ 128 | void printArbInfo(vector &arbPath, unordered_map &feeMap) 129 | { 130 | for(int i = 0; i < arbPath.size(); i++){ 131 | cout << "From " << arbPath[i].from << " to " << arbPath[i].to; 132 | cout << " via " << arbPath[i].exchange << " using a " << arbPath[i].bidOrask; 133 | cout << " at " << WeightConversion(arbPath[i].orderPrice); 134 | cout << " with " << feeMap[arbPath[i].exchange] * 100 << "% fee" << endl; 135 | } 136 | } 137 | 138 | 139 | /* 140 | * 141 | * Print method for a checkpoint update on the arbitrage 142 | * find rate overall and since the last checkpoint 143 | * 144 | */ 145 | void CheckPointInfo(int frameworkIterations, int positiveArbs, int &currIterations, int &currArbsFound) 146 | { 147 | if (frameworkIterations % 25 == 0) 148 | { 149 | cout << endl; 150 | cout << "Current iteration: " << frameworkIterations << endl;; 151 | cout << "\t-over last 25 iterations, arbitrage find rate:\t" << (double)currArbsFound/currIterations << "%" << endl; 152 | cout << "\t-overall arbitrage find rate:\t" << (double)positiveArbs/frameworkIterations<< "%" << endl; 153 | cout << endl; 154 | currIterations=0; 155 | currArbsFound=0; 156 | } 157 | } 158 | 159 | 160 | /* 161 | * 162 | * Print method for determining the profitability 163 | * of a given arbitrage cycle detected 164 | * 165 | */ 166 | void printArbProfitability(vector &arbPath, unordered_map &feeMap) 167 | { 168 | double currProfit = 0; 169 | int arbLen = arbPath.size(); 170 | for (int i=0; i &arbPath, unordered_map &feeMap) 185 | { 186 | double profit = 0; 187 | for (int i = 0; i < arbPath.size(); i++) 188 | { 189 | profit += arbPath[i].orderPrice + log(1-feeMap[arbPath[i].exchange]); 190 | } 191 | profit = (WeightConversion(profit) - 1) * 100; 192 | return profit; 193 | } 194 | 195 | 196 | /* 197 | * 198 | * Print method for logging Arbitrage path vertex and edges 199 | * 200 | */ 201 | void printArbEdgeInfo(Graph &g, vector &arbPath) 202 | { 203 | for (int i = 0; i < arbPath.size(); i++) 204 | { 205 | g.printEdge(arbPath[i].to, arbPath[i].from, arbPath[i].exchange); 206 | g.printEdge(arbPath[i].from, arbPath[i].to, arbPath[i].exchange); 207 | cout << endl; 208 | } 209 | } 210 | 211 | 212 | /* 213 | * 214 | * Debugging print method for printing some stars 215 | * 216 | */ 217 | void printStars() 218 | { 219 | cout << "*****************" << endl; 220 | } 221 | 222 | void LogArbInfo(vector &arbPath, unordered_map &feeMap, string startCoin, double idealAmountProfit) 223 | { 224 | if (arbPath.size() > 0) 225 | { 226 | cout << startCoin << "->"; 227 | for (int i = 0; i < arbPath.size()-1; i++) 228 | cout << arbPath[i].to << "->"; 229 | cout << startCoin; 230 | cout << ", tick_p=" << arbPathMaxProfit(arbPath, feeMap) << "%"; 231 | cout << ", orbo_p=" << idealAmountProfit << "%" << endl; 232 | } 233 | else 234 | { 235 | cout << "no profitable path detected" << endl; 236 | } 237 | } 238 | 239 | 240 | /* 241 | * 242 | * Method for determining if current profit is the max profit 243 | * 244 | */ 245 | bool maxProfitCheck(double& maxProfit, double& currProfit, double& lowerThreshold, double& upperThreshold){ 246 | if (currProfit > maxProfit && currProfit > lowerThreshold && currProfit < upperThreshold) 247 | { 248 | maxProfit = currProfit; 249 | return true; 250 | } 251 | return false; 252 | } 253 | 254 | 255 | /* 256 | * 257 | * Method to record information about the most profitable arbitrage path 258 | * Designed to be used for profit validation and order/trade amount optimization 259 | * 260 | */ 261 | void updateMaxPath(vector& negCyclePath, vector trades) 262 | { 263 | vector path (trades.size()); 264 | 265 | // firstTrade is special case; each edge struct only contains the destination currency 266 | TrackProfit firstTrade {trades[trades.size()-1].to, trades[0].to, trades[0].exPrice, trades[0].bidOrAsk, trades[0].exchange}; 267 | path[0] = firstTrade; 268 | 269 | // Add each trade edge to the cyclePath for later validation 270 | for(int i = 0; i < trades.size()-1; i++) 271 | { 272 | TrackProfit trade {trades[i].to, trades[i+1].to, trades[i+1].exPrice, trades[i+1].bidOrAsk, trades[i+1].exchange}; 273 | path[i+1] = trade; 274 | } 275 | 276 | negCyclePath = path; 277 | } 278 | 279 | 280 | /* 281 | * 282 | * Band-aid struct created to reduce arguments required for arbitrage finding functions 283 | * 284 | */ 285 | struct processInput{ 286 | double lowerBound; 287 | double upperBound; 288 | double &maxProfit; 289 | int arbLen; 290 | string source; 291 | }; 292 | 293 | 294 | /* 295 | * 296 | * Algorithm attempts to fill in this path with the most profitable trade 297 | * SourceCoin --> Coin1 --> SourceCoin 298 | * 299 | */ 300 | void ProcessLen2(Graph &g, vector &negCyclePath, processInput inputVars) 301 | { 302 | double currProfit; 303 | /* 304 | * 305 | * Algorithm attempts to fill in this path with the most profitable trade 306 | * SourceCoin --> Coin1 --> SourceCoin 307 | * 308 | */ 309 | for (Edge firstTradeEdge : g.adjacencyList[inputVars.source]) 310 | { 311 | // first trade cost 312 | currProfit = (firstTradeEdge.exPrice + firstTradeEdge.fee); 313 | 314 | // second trade 315 | for (Edge secondTradeEdge : g.adjacencyList[firstTradeEdge.to]) 316 | { 317 | // all arb paths must start and end at the source currencies 318 | if (secondTradeEdge.to == inputVars.source) 319 | { 320 | currProfit += (secondTradeEdge.exPrice + secondTradeEdge.fee); 321 | 322 | // need to do a max profit check 323 | if (maxProfitCheck(inputVars.maxProfit, currProfit, inputVars.lowerBound, inputVars.upperBound)) 324 | { 325 | vector path {firstTradeEdge, secondTradeEdge}; 326 | updateMaxPath(negCyclePath, path); 327 | } 328 | break; 329 | } 330 | } 331 | } 332 | } 333 | 334 | 335 | /* 336 | * 337 | * Algorithm attempts to fill in this path with the most profitable trade 338 | * SourceCoin --> Coin1 --> Coin2 --> SourceCoin 339 | * 340 | */ 341 | void ProcessLen3(Graph &g, vector &negCyclePath, processInput inputVars) 342 | { 343 | double currProfit; 344 | 345 | // brute force combinations to maximize profitability 346 | for (Edge firstTradeEdge : g.adjacencyList[inputVars.source]) 347 | { 348 | if (firstTradeEdge.to == inputVars.source) 349 | continue; 350 | currProfit += (firstTradeEdge.exPrice + firstTradeEdge.fee); 351 | 352 | for (Edge secondTradeEdge : g.adjacencyList[firstTradeEdge.to]) 353 | { 354 | if (secondTradeEdge.to == inputVars.source) 355 | continue; 356 | currProfit += (secondTradeEdge.exPrice + secondTradeEdge.fee); 357 | 358 | for (Edge thirdTradeEdge : g.adjacencyList[secondTradeEdge.to]) 359 | { 360 | // all arb paths must start and end at the source currencies 361 | if (thirdTradeEdge.to == inputVars.source) 362 | { 363 | currProfit += (thirdTradeEdge.exPrice + thirdTradeEdge.fee); 364 | // need to do a max profit check 365 | if (maxProfitCheck(inputVars.maxProfit, currProfit, inputVars.lowerBound, inputVars.upperBound)) 366 | { 367 | vector path {firstTradeEdge, secondTradeEdge, thirdTradeEdge}; 368 | updateMaxPath(negCyclePath, path); 369 | } 370 | currProfit -= (thirdTradeEdge.exPrice + thirdTradeEdge.fee); 371 | break; 372 | } 373 | } 374 | currProfit -= (secondTradeEdge.exPrice + secondTradeEdge.fee); 375 | } 376 | currProfit -= (firstTradeEdge.exPrice + firstTradeEdge.fee); 377 | } 378 | } 379 | 380 | 381 | /* 382 | * 383 | * Base triangular arbitrage algorithm from ProcessLen3 extrapolated 384 | * for parallel time improvement of 4 path arbitrages 385 | * 386 | */ 387 | void ProcessBase3For4(Graph &g, vector &negCyclePath, processInput inputVars, 388 | Edge firstTradeEdge, double currProfit, mutex &negCyclePath_mutex) 389 | { 390 | for (Edge secondTradeEdge : g.adjacencyList[firstTradeEdge.to]) 391 | { 392 | if (secondTradeEdge.to == inputVars.source) 393 | continue; 394 | currProfit += (secondTradeEdge.exPrice + secondTradeEdge.fee); 395 | 396 | for (Edge thirdTradeEdge : g.adjacencyList[secondTradeEdge.to]) 397 | { 398 | if (thirdTradeEdge.to == inputVars.source) 399 | continue; 400 | currProfit += (thirdTradeEdge.exPrice + thirdTradeEdge.fee); 401 | 402 | for (Edge fourthTradeEdge : g.adjacencyList[thirdTradeEdge.to]) 403 | { 404 | // all arb paths must start and end at the source currencies 405 | if (fourthTradeEdge.to == inputVars.source) 406 | { 407 | currProfit += (fourthTradeEdge.exPrice + fourthTradeEdge.fee); 408 | // need to do a max profit check 409 | if (maxProfitCheck(inputVars.maxProfit, currProfit, inputVars.lowerBound, inputVars.upperBound)) 410 | { 411 | vector path {firstTradeEdge, secondTradeEdge, thirdTradeEdge, fourthTradeEdge}; 412 | negCyclePath_mutex.lock(); 413 | updateMaxPath(negCyclePath, path); 414 | negCyclePath_mutex.unlock(); 415 | } 416 | currProfit -= (fourthTradeEdge.exPrice + fourthTradeEdge.fee); 417 | break; 418 | } 419 | } 420 | currProfit -= (thirdTradeEdge.exPrice + thirdTradeEdge.fee); 421 | } 422 | currProfit -= (secondTradeEdge.exPrice + secondTradeEdge.fee); 423 | } 424 | } 425 | 426 | 427 | /* 428 | * 429 | * Base triangular arbitrage algorithm from ProcessLen3 extrapolated 430 | * for parallel time improvement of 5 path arbitrages 431 | * 432 | */ 433 | void ProcessBase3For5(Graph &g, vector &negCyclePath, processInput inputVars, 434 | Edge firstTradeEdge, Edge secondTradeEdge, double currProfit, mutex &negCyclePath_mutex) 435 | { 436 | for (Edge thirdTradeEdge : g.adjacencyList[secondTradeEdge.to]) 437 | { 438 | if (thirdTradeEdge.to == inputVars.source) 439 | continue; 440 | currProfit += (thirdTradeEdge.exPrice + thirdTradeEdge.fee); 441 | 442 | for (Edge fourthTradeEdge : g.adjacencyList[thirdTradeEdge.to]) 443 | { 444 | if (fourthTradeEdge.to == inputVars.source) 445 | continue; 446 | currProfit += (fourthTradeEdge.exPrice + fourthTradeEdge.fee); 447 | 448 | for (Edge fifthTradeEdge : g.adjacencyList[fourthTradeEdge.to]) 449 | { 450 | // all arb paths must start and end at the source currencies 451 | if (fifthTradeEdge.to == inputVars.source) 452 | { 453 | currProfit += (fifthTradeEdge.exPrice + fifthTradeEdge.fee); 454 | 455 | // need to do a max profit check 456 | if (maxProfitCheck(inputVars.maxProfit, currProfit, inputVars.lowerBound, inputVars.upperBound)) 457 | { 458 | vector path {firstTradeEdge, secondTradeEdge, thirdTradeEdge, fourthTradeEdge, fifthTradeEdge}; 459 | negCyclePath_mutex.lock(); 460 | updateMaxPath(negCyclePath, path); 461 | negCyclePath_mutex.unlock(); 462 | } 463 | currProfit -= (fifthTradeEdge.exPrice + fifthTradeEdge.fee); 464 | break; 465 | } 466 | } 467 | currProfit -= (fourthTradeEdge.exPrice + fourthTradeEdge.fee); 468 | } 469 | currProfit -= (thirdTradeEdge.exPrice + thirdTradeEdge.fee); 470 | } 471 | } 472 | 473 | 474 | /* 475 | * 476 | * Algorithm attempts to fill in this path with the most profitable trade 477 | * SourceCoin --> Coin1 --> Coin2 --> Coin3 --> SourceCoin 478 | * 479 | */ 480 | void ProcessLen4(Graph &g, vector &negCyclePath, processInput inputVars) 481 | { 482 | double currProfit; 483 | vector threads; 484 | mutex negCyclePath_mutex; 485 | 486 | // brute force combinations to maximize profitability 487 | for (Edge firstTradeEdge : g.adjacencyList[inputVars.source]) 488 | { 489 | currProfit = (firstTradeEdge.exPrice + firstTradeEdge.fee); 490 | threads.push_back(thread(ProcessBase3For4, 491 | ref(g), ref(negCyclePath), ref(inputVars), 492 | ref(firstTradeEdge), ref(currProfit), 493 | ref(negCyclePath_mutex))); 494 | } 495 | for (auto &thread : threads) { 496 | thread.join(); 497 | } 498 | } 499 | 500 | 501 | /* 502 | * 503 | * Algorithm attempts to fill in this path with the most profitable trade 504 | * SourceCoin --> Coin1 --> Coin2 --> Coin3 --> Coin4 --> SourceCoin 505 | * 506 | */ 507 | void ProcessLen5(Graph &g, vector &negCyclePath, processInput inputVars) 508 | { 509 | double currProfit; 510 | mutex negCyclePath_mutex; 511 | vector threads; 512 | for (Edge firstTradeEdge : g.adjacencyList[inputVars.source]) 513 | { 514 | currProfit = (firstTradeEdge.exPrice + firstTradeEdge.fee); 515 | 516 | for (Edge secondTradeEdge : g.adjacencyList[firstTradeEdge.to]) 517 | { 518 | currProfit += (secondTradeEdge.exPrice + secondTradeEdge.fee); 519 | 520 | threads.push_back(thread(ProcessBase3For5, ref(g), ref(negCyclePath), 521 | ref(inputVars), ref(firstTradeEdge), ref(secondTradeEdge), 522 | ref(currProfit), ref(negCyclePath_mutex))); 523 | 524 | currProfit -= (secondTradeEdge.exPrice + secondTradeEdge.fee); 525 | } 526 | } 527 | for (thread &thread : threads) { 528 | thread.join(); 529 | } 530 | } 531 | 532 | 533 | 534 | void ArbDetectControl(Graph &g, vector &negCyclePath, processInput inputVars) 535 | { 536 | double currProfit = 0; 537 | if (inputVars.arbLen > 3) 538 | { 539 | if (inputVars.arbLen == 4) 540 | { 541 | ProcessLen4(g, negCyclePath, inputVars); 542 | } 543 | else if (inputVars.arbLen == 5) 544 | { 545 | ProcessLen5(g, negCyclePath, inputVars); 546 | } 547 | } 548 | 549 | if (inputVars.arbLen == 3) 550 | { 551 | ProcessLen3(g, negCyclePath, inputVars); 552 | } 553 | else if (inputVars.arbLen == 2) 554 | { 555 | ProcessLen2(g, negCyclePath, inputVars); 556 | } 557 | 558 | } 559 | 560 | 561 | /* 562 | * O(n^2/p * n^3) brute force algorithm for determining arbitrage profitability 563 | * p is the number of available processors 564 | * Algorithm attempts to fill in this path with the most profitable trade 565 | * source --> Coin1 --> Coin2 --> ... --> source 566 | * - Each --> represents a trade and at each the bid or ask price can be used 567 | * 568 | */ 569 | vector ArbDetect(Graph& g, string source, double lowerProfitThreshold, double upperProfitThreshold, int arbLen) 570 | { 571 | double upperBound = log(upperProfitThreshold); 572 | double lowerBound = log(lowerProfitThreshold); 573 | double maxProfit = 0; 574 | vector negCyclePath; 575 | 576 | processInput arbFindVars = {lowerBound, upperBound, maxProfit, arbLen, source}; 577 | ArbDetectControl(g, negCyclePath, arbFindVars); 578 | 579 | return negCyclePath; 580 | } -------------------------------------------------------------------------------- /Framework/Header_Files/exchange_api_pull.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace std; 16 | using namespace std::chrono; 17 | 18 | 19 | /* 20 | * 21 | * Determines size for buffer needed for data received from API responses 22 | * 23 | */ 24 | static size_t writeCallback(void *contents, size_t size, size_t nmemb, void *userp) 25 | { 26 | ((string *)userp)->append((char *)contents, size * nmemb); 27 | return size * nmemb; 28 | } 29 | 30 | 31 | /* 32 | * 33 | * Helper method that converts a string to lowercase 34 | * (should be in the string library) 35 | * 36 | */ 37 | string toLowerCase(string str) { 38 | for (char& c : str) { 39 | if (c >= 'A' && c <= 'Z') { 40 | c = c + ('a' - 'A'); 41 | } 42 | } 43 | return str; 44 | } 45 | 46 | 47 | /* 48 | * 49 | * Develops a HashMap of predetermined viable trading pairs to add to 50 | * an active graph of trading pair exchanges 51 | * Map Structure: "Coin1Coin2" -> ["Coin1", "Coin2"] 52 | * 53 | */ 54 | unordered_map> buildSymbolHashMap(string symbolPath) 55 | { 56 | // read from Symbol_Space_Split.txt and build a dictionary 57 | //TODO: replace with updated path after scraping script has been created 58 | ifstream input_file(symbolPath.c_str()); 59 | string line, lineCopy; 60 | short int slashPos; 61 | unordered_map> symbolMap; 62 | 63 | 64 | if (input_file.is_open()) 65 | { 66 | while (getline(input_file, line)) 67 | { 68 | short strLen = line.length(), forSlashPos = line.find("/"); 69 | string firstCoin = line.substr(1, forSlashPos-1); 70 | string secondCoin = line.substr(forSlashPos + 1, strLen - forSlashPos - 2); 71 | 72 | vector symbolsVec = {firstCoin, secondCoin}; 73 | string symbolKey = firstCoin + secondCoin; 74 | // cout << "Key: " << symbolKey << " coins: " << firstCoin << ", " << secondCoin << endl; 75 | symbolMap[symbolKey] = symbolsVec; 76 | } 77 | input_file.close(); 78 | } 79 | else 80 | { 81 | cout << "Unable to open symbol pruning file" << endl; 82 | } 83 | return symbolMap; 84 | } 85 | 86 | 87 | /* 88 | * 89 | * Resize method to take the massive symbolMap unordered_map of random combinations 90 | * of possible viable trading pairs to fixed in size to only the trading pairs 91 | * that are identified as active on exchanges 92 | * 93 | */ 94 | void symbolHashMapResize(unordered_map> &symbolMap, unordered_set &seenSymbols) 95 | { 96 | vector toErase; 97 | string tradingPairSymbol; 98 | // iterate over the keys of symbolMap 99 | for (pair> symbolEntry : symbolMap) 100 | { 101 | // if tradingPair is not deemed as active by API pull then mark it for erase 102 | tradingPairSymbol = symbolEntry.first; 103 | if (seenSymbols.find(tradingPairSymbol) == seenSymbols.end()) 104 | { 105 | toErase.push_back(tradingPairSymbol); 106 | } 107 | } 108 | 109 | // erase all symbols in the symbolMap that do not relate to active trading pairs 110 | for(string eraseStr : toErase) 111 | { 112 | symbolMap.erase(eraseStr); 113 | } 114 | 115 | } 116 | 117 | 118 | /* 119 | * 120 | * Helper method for identifying which buy and sell side of orderbook 121 | * by the units for coin amount and coin price 122 | * 123 | */ 124 | void updateOrderBookSides(vector &orderBookSides, string &symbol, TrackProfit &spotTrade, string &delimiter) 125 | { 126 | // check naming variations for different exchanges 127 | if (symbol == spotTrade.to + delimiter + spotTrade.from || 128 | toLowerCase(symbol) == toLowerCase(spotTrade.to) + delimiter + toLowerCase(spotTrade.from)) 129 | { 130 | orderBookSides[0] = spotTrade.to; // Amount unit 131 | orderBookSides[1] = spotTrade.from; // Price unit 132 | } 133 | else 134 | { 135 | orderBookSides[0] = spotTrade.from; // Amount unit 136 | orderBookSides[1] = spotTrade.to; // Price unit 137 | } 138 | } 139 | 140 | 141 | /* 142 | * 143 | * Develops a HashMap of exchanges and their spot trading fees 144 | * Map Structure: Binance -> [0.002] 145 | * TODO: Consider logging the fee map entries 146 | * 147 | */ 148 | unordered_map buildFeeMap(){ 149 | unordered_map feeMap; 150 | feeMap["binance"] = 0.002; 151 | feeMap["bitget"] = 0.002; 152 | feeMap["bitmart"] = 0.005; 153 | feeMap["gateio"] = 0.003; 154 | feeMap["huobi"] = 0.002; 155 | feeMap["kucoin"] = 0.002; 156 | return feeMap; 157 | } 158 | 159 | 160 | /* 161 | * 162 | * Pull spot ticker data from Binance Exchange via API 163 | * and add data to Graph 164 | * 165 | */ 166 | void pullBinance(unordered_map > &symbolMap, Graph &g, bool setGraph, unordered_set &seenSymbols, mutex &seenSymbols_mutex) 167 | { 168 | CURL *curl; 169 | CURLcode res; 170 | string response; 171 | string exchange = "binance"; 172 | double exchangeFee = 0.002; 173 | 174 | 175 | curl = curl_easy_init(); 176 | if (curl) 177 | { 178 | // cout << "Start pull from " << exchange << "..."; 179 | const char* exchangeURL = "https://api.binance.com/api/v3/ticker/bookTicker"; 180 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 181 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 182 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 183 | res = curl_easy_perform(curl); 184 | 185 | // Check for errors 186 | if (res != CURLE_OK) 187 | { 188 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 189 | } 190 | 191 | try 192 | { 193 | nlohmann::json json_data = nlohmann::json::parse(response); 194 | 195 | for (const auto &item : json_data) 196 | { 197 | string tradeSymbol = string(item["symbol"]); 198 | vector assets = symbolMap[tradeSymbol]; 199 | assets = symbolMap[tradeSymbol]; 200 | 201 | 202 | // Trading pair is not viable by user settings 203 | if (assets.size() != 2) 204 | continue; 205 | 206 | string fromAsset = assets[0], toAsset = assets[1]; 207 | string strBidPrice = item["bidPrice"], strAskPrice = item["askPrice"]; 208 | double bidPrice = stod(strBidPrice), askPrice = stod(strAskPrice); 209 | if ((bidPrice == 0.0) || (askPrice == 0.0)) 210 | { 211 | continue; 212 | } 213 | if (setGraph) 214 | { 215 | g.addEdge(fromAsset, toAsset, exchangeFee, exchange); 216 | // Record what trading symbol was used to resize symbolMap later 217 | // mark TradingPair as seen; symbolMap will be resized at set up with this information 218 | seenSymbols_mutex.lock(); 219 | seenSymbols.insert(tradeSymbol); 220 | seenSymbols.insert(toAsset + fromAsset); 221 | seenSymbols_mutex.unlock(); 222 | } 223 | else 224 | { 225 | g.updateEdge(fromAsset, toAsset, bidPrice, askPrice, exchange); 226 | } 227 | } 228 | } 229 | catch (const exception &e) 230 | { 231 | cout << exchange << " SSL EXCEPTION DETECTED" << endl; 232 | } 233 | 234 | curl_easy_cleanup(curl); 235 | } 236 | } 237 | 238 | 239 | /* 240 | * 241 | * Pull order book data from Binance Exchange via API 242 | * and return info 243 | * 244 | */ 245 | void pullBinanceOrderBook(TrackProfit &spotTrade, vector> &orderBookData, vector &orderBookSides, int &nDepth) 246 | { 247 | CURL *curl; 248 | CURLcode res; 249 | string response; 250 | vector symbolCombo; string delimiter = ""; 251 | symbolCombo.push_back(spotTrade.to + spotTrade.from); symbolCombo.push_back(spotTrade.from + spotTrade.to); 252 | string baseURL = "https://api.binance.com/api/v3/depth", query, URL; 253 | 254 | 255 | curl = curl_easy_init(); 256 | if (curl) 257 | { 258 | for(string &symbol : symbolCombo) 259 | { 260 | query = "?symbol=" + symbol + "&limit=" + to_string(nDepth); 261 | URL = baseURL + query; 262 | const char* exchangeURL = URL.c_str(); 263 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 264 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 265 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 266 | res = curl_easy_perform(curl); 267 | 268 | // Check for errors 269 | if (res != CURLE_OK) 270 | { 271 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 272 | } 273 | 274 | nlohmann::json json_data = nlohmann::json::parse(response); 275 | // cout << "exchange: " << spotTrade.exchange << "response: " << response << endl; 276 | response.clear(); // Clear the response variable 277 | // invalid combination 278 | if (json_data.find("msg") != json_data.end()) 279 | continue; 280 | 281 | int jsonDataBidCtn = json_data["bids"].size(); 282 | int jsonDataAskCtn = json_data["asks"].size(); 283 | int nDepthCopy = min(min(nDepth, jsonDataAskCtn), jsonDataBidCtn); 284 | 285 | // parse out each active order in orderbook 286 | for (int i = 0; i < nDepthCopy; i++) 287 | { 288 | string strBidPrice = json_data["bids"][i][0], strBidAmt = json_data["bids"][i][1]; 289 | double bidPrice = stod(strBidPrice), bidAmt = stod(strBidAmt); 290 | string strAskPrice = json_data["asks"][i][0], strAskAmt = json_data["asks"][i][1]; 291 | double askPrice = stod(strAskPrice), askAmt = stod(strAskAmt); 292 | orderBookData[0][i] = log(bidPrice); orderBookData[1][i] = bidAmt; 293 | orderBookData[2][i] = log(askPrice); orderBookData[3][i] = log(askPrice) + askAmt; 294 | } 295 | // resize the array when needed 296 | if (nDepthCopy != nDepth) 297 | { 298 | orderBookData[0].resize(nDepthCopy); orderBookData[1].resize(nDepthCopy); 299 | orderBookData[2].resize(nDepthCopy); orderBookData[3].resize(nDepthCopy); 300 | } 301 | 302 | updateOrderBookSides(orderBookSides, symbol, spotTrade, delimiter); 303 | break; 304 | 305 | } 306 | curl_easy_cleanup(curl); 307 | } 308 | } 309 | 310 | 311 | /* 312 | * 313 | * Pull spot ticker data from bitget Exchange via API 314 | * and add data to Graph 315 | * 316 | */ 317 | void pullBitget(unordered_map > &symbolMap, Graph &g, bool setGraph, unordered_set &seenSymbols, mutex &seenSymbols_mutex) 318 | { 319 | CURL *curl; 320 | CURLcode res; 321 | string response; 322 | string exchange = "bitget"; 323 | double exchangeFee = 0.002; 324 | 325 | curl = curl_easy_init(); 326 | if (curl) 327 | { 328 | // cout << "Start pull from " << exchange << "..."; 329 | const char* exchangeURL = "https://api.bitget.com/api/spot/v1/market/tickers"; 330 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 331 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 332 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 333 | res = curl_easy_perform(curl); 334 | 335 | 336 | // Check for errors 337 | if (res != CURLE_OK) 338 | { 339 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 340 | } 341 | 342 | try { 343 | nlohmann::json json_data = nlohmann::json::parse(response); 344 | 345 | for (auto& item : json_data["data"]) { 346 | // symbol comes in uppercase with the coins seperated by a hyphen 347 | string tradeSymbol = item["symbol"]; 348 | vector assets = symbolMap[tradeSymbol]; 349 | if (assets.size() != 2) 350 | { 351 | continue; 352 | } 353 | string fromAsset = assets[0], toAsset = assets[1]; 354 | string strBidPrice = item["buyOne"], strAskPrice = item["sellOne"]; 355 | long double bidPrice = stod(strBidPrice), askPrice = stod(strAskPrice); 356 | if ((bidPrice == 0.0) || (askPrice == 0.0)){ 357 | continue; 358 | } 359 | if (setGraph){ 360 | g.addEdge(fromAsset, toAsset, exchangeFee, exchange); 361 | // mark TradingPair as seen; symbolMap will be resized at set up with this information 362 | seenSymbols_mutex.lock(); 363 | seenSymbols.insert(tradeSymbol); 364 | seenSymbols.insert(toAsset + fromAsset); 365 | seenSymbols_mutex.unlock(); 366 | } 367 | else{ 368 | g.updateEdge(fromAsset, toAsset, bidPrice, askPrice, exchange); 369 | } 370 | } 371 | } 372 | catch (const exception &e) 373 | { 374 | cout << exchange << " SSL EXCEPTION DETECTED" << endl; 375 | } 376 | 377 | // cout << "Finished pull from " << exchange << "\n" << endl; 378 | curl_easy_cleanup(curl); 379 | } 380 | } 381 | 382 | 383 | /* 384 | * 385 | * Pull order book data from bitget Exchange via API 386 | * and add data to Graph 387 | * 388 | */ 389 | void pullBitgetOrderBook(TrackProfit &spotTrade, vector> &orderBookData, vector &orderBookSides, int nDepth) 390 | { 391 | CURL *curl; 392 | CURLcode res; 393 | string response; 394 | vector symbolCombo; string delimiter = ""; 395 | symbolCombo.push_back(spotTrade.to + spotTrade.from); symbolCombo.push_back(spotTrade.from + spotTrade.to); 396 | string baseURL = "https://capi.bitget.com/data/v1/market/depth", query, URL; 397 | 398 | curl = curl_easy_init(); 399 | if (curl) 400 | { 401 | for(string &symbol : symbolCombo) 402 | { 403 | query = "?symbol=" + symbol; 404 | URL = baseURL + query; 405 | const char* exchangeURL = URL.c_str(); 406 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 407 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 408 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 409 | res = curl_easy_perform(curl); 410 | 411 | // Check for errors 412 | if (res != CURLE_OK) 413 | { 414 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 415 | } 416 | 417 | nlohmann::json json_data = nlohmann::json::parse(response); 418 | // cout << "exchange: " << spotTrade.exchange << "response: " << response << endl; 419 | response.clear(); // Clear the response variable 420 | // invalid API request 421 | if (json_data["data"]["asks"].size() == 0) 422 | { 423 | continue; 424 | } 425 | 426 | int jsonDataBidCtn = json_data["data"]["bids"].size(); 427 | int jsonDataAskCtn = json_data["data"]["asks"].size(); 428 | int nDepthCopy = min(min(nDepth, jsonDataAskCtn), jsonDataBidCtn); 429 | 430 | // parse out each active order in orderbook 431 | for (int i = 0; i < nDepthCopy; i++) 432 | { 433 | string strBidPrice = json_data["data"]["bids"][i][0], strBidAmt = json_data["data"]["bids"][i][1]; 434 | double bidPrice = stod(strBidPrice), bidAmt = stod(strBidAmt); 435 | string strAskPrice = json_data["data"]["asks"][i][0], strAskAmt = json_data["data"]["asks"][i][1]; 436 | double askPrice = stod(strAskPrice), askAmt = stod(strAskAmt); 437 | orderBookData[0][i] = log(bidPrice); orderBookData[1][i] = bidAmt; 438 | orderBookData[2][i] = log(askPrice); orderBookData[3][i] = askAmt; 439 | } 440 | // resize the array when needed 441 | if (nDepthCopy != nDepth) 442 | { 443 | orderBookData[0].resize(nDepthCopy); orderBookData[1].resize(nDepthCopy); 444 | orderBookData[2].resize(nDepthCopy); orderBookData[3].resize(nDepthCopy); 445 | } 446 | 447 | updateOrderBookSides(orderBookSides, symbol, spotTrade, delimiter); 448 | // correct url has been traversed 449 | break; 450 | 451 | } 452 | curl_easy_cleanup(curl); 453 | } 454 | } 455 | 456 | 457 | /* 458 | * 459 | * Pull spot ticker data from BitMart Exchange via API 460 | * 461 | */ 462 | void pullBitMart(unordered_map > &symbolMap, Graph &g, bool setGraph, unordered_set &seenSymbols, mutex &seenSymbols_mutex) 463 | { 464 | CURL *curl; 465 | CURLcode res; 466 | string response; 467 | string exchange = "bitmart"; 468 | double exchangeFee = 0.005; 469 | 470 | curl = curl_easy_init(); 471 | if (curl) 472 | { 473 | // cout << "Start pull from " << exchange << "..."; 474 | const char* exchangeURL = "https://api-cloud.bitmart.com/spot/v2/ticker"; 475 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 476 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 477 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 478 | res = curl_easy_perform(curl); 479 | 480 | // Check for errors 481 | if (res != CURLE_OK) 482 | { 483 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 484 | } 485 | 486 | try{ 487 | nlohmann::json json_data = nlohmann::json::parse(response); 488 | 489 | for (auto& item : json_data["data"]["tickers"]) { 490 | // symbol comes in uppercase with the coins seperated by a hyphen 491 | string tradeSymbol = item["symbol"]; 492 | int delimeterPos = tradeSymbol.find("_"); 493 | tradeSymbol = tradeSymbol.substr(0, delimeterPos) + tradeSymbol.substr(delimeterPos+1); 494 | vector assets = symbolMap[tradeSymbol]; 495 | if (assets.size() != 2) 496 | { 497 | continue; 498 | } 499 | string fromAsset = assets[0], toAsset = assets[1]; 500 | string strBidPrice = item["best_bid"], strAskPrice = item["best_ask"]; 501 | long double bidPrice = stod(strBidPrice), askPrice = stod(strAskPrice); 502 | if ((bidPrice == 0.0) || (askPrice == 0.0)){ 503 | continue; 504 | } 505 | if (setGraph){ 506 | g.addEdge(fromAsset, toAsset, exchangeFee, exchange); 507 | 508 | // mark TradingPair as seen; symbolMap will be resized at set up with this information 509 | seenSymbols_mutex.lock(); 510 | seenSymbols.insert(tradeSymbol); 511 | seenSymbols.insert(toAsset + fromAsset); 512 | seenSymbols_mutex.unlock(); 513 | } 514 | else{ 515 | g.updateEdge(fromAsset, toAsset, bidPrice, askPrice, exchange); 516 | } 517 | 518 | } 519 | 520 | } 521 | catch (const exception &e) 522 | { 523 | cout << exchange << " SSL EXCEPTION DETECTED" << endl; 524 | } 525 | // cout << "Finished pull from " << exchange << "\n" << endl; 526 | curl_easy_cleanup(curl); 527 | } 528 | } 529 | 530 | 531 | /* 532 | * 533 | * Pull order book data from BitMart Exchange via API 534 | * 535 | */ 536 | void pullBitMartOrderBook(TrackProfit &spotTrade, vector> &orderBookData, vector &orderBookSides, int nDepth) 537 | { 538 | CURL *curl; 539 | CURLcode res; 540 | string response; 541 | vector symbolCombo; string delimiter = "_"; 542 | symbolCombo.push_back(spotTrade.to + "_" +spotTrade.from); symbolCombo.push_back(spotTrade.from + "_" + spotTrade.to); 543 | string baseURL = "https://api-cloud.bitmart.com/spot/v1/symbols/book", query, URL; 544 | 545 | curl = curl_easy_init(); 546 | if (curl) 547 | { 548 | for(string &symbol : symbolCombo) 549 | { 550 | query = "?symbol=" + symbol + "&size=" + to_string(nDepth); 551 | URL = baseURL + query; 552 | const char* exchangeURL = URL.c_str(); 553 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 554 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 555 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 556 | res = curl_easy_perform(curl); 557 | 558 | // Check for errors 559 | if (res != CURLE_OK) 560 | { 561 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 562 | } 563 | 564 | nlohmann::json json_data = nlohmann::json::parse(response); 565 | // cout << "exchange: " << spotTrade.exchange << "response: " << response << endl; 566 | response.clear(); // Clear the response variable 567 | // invalid API request 568 | if (json_data["message"] != "OK") 569 | { 570 | continue; 571 | } 572 | int jsonDataBidCtn = json_data["data"]["buys"].size(); 573 | int jsonDataAskCtn = json_data["data"]["sells"].size(); 574 | int nDepthCopy = min(min(nDepth, jsonDataAskCtn), jsonDataBidCtn); 575 | 576 | // parse out each active order in orderbook 577 | for (int i = 0; i < nDepthCopy; i++) 578 | { 579 | string strBidPrice = json_data["data"]["buys"][i]["price"], strBidAmt = json_data["data"]["buys"][i]["amount"]; 580 | double bidPrice = stod(strBidPrice), bidAmt = stod(strBidAmt); 581 | string strAskPrice = json_data["data"]["sells"][i]["price"], strAskAmt = json_data["data"]["sells"][i]["amount"]; 582 | double askPrice = stod(strAskPrice), askAmt = stod(strAskAmt); 583 | orderBookData[0][i] = log(bidPrice); orderBookData[1][i] = bidAmt; 584 | orderBookData[2][i] = log(askPrice); orderBookData[3][i] = askAmt; 585 | } 586 | // resize the array when needed 587 | if (nDepthCopy != nDepth) 588 | { 589 | orderBookData[0].resize(nDepthCopy); orderBookData[1].resize(nDepthCopy); 590 | orderBookData[2].resize(nDepthCopy); orderBookData[3].resize(nDepthCopy); 591 | } 592 | 593 | updateOrderBookSides(orderBookSides, symbol, spotTrade, delimiter); 594 | // correct url has been traversed 595 | break; 596 | 597 | } 598 | curl_easy_cleanup(curl); 599 | } 600 | } 601 | 602 | 603 | /* 604 | * 605 | * Pull spot ticker data from Gate.io Exchange via API 606 | * and add data to Graph 607 | * 608 | */ 609 | void pullGateio(unordered_map > &symbolMap, Graph &g, bool setGraph, unordered_set &seenSymbols, mutex &seenSymbols_mutex) 610 | { 611 | CURL *curl; 612 | CURLcode res; 613 | string response; 614 | string exchange = "gateio"; 615 | double exchangeFee = 0.003; 616 | 617 | curl = curl_easy_init(); 618 | if (curl) 619 | { 620 | const char* exchangeURL = "https://api.gateio.ws/api/v4/spot/tickers"; 621 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 622 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 623 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 624 | res = curl_easy_perform(curl); 625 | 626 | // Check for errors 627 | if (res != CURLE_OK) 628 | { 629 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 630 | } 631 | 632 | try 633 | { 634 | nlohmann::json json_data = nlohmann::json::parse(response); 635 | 636 | for (auto& item : json_data) 637 | { 638 | // symbol comes in uppercase with the coins seperated by a hyphen 639 | string tradeSymbol = item["currency_pair"]; 640 | int delimeterPos = tradeSymbol.find("_"); 641 | tradeSymbol = tradeSymbol.substr(0, delimeterPos) + tradeSymbol.substr(delimeterPos+1); 642 | vector assets = symbolMap[tradeSymbol]; 643 | if (assets.size() != 2) 644 | continue; 645 | 646 | string fromAsset = assets[0], toAsset = assets[1]; 647 | string strBidPrice = item["highest_bid"], strAskPrice = item["lowest_ask"]; 648 | if ((strBidPrice == "") || (strAskPrice == "")) 649 | continue; 650 | long double bidPrice = stod(strBidPrice), askPrice = stod(strAskPrice); 651 | if ((bidPrice == 0.0) || (askPrice == 0.0)) 652 | continue; 653 | 654 | if (setGraph) 655 | { 656 | g.addEdge(fromAsset, toAsset, exchangeFee, exchange); 657 | // mark TradingPair as seen; symbolMap will be resized at set up with this information 658 | seenSymbols_mutex.lock(); 659 | seenSymbols.insert(tradeSymbol); 660 | seenSymbols.insert(toAsset + fromAsset); 661 | seenSymbols_mutex.unlock(); 662 | } 663 | else 664 | g.updateEdge(fromAsset, toAsset, bidPrice, askPrice, exchange); 665 | } 666 | } 667 | catch (const exception &e) 668 | { 669 | cout << exchange << " SSL EXCEPTION DETECTED" << endl; 670 | } 671 | 672 | curl_easy_cleanup(curl); 673 | } 674 | } 675 | 676 | 677 | /* 678 | * 679 | * Pull order book data from Gate.io Exchange via API 680 | * 681 | */ 682 | void pullGateioOrderBook(TrackProfit &spotTrade, vector> &orderBookData, vector &orderBookSides, int nDepth) 683 | { 684 | CURL *curl; 685 | CURLcode res; 686 | string response; 687 | vector symbolCombo; string delimiter = "_"; 688 | symbolCombo.push_back(spotTrade.to + "_" +spotTrade.from); symbolCombo.push_back(spotTrade.from + "_" + spotTrade.to); 689 | string baseURL = "https://api.gateio.ws/api/v4/spot/order_book", query, URL; 690 | 691 | curl = curl_easy_init(); 692 | if (curl) 693 | { 694 | for(string &symbol : symbolCombo) 695 | { 696 | query = "?currency_pair=" + symbol + "&limit=" + to_string(nDepth); 697 | URL = baseURL + query; 698 | const char* exchangeURL = URL.c_str(); 699 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 700 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 701 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 702 | res = curl_easy_perform(curl); 703 | 704 | // Check for errors 705 | if (res != CURLE_OK) 706 | { 707 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 708 | } 709 | 710 | nlohmann::json json_data = nlohmann::json::parse(response); 711 | // cout << "exchange: " << spotTrade.exchange << "response: " << response << endl; 712 | response.clear(); // Clear the response variable 713 | // invalid API request 714 | if (json_data.find("message") != json_data.end()) 715 | continue; 716 | 717 | int jsonDataBidCtn = json_data["bids"].size(); 718 | int jsonDataAskCtn = json_data["asks"].size(); 719 | int nDepthCopy = min(min(nDepth, jsonDataAskCtn), jsonDataBidCtn); 720 | 721 | // parse out each active order in orderbook 722 | for (int i = 0; i < nDepthCopy; i++) 723 | { 724 | string strBidPrice = json_data["bids"][i][0], strBidAmt = json_data["bids"][i][1]; 725 | double bidPrice = stod(strBidPrice), bidAmt = stod(strBidAmt); 726 | string strAskPrice = json_data["asks"][i][0], strAskAmt = json_data["asks"][i][1]; 727 | double askPrice = stod(strAskPrice), askAmt = stod(strAskAmt); 728 | orderBookData[0][i] = log(bidPrice); orderBookData[1][i] = bidAmt; 729 | orderBookData[2][i] = log(askPrice); orderBookData[3][i] = askAmt; 730 | } 731 | // resize the array when needed 732 | if (nDepthCopy != nDepth) 733 | { 734 | orderBookData[0].resize(nDepthCopy); orderBookData[1].resize(nDepthCopy); 735 | orderBookData[2].resize(nDepthCopy); orderBookData[3].resize(nDepthCopy); 736 | } 737 | 738 | updateOrderBookSides(orderBookSides, symbol, spotTrade, delimiter); 739 | // correct url has been traversed 740 | break; 741 | 742 | } 743 | curl_easy_cleanup(curl); 744 | } 745 | } 746 | 747 | 748 | /* 749 | * 750 | * Pull spot ticker data from Huobi Exchange via API 751 | * and add data to Graph 752 | * 753 | */ 754 | void pullHuobi(unordered_map > &symbolMap, Graph &g, bool setGraph, unordered_set &seenSymbols, mutex &seenSymbols_mutex) 755 | { 756 | CURL *curl; 757 | CURLcode res; 758 | string response; 759 | string exchange = "huobi"; 760 | double exchangeFee = 0.002; 761 | 762 | curl = curl_easy_init(); 763 | if (curl) 764 | { 765 | const char* exchangeURL = "https://api.huobi.pro/market/tickers"; 766 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 767 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 768 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 769 | res = curl_easy_perform(curl); 770 | 771 | // Check for errors 772 | if (res != CURLE_OK) 773 | { 774 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 775 | } 776 | 777 | try 778 | { 779 | nlohmann::json json_data = nlohmann::json::parse(response); 780 | for (auto& item : json_data["data"]) { 781 | // Huobi returns symbol name in all lower case where my hashmap is all uppercase 782 | string tradeSymbol = item["symbol"]; 783 | string tradeSymbolUpper; 784 | for (char c : tradeSymbol){ 785 | tradeSymbolUpper += toupper(c); 786 | } 787 | 788 | vector assets = symbolMap[tradeSymbolUpper]; 789 | if (assets.size() != 2) 790 | continue; 791 | 792 | string fromAsset = assets[0], toAsset = assets[1]; 793 | double bidPrice = item["bid"], askPrice = item["ask"]; 794 | if ((bidPrice == 0.0) || (askPrice == 0.0)){ 795 | continue; 796 | } 797 | if (setGraph){ 798 | g.addEdge(fromAsset, toAsset, exchangeFee, exchange); 799 | // Record what trading symbol was used to resize symbolMap later 800 | // mark TradingPair as seen; symbolMap will be resized at set up with this information 801 | seenSymbols_mutex.lock(); 802 | seenSymbols.insert(tradeSymbolUpper); 803 | seenSymbols.insert(toAsset + fromAsset); 804 | seenSymbols_mutex.unlock(); 805 | } 806 | else 807 | { 808 | g.updateEdge(fromAsset, toAsset, bidPrice, askPrice, exchange); 809 | } 810 | 811 | } 812 | } 813 | catch (const exception &e) 814 | { 815 | cout << exchange << " SSL EXCEPTION DETECTED" << endl; 816 | } 817 | 818 | curl_easy_cleanup(curl); 819 | } 820 | } 821 | 822 | 823 | /* 824 | * 825 | * Pull order book data from Huobi Exchange via API 826 | * 827 | */ 828 | void pullHuobiOrderBook(TrackProfit &spotTrade, vector> &orderBookData, vector &orderBookSides, int nDepth) 829 | { 830 | CURL *curl; 831 | CURLcode res; 832 | string response; 833 | vector symbolCombo; string delimiter = ""; 834 | symbolCombo.push_back(toLowerCase(spotTrade.to + spotTrade.from)); 835 | symbolCombo.push_back(toLowerCase(spotTrade.from + spotTrade.to)); 836 | string baseURL = "https://api.huobi.pro/market/depth?", query, URL; 837 | 838 | curl = curl_easy_init(); 839 | if (curl) 840 | { 841 | for(string &symbol : symbolCombo) 842 | { 843 | query = "symbol=" + symbol + "&type=step0"; 844 | URL = baseURL + query; 845 | const char* exchangeURL = URL.c_str(); 846 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 847 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 848 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 849 | res = curl_easy_perform(curl); 850 | 851 | // Check for errors 852 | if (res != CURLE_OK) 853 | { 854 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 855 | } 856 | 857 | nlohmann::json json_data = nlohmann::json::parse(response); 858 | // cout << "exchange: " << spotTrade.exchange << "response: " << response << endl; 859 | response.clear(); // Clear the response variable 860 | 861 | // invalid API request 862 | if (json_data.find("err-msg") != json_data.end()) 863 | continue; 864 | 865 | int jsonDataBidCtn = json_data["tick"]["bids"].size(); 866 | int jsonDataAskCtn = json_data["tick"]["asks"].size(); 867 | int nDepthCopy = min(min(nDepth, jsonDataAskCtn), jsonDataBidCtn); 868 | 869 | // parse out each active order in orderbook 870 | for (int i = 0; i < nDepthCopy; i++) 871 | { 872 | double bidPrice = json_data["tick"]["bids"][i][0], bidAmt = json_data["tick"]["bids"][i][1]; 873 | double askPrice = json_data["tick"]["asks"][i][0], askAmt = json_data["tick"]["asks"][i][1]; 874 | orderBookData[0][i] = log(bidPrice); orderBookData[1][i] = bidAmt; 875 | orderBookData[2][i] = log(askPrice); orderBookData[3][i] = askAmt; 876 | } 877 | // resize the array when needed 878 | if (nDepthCopy != nDepth) 879 | { 880 | orderBookData[0].resize(nDepthCopy); orderBookData[1].resize(nDepthCopy); 881 | orderBookData[2].resize(nDepthCopy); orderBookData[3].resize(nDepthCopy); 882 | } 883 | 884 | updateOrderBookSides(orderBookSides, symbol, spotTrade, delimiter); 885 | // correct url has been traversed 886 | break; 887 | 888 | } 889 | curl_easy_cleanup(curl); 890 | } 891 | } 892 | 893 | /* 894 | * 895 | * Pull spot ticker data from Kucoin Exchange via API 896 | * and add data to Graph 897 | * 898 | */ 899 | void pullKucoin(unordered_map > &symbolMap, Graph &g, bool setGraph, unordered_set &seenSymbols, mutex &seenSymbols_mutex) 900 | { 901 | CURL *curl; 902 | CURLcode res; 903 | string response; 904 | string exchange = "kucoin"; 905 | double exchangeFee; 906 | 907 | curl = curl_easy_init(); 908 | if (curl) 909 | { 910 | const char* exchangeURL = "https://api.kucoin.com/api/v1/market/allTickers"; 911 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL ); 912 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 913 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 914 | res = curl_easy_perform(curl); 915 | 916 | // Check for errors 917 | if (res != CURLE_OK) 918 | { 919 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 920 | } 921 | 922 | // TODO: Test to make sure something correct is returned from API call aka on the right VPN 923 | try 924 | { 925 | nlohmann::json json_data = nlohmann::json::parse(response); 926 | for (auto& item : json_data["data"]["ticker"]) { 927 | // symbol comes in uppercase with the coins seperated by a hyphen 928 | string tradeSymbol = item["symbol"]; 929 | int delimeterPos = tradeSymbol.find("-"); 930 | tradeSymbol = tradeSymbol.substr(0, delimeterPos) + tradeSymbol.substr(delimeterPos+1); 931 | vector assets = symbolMap[tradeSymbol]; 932 | if (assets.size() != 2) 933 | continue; 934 | 935 | string fromAsset = assets[0], toAsset = assets[1]; 936 | string strBidPrice = item["buy"], strAskPrice = item["sell"]; 937 | long double bidPrice = stod(strBidPrice), askPrice = stod(strAskPrice); 938 | if ((bidPrice == 0.0) || (askPrice == 0.0)){ 939 | continue; 940 | } 941 | 942 | if (setGraph){ 943 | string strTakerFee = item["takerFeeRate"], strMakerFee = item["makerFeeRate"]; 944 | exchangeFee = stod(strTakerFee) + stod(strMakerFee); 945 | g.addEdge(fromAsset, toAsset, exchangeFee, exchange); 946 | // Record what trading symbol was used to resize symbolMap later 947 | // mark TradingPair as seen; symbolMap will be resized at set up with this information 948 | seenSymbols_mutex.lock(); 949 | seenSymbols.insert(tradeSymbol); 950 | seenSymbols.insert(toAsset + fromAsset); 951 | seenSymbols_mutex.unlock(); 952 | } 953 | else{ 954 | g.updateEdge(fromAsset, toAsset, bidPrice, askPrice, exchange); 955 | } 956 | } 957 | } 958 | catch (const exception &e) 959 | { 960 | cout << exchange << " SSL EXCEPTION DETECTED" << endl; 961 | } 962 | 963 | curl_easy_cleanup(curl); 964 | } 965 | } 966 | 967 | 968 | /* 969 | * 970 | * Pull order book data from Kucoin Exchange via API 971 | * 972 | */ 973 | void pullKucoinOrderBook(TrackProfit &spotTrade, vector> &orderBookData, vector &orderBookSides, int nDepth) 974 | { 975 | CURL *curl; 976 | CURLcode res; 977 | string response; 978 | vector symbolCombo; string delimiter = "-"; 979 | symbolCombo.push_back(spotTrade.to + "-" + spotTrade.from); 980 | symbolCombo.push_back(spotTrade.from + "-" + spotTrade.to); 981 | string baseURL = "https://api.kucoin.com/api/v1/market/orderbook/level2_100", query, URL; 982 | 983 | 984 | curl = curl_easy_init(); 985 | if (curl) 986 | { 987 | for(string &symbol : symbolCombo) 988 | { 989 | query = "?symbol=" + symbol; 990 | URL = baseURL + query; 991 | const char* exchangeURL = URL.c_str(); 992 | curl_easy_setopt(curl, CURLOPT_URL, exchangeURL); 993 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); 994 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); 995 | res = curl_easy_perform(curl); 996 | 997 | // Check for errors 998 | if (res != CURLE_OK) 999 | { 1000 | cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << endl; 1001 | } 1002 | 1003 | nlohmann::json json_data = nlohmann::json::parse(response); 1004 | // cout << "exchange: " << spotTrade.exchange << "response: " << response << endl; 1005 | response.clear(); // Clear the response variable 1006 | 1007 | // invalid API request 1008 | if (json_data["data"]["time"] == 0) 1009 | { 1010 | continue; 1011 | } 1012 | 1013 | int jsonDataBidCtn = json_data["data"]["bids"].size(); 1014 | int jsonDataAskCtn = json_data["data"]["asks"].size(); 1015 | int nDepthCopy = min(min(nDepth, jsonDataAskCtn), jsonDataBidCtn); 1016 | 1017 | // parse out each active order in orderbook 1018 | for (int i = 0; i < nDepthCopy; i++) 1019 | { 1020 | string strBidPrice = json_data["data"]["bids"][i][0], strBidAmt = json_data["data"]["bids"][i][1]; 1021 | double bidPrice = stod(strBidPrice), bidAmt = stod(strBidAmt); 1022 | string strAskPrice = json_data["data"]["asks"][i][0], strAskAmt = json_data["data"]["asks"][i][1]; 1023 | double askPrice = stod(strAskPrice), askAmt = stod(strAskAmt); 1024 | orderBookData[0][i] = log(bidPrice); orderBookData[1][i] = bidAmt; 1025 | orderBookData[2][i] = log(askPrice); orderBookData[3][i] = askAmt; 1026 | } 1027 | // resize the array when needed 1028 | if (nDepthCopy != nDepth) 1029 | { 1030 | orderBookData[0].resize(nDepthCopy); orderBookData[1].resize(nDepthCopy); 1031 | orderBookData[2].resize(nDepthCopy); orderBookData[3].resize(nDepthCopy); 1032 | } 1033 | 1034 | updateOrderBookSides(orderBookSides, symbol, spotTrade, delimiter); 1035 | // correct url has been traversed 1036 | break; 1037 | 1038 | } 1039 | curl_easy_cleanup(curl); 1040 | } 1041 | } 1042 | 1043 | /* 1044 | * 1045 | * Parallel pull spot ticker data for all coins on all exchanges currently 1046 | * supported in this framework via API 1047 | * 1048 | */ 1049 | void pullAllTicker(unordered_map > &symbolMap, Graph &g, bool setGraph, unordered_set &seenSymbols, unordered_set ignoreExchanges) { 1050 | mutex seenSymbols_Mutex; 1051 | vector threads; 1052 | if (ignoreExchanges.find("binance") == ignoreExchanges.end()) 1053 | threads.push_back(thread(pullBinance, ref(symbolMap), ref(g), ref(setGraph), ref(seenSymbols), ref(seenSymbols_Mutex))); 1054 | 1055 | if (ignoreExchanges.find("bitget") == ignoreExchanges.end()) 1056 | threads.push_back(thread(pullBitget, ref(symbolMap), ref(g), ref(setGraph), ref(seenSymbols), ref(seenSymbols_Mutex))); 1057 | 1058 | if (ignoreExchanges.find("bitmart") == ignoreExchanges.end()) 1059 | threads.push_back(thread(pullBitMart, ref(symbolMap), ref(g), ref(setGraph), ref(seenSymbols), ref(seenSymbols_Mutex))); 1060 | 1061 | if (ignoreExchanges.find("gateio") == ignoreExchanges.end()) 1062 | threads.push_back(thread(pullGateio, ref(symbolMap), ref(g), ref(setGraph), ref(seenSymbols), ref(seenSymbols_Mutex))); 1063 | 1064 | if (ignoreExchanges.find("kucoin") == ignoreExchanges.end()) 1065 | threads.push_back(thread(pullKucoin, ref(symbolMap), ref(g), ref(setGraph), ref(seenSymbols), ref(seenSymbols_Mutex))); 1066 | 1067 | if (ignoreExchanges.find("huobi") == ignoreExchanges.end()) 1068 | threads.push_back(thread(pullHuobi, ref(symbolMap), ref(g), ref(setGraph), ref(seenSymbols), ref(seenSymbols_Mutex))); 1069 | 1070 | for (auto &thread : threads) { 1071 | thread.join(); 1072 | } 1073 | } 1074 | 1075 | 1076 | /* 1077 | * 1078 | * Parallel pull orderbook data for all trading pairs that 1079 | * have been identified as a profitable by arbitrage finder 1080 | * 1081 | */ 1082 | void pullAllOrderBook(vector &arbPath, vector>> &orderBookData, vector> &orderBookSides, int &nDepth) { 1083 | vector threads; 1084 | for (int i = 0; i < arbPath.size(); i++) 1085 | { 1086 | if (arbPath[i].exchange == "binance") 1087 | threads.push_back(thread(pullBinanceOrderBook, ref(arbPath[i]), ref(orderBookData[i]), ref(orderBookSides[i]), ref(nDepth))); 1088 | else if (arbPath[i].exchange == "bitget") 1089 | threads.push_back(thread(pullBitgetOrderBook, ref(arbPath[i]), ref(orderBookData[i]), ref(orderBookSides[i]), ref(nDepth))); 1090 | else if (arbPath[i].exchange == "bitmart") 1091 | threads.push_back(thread(pullBitMartOrderBook, ref(arbPath[i]), ref(orderBookData[i]), ref(orderBookSides[i]), ref(nDepth))); 1092 | else if (arbPath[i].exchange == "gateio") 1093 | threads.push_back(thread(pullGateioOrderBook, ref(arbPath[i]), ref(orderBookData[i]), ref(orderBookSides[i]), ref(nDepth))); 1094 | else if (arbPath[i].exchange == "kucoin") 1095 | threads.push_back(thread(pullKucoinOrderBook, ref(arbPath[i]), ref(orderBookData[i]), ref(orderBookSides[i]), ref(nDepth))); 1096 | else if (arbPath[i].exchange == "huobi") 1097 | threads.push_back(thread(pullHuobiOrderBook, ref(arbPath[i]), ref(orderBookData[i]), ref(orderBookSides[i]), ref(nDepth))); 1098 | } 1099 | for (auto &thread : threads) { 1100 | thread.join(); 1101 | } 1102 | } 1103 | 1104 | 1105 | --------------------------------------------------------------------------------