├── .gitattributes ├── utils ├── banner.jpg ├── graph_BTC_buy_conditions.png ├── stats_and_plots.py ├── timing.py ├── misc.py ├── trade_strategies.py ├── exchange.py ├── mail_notifier.py └── mail_template │ ├── info.html │ ├── insufficientFundsWarning.html │ ├── critical.html │ ├── error.html │ └── success.html ├── trades_example ├── graph_BTC.png ├── graph_LTC.png ├── graph_XRP.png ├── next_purchases.csv ├── stats.csv ├── orders.csv ├── log.txt └── orders.json ├── .gitignore ├── auth └── API_keys_example.yml ├── config └── config_example.yml ├── README.md └── dca_bot.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-detectable=false -------------------------------------------------------------------------------- /utils/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingCryptoTrading/dca-crypto-bot/HEAD/utils/banner.jpg -------------------------------------------------------------------------------- /trades_example/graph_BTC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingCryptoTrading/dca-crypto-bot/HEAD/trades_example/graph_BTC.png -------------------------------------------------------------------------------- /trades_example/graph_LTC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingCryptoTrading/dca-crypto-bot/HEAD/trades_example/graph_LTC.png -------------------------------------------------------------------------------- /trades_example/graph_XRP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingCryptoTrading/dca-crypto-bot/HEAD/trades_example/graph_XRP.png -------------------------------------------------------------------------------- /utils/graph_BTC_buy_conditions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodingCryptoTrading/dca-crypto-bot/HEAD/utils/graph_BTC_buy_conditions.png -------------------------------------------------------------------------------- /trades_example/next_purchases.csv: -------------------------------------------------------------------------------- 1 | Coin,Purchase Time,Cycle 2 | BTC,2022-05-01 20:27:10.869433,minutely 3 | XRP,2022-05-01 20:27:10.869433,minutely 4 | LTC,2022-05-01 20:27:10.869433,minutely 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | auth/API_keys.yml 2 | config/config.yml 3 | trades/ 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | .idea -------------------------------------------------------------------------------- /trades_example/stats.csv: -------------------------------------------------------------------------------- 1 | Coin,N,Quantity,AvgPrice,TotalCost,ROI,ROI% 2 | BTC,7.0,0.004571,38255.22592850447,174.86452258,-0.13694420227873214,-0.0783144575344496 3 | XRP,7.0,137.2,0.7634000000000002,104.73847999999998,-3.0464519795714287e-14,-2.908627258645943e-14 4 | LTC,7.0,1.7394999999999998,100.59999999999998,174.9937,2.4719781777093883e-14,1.4126098126443344e-14 5 | -------------------------------------------------------------------------------- /auth/API_keys_example.yml: -------------------------------------------------------------------------------- 1 | BINANCE: 2 | TEST: # https://testnet.binance.vision 3 | APIKEY: 'place here your API key' 4 | SECRET: 'place here your secret' 5 | REAL: 6 | APIKEY: 'place here your API key' 7 | SECRET: 'place here your secret' 8 | 9 | FTX: 10 | REAL: 11 | APIKEY: 'place here your API key' 12 | SECRET: 'place here your secret' 13 | 14 | # Some exchanges may require an additional parameter, e.g: 15 | COINBASEPRO: 16 | REAL: 17 | APIKEY: 'place here your API key' 18 | SECRET: 'place here your secret' 19 | PASSPHRASE: 'place here your passphrase (sometimes called password)' 20 | 21 | KUCOIN: 22 | REAL: 23 | APIKEY: 'place here your API key' 24 | SECRET: 'place here your secret' 25 | PASSPHRASE: 'place here your passphrase (sometimes called password)' 26 | 27 | 28 | -------------------------------------------------------------------------------- /config/config_example.yml: -------------------------------------------------------------------------------- 1 | COINS: # List of coins to buy. These are just examples mainly for the test mode. For real purchases, make your own list! 2 | BTC: 3 | PAIRING: USDT # Choose the currency to use to buy the coin 4 | AMOUNT: 25 # Quantity to buy in your pairing currency 5 | CYCLE: 'daily' # Recurring Cycle can be: 'daily', 'weekly', 'bi-weekly', 'monthly' 6 | ON_WEEKDAY: 6 # [only for weekly and bi-weekly] Repeats on 0-6 (0=Monday...6=Sunday) 7 | ON_DAY: 1 # [only for monthly]. Date of the month [1-28] 8 | AT_TIME: '19:30' # Format: 0 <= hour <= 23, 0 <= minute <= 59 9 | XRP: 10 | PAIRING: BUSD # Choose the currency to use to buy the coin 11 | AMOUNT: 15 # Quantity to buy in your pairing currency 12 | CYCLE: 'weekly' # Recurring Cycle can be: 'daily', 'weekly', 'bi-weekly', 'monthly' 13 | ON_WEEKDAY: 3 # [only for weekly and bi-weekly] Repeats on 0-6 (0=Monday...6=Sunday) 14 | AT_TIME: '08:00' # Format: 0 <= hour <= 23, 0 <= minute <= 59 15 | LTC: 16 | PAIRING: BUSD # Choose the currency to use to buy the coin 17 | AMOUNT: 25 # Quantity to buy in your pairing currency 18 | CYCLE: 'bi-weekly' # Recurring Cycle can be: 'daily', 'weekly', 'bi-weekly', 'monthly' 19 | ON_WEEKDAY: 3 # [only for weekly and bi-weekly] Repeats on 0-6 (0=Monday...6=Sunday) 20 | AT_TIME: '20:00' # Format: 0 <= hour <= 23, 0 <= minute <= 59 21 | 22 | ### Exchange section ### 23 | EXCHANGE: 'binance' 24 | TEST: True 25 | 26 | ### Notification section ### 27 | SEND_NOTIFICATIONS: True 28 | 29 | # email sender info 30 | EMAIL_ADDRESS_FROM: 'sender@email.com' 31 | EMAIL_PASSWORD: 'sender email password' 32 | SMTP_SERVER: 'smtp.mail.yahoo.com' # sender email SMTP server 33 | 34 | # email recipient 35 | EMAIL_ADDRESS_TO: 'recipient@email.com' -------------------------------------------------------------------------------- /utils/stats_and_plots.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from utils.misc import * 3 | 4 | 5 | def plot_purchases(coin, df_orders, pairing): 6 | prices = df_orders['price'][df_orders['coin'] == coin].values 7 | costs = df_orders['cost'][df_orders['coin'] == coin].values 8 | # Calculate the weighted average 9 | avg = (prices * costs).sum() / costs.sum() 10 | 11 | plt.rcParams.update({'font.size': 12}) 12 | 13 | fig, ax = plt.subplots() 14 | 15 | color_text_lines = '#6c6c72' 16 | 17 | plt.plot(prices, '-o', color='#6f7a8f', mfc='#d0aa93', linewidth=0.4, markersize=9) 18 | plt.axhline(y=avg, color='#da6517', linestyle='-', linewidth=0.8) 19 | 20 | plt.title(f'{coin} | Average {round_price(avg)} {pairing}', 21 | color=color_text_lines) 22 | ax.ticklabel_format(useOffset=False, style='plain') 23 | plt.ylabel(pairing, color=color_text_lines) 24 | plt.xlabel('Purchases', color=color_text_lines) 25 | plt.xticks([], []) 26 | 27 | # ax.grid('on', linestyle='--', linewidth=0.5, alpha = 0.5) 28 | ax.xaxis.set_tick_params(size=0) 29 | ax.yaxis.set_tick_params(size=0) 30 | ax.tick_params(axis='y', colors=color_text_lines) 31 | 32 | 33 | # ax.spines['bottom'].set_color('#6c6c72') 34 | # ax.spines['left'].set_color('#6c6c72') 35 | 36 | ax.spines['right'].set_visible(False) 37 | ax.spines['top'].set_visible(False) 38 | 39 | plt.tight_layout() 40 | 41 | plt.savefig(f'trades/graph_{coin}.png') 42 | plt.close() 43 | 44 | 45 | def calculate_stats(coin, df_orders, df_stats, stats_path): 46 | ''' 47 | Given the df_orders calculate stats and append to df_stats, 48 | also, save to disk the stat df 49 | ''' 50 | prices = df_orders['price'][df_orders['coin'] == coin].values 51 | costs = df_orders['cost'][df_orders['coin'] == coin].values 52 | # fees = df_orders['fee'][df_orders['coin'] == coin].values 53 | 54 | # Calculate the weighted average 55 | avg = (prices * costs).sum() / costs.sum() 56 | # Total asset accumulated 57 | fills = df_orders['filled'][df_orders['coin'] == coin].values 58 | total_asset = fills.sum() 59 | # Total cost: 60 | total_cost = costs.sum() 61 | 62 | # ROI 63 | roi = 100 * (prices[-1] - avg) / prices[-1] 64 | # Gain/Loss: 65 | gain = roi * total_cost / 100 66 | 67 | df_stats.loc[coin] = [len(prices), total_asset, avg, total_cost, gain, roi] 68 | # save stats to disk 69 | df_stats.to_csv(stats_path) 70 | return df_stats 71 | -------------------------------------------------------------------------------- /utils/timing.py: -------------------------------------------------------------------------------- 1 | 2 | def get_on_weekday(x): 3 | """ 4 | Return on_weekday after checking it is in the right interval (0-6) 5 | """ 6 | x = int(x) # just in case a string was entered 7 | if x in [0,1,2,3,4,5,6]: 8 | return x 9 | else: 10 | raise Exception('ON_WEEKDAY should range from 0 to 6') 11 | 12 | 13 | def get_on_day(x): 14 | """ 15 | Return on_weekday after checking it is in the right interval (0-6) 16 | """ 17 | x = int(x) # just in case a string was entered 18 | if x in list(range(1,29)): 19 | return x 20 | else: 21 | raise Exception('ON_DAT should range from 1 to 28') 22 | 23 | 24 | def get_hour_minute(at_time): 25 | """ 26 | Convert the AT_TIME config variable from string to a couple of integers (hour, minutes) 27 | """ 28 | if isinstance(at_time, int): 29 | # in this case assume there are no minutes: 30 | hours = at_time 31 | minutes = 0 32 | elif isinstance(at_time, str): 33 | # convert to integers: 34 | at_time = [int(x) for x in at_time.split(':')] 35 | if len(at_time) == 1: 36 | hours = at_time[0] 37 | minutes = 0 38 | else: 39 | hours = at_time[0] 40 | minutes = at_time[1] 41 | if hours > 23 or minutes > 59: 42 | raise Exception('AT_HOUR should have hours ranging from 0 to 23 and minutes ranging from 0 to 59') 43 | return [hours, minutes] 44 | 45 | 46 | def retry_info(): 47 | retry_for_funds = {} # for funds error (insufficient balance) 48 | retry_for_network = {} # generic network error 49 | 50 | # first element is the maximum number of attempts, second element is the waiting time in seconds 51 | retry_for_funds['minutely'] = [3, 10] # for testing purpose 52 | retry_for_funds['daily'] = [1, 12*60*60] # check only once after 12 hours 53 | retry_for_funds['weekly'] = [2, 24*60*60] 54 | retry_for_funds['bi-weekly'] = [2, 24*60*60] 55 | retry_for_funds['monthly'] = [3, 24*60*60] 56 | 57 | # first element is the maximum number of attempts, second element is the waiting time in seconds 58 | retry_for_network['minutely'] = [3, 10] # for testing purpose 59 | retry_for_network['daily'] = [12, 1*60*60] # check every hour for the next 12 hours 60 | retry_for_network['weekly'] = [24, 1*60*60] 61 | retry_for_network['bi-weekly'] = [24, 1*60*60] 62 | retry_for_network['monthly'] = [24, 1*60*60] 63 | 64 | return retry_for_funds, retry_for_network 65 | -------------------------------------------------------------------------------- /trades_example/orders.csv: -------------------------------------------------------------------------------- 1 | N,datetime (local),datetime (exchange),timestamp,coin,symbol,status,filled,price,cost,remaining,fee,fee currency,fee rate 2 | 0,2022-05-01T20:20:14,2022-05-01T18:20:14.545Z,1651429214545,BTC,BTC/USDT,closed,0.000652,38326.39,24.98880628,0.0,0.0,BTC,N.A. 3 | 1,2022-05-01T20:20:17,2022-05-01T18:20:17.912Z,1651429217912,XRP,XRP/BUSD,closed,19.6,0.7634,14.96264,0.0,0.0,XRP,N.A. 4 | 2,2022-05-01T20:20:21,2022-05-01T18:20:21.804Z,1651429221804,LTC,LTC/BUSD,closed,0.2485,100.6,24.9991,0.0,0.0,LTC,N.A. 5 | 3,2022-05-01T20:21:11,2022-05-01T18:21:11.674Z,1651429271674,BTC,BTC/USDT,closed,0.000653,38252.67,24.97899351,0.0,0.0,BTC,N.A. 6 | 4,2022-05-01T20:21:14,2022-05-01T18:21:15.046Z,1651429275046,XRP,XRP/BUSD,closed,19.6,0.7634,14.96264,0.0,0.0,XRP,N.A. 7 | 5,2022-05-01T20:21:18,2022-05-01T18:21:18.792Z,1651429278792,LTC,LTC/BUSD,closed,0.2485,100.6,24.9991,0.0,0.0,LTC,N.A. 8 | 6,2022-05-01T20:22:11,2022-05-01T18:22:11.654Z,1651429331654,BTC,BTC/USDT,closed,0.000653,38255.12,24.98059336,0.0,0.0,BTC,N.A. 9 | 7,2022-05-01T20:22:14,2022-05-01T18:22:15.023Z,1651429335023,XRP,XRP/BUSD,closed,19.6,0.7634,14.96264,0.0,0.0,XRP,N.A. 10 | 8,2022-05-01T20:22:18,2022-05-01T18:22:18.681Z,1651429338681,LTC,LTC/BUSD,closed,0.2485,100.6,24.9991,0.0,0.0,LTC,N.A. 11 | 9,2022-05-01T20:23:11,2022-05-01T18:23:11.674Z,1651429391674,BTC,BTC/USDT,closed,0.000653,38240.61,24.97111833,0.0,0.0,BTC,N.A. 12 | 10,2022-05-01T20:23:15,2022-05-01T18:23:15.490Z,1651429395490,XRP,XRP/BUSD,closed,19.6,0.7634,14.96264,0.0,0.0,XRP,N.A. 13 | 11,2022-05-01T20:23:19,2022-05-01T18:23:19.317Z,1651429399317,LTC,LTC/BUSD,closed,0.2485,100.6,24.9991,0.0,0.0,LTC,N.A. 14 | 12,2022-05-01T20:24:11,2022-05-01T18:24:11.656Z,1651429451656,BTC,BTC/USDT,closed,0.000653,38255.15,24.98061295,0.0,0.0,BTC,N.A. 15 | 13,2022-05-01T20:24:14,2022-05-01T18:24:14.828Z,1651429454828,XRP,XRP/BUSD,closed,19.6,0.7634,14.96264,0.0,0.0,XRP,N.A. 16 | 14,2022-05-01T20:24:18,2022-05-01T18:24:18.087Z,1651429458087,LTC,LTC/BUSD,closed,0.2485,100.6,24.9991,0.0,0.0,LTC,N.A. 17 | 15,2022-05-01T20:25:11,2022-05-01T18:25:11.659Z,1651429511659,BTC,BTC/USDT,closed,0.000653,38231.33,24.96505849,0.0,0.0,BTC,N.A. 18 | 16,2022-05-01T20:25:14,2022-05-01T18:25:14.919Z,1651429514919,XRP,XRP/BUSD,closed,19.6,0.7634,14.96264,0.0,0.0,XRP,N.A. 19 | 17,2022-05-01T20:25:17,2022-05-01T18:25:17.945Z,1651429517945,LTC,LTC/BUSD,closed,0.2485,100.6,24.9991,0.0,0.0,LTC,N.A. 20 | 18,2022-05-01T20:26:11,2022-05-01T18:26:11.220Z,1651429571220,BTC,BTC/USDT,closed,0.000654,38225.29,24.99933966,0.0,0.0,BTC,N.A. 21 | 19,2022-05-01T20:26:14,2022-05-01T18:26:14.231Z,1651429574231,XRP,XRP/BUSD,closed,19.6,0.7634,14.96264,0.0,0.0,XRP,N.A. 22 | 20,2022-05-01T20:26:17,2022-05-01T18:26:17.262Z,1651429577262,LTC,LTC/BUSD,closed,0.2485,100.6,24.9991,0.0,0.0,LTC,N.A. 23 | -------------------------------------------------------------------------------- /utils/misc.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys, os 3 | import json 4 | import yaml 5 | import pandas as pd 6 | 7 | 8 | def load_config(file): 9 | with open(file) as file: 10 | return yaml.load(file, Loader=yaml.FullLoader) 11 | 12 | 13 | def register_logger(log_file=None, stdout=True): 14 | log = logging.getLogger() # root logger 15 | for hdlr in log.handlers[:]: # remove all old handlers 16 | log.removeHandler(hdlr) 17 | 18 | handlers = [] 19 | 20 | formatter = logging.Formatter("%(asctime)s %(levelname)s%(message)s", 21 | "%Y-%m-%d %H:%M:%S") 22 | 23 | if stdout: 24 | handlers.append(logging.StreamHandler(stream=sys.stdout)) 25 | 26 | if log_file is not None: 27 | handlers.append(logging.FileHandler(log_file)) 28 | 29 | for h in handlers: 30 | h.setFormatter(formatter) 31 | 32 | logging.basicConfig(handlers=handlers) 33 | 34 | logging.addLevelName(logging.INFO, '') 35 | logging.addLevelName(logging.ERROR, 'ERROR ') 36 | logging.addLevelName(logging.WARNING, 'WARNING ') 37 | 38 | logging.root.setLevel(logging.INFO) 39 | 40 | 41 | def read_csv_custom(filepath): 42 | try: 43 | # we had to add engine since on PI was giving segmentation fault 44 | df = pd.read_csv(filepath, index_col=0, engine='python') 45 | except: 46 | df = pd.read_csv(filepath, index_col=0) 47 | return df 48 | 49 | 50 | def store_json_order(filename, order): 51 | """ 52 | Save order into local json file 53 | """ 54 | if not os.path.exists(filename): 55 | data = [] 56 | else: 57 | # 1. Read file contents 58 | with open(filename, "r") as file: 59 | data = json.load(file) 60 | # 2. Update json object 61 | data.append(order) 62 | 63 | # 3. Write json file 64 | with open(filename, "w") as file: 65 | json.dump(data, file, indent=4) 66 | 67 | 68 | def are_you_sure_to_continue(): 69 | while True: 70 | query = input('You are going to make real purchases, do you want to continue? [y/n]: ') 71 | Fl = query[0].lower() 72 | if query == '' or not Fl in ['y', 'n', 'yes', 'no']: 73 | print('Please answer with yes or no!') 74 | else: 75 | break 76 | if Fl == 'y': 77 | return 78 | if Fl == 'n': 79 | quit() 80 | 81 | 82 | def float_to_float_sf(x, sf=3): 83 | """ 84 | Converts float to string with one significant figure 85 | while refraining from scientific notation 86 | 87 | inputs: 88 | x: input float to be converted to string (float) 89 | sf: significant_figures 90 | """ 91 | 92 | import numpy as np 93 | 94 | # Get decimal exponent of input float 95 | exp = int(f"{x:e}".split("e")[1]) 96 | 97 | # Get rid of all digits after the first significant figure 98 | x_fsf = round(round(x*10**-exp, sf-1) * 10**exp, 12) 99 | 100 | # Get rid of scientific notation and convert to string 101 | x_str = np.format_float_positional(x_fsf) 102 | 103 | # Return string output 104 | return x_str 105 | 106 | 107 | def round_price(x): 108 | if x == 0: 109 | x = '0' 110 | elif abs(x) < 1: 111 | # use 3 significant digits: 112 | x = float_to_float_sf(x, sf=3) 113 | else: 114 | x = '{:.2f}'.format(x) 115 | return x 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /utils/trade_strategies.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import logging 3 | import matplotlib.pyplot as plt 4 | 5 | class PriceMapper(object): 6 | def __init__(self, amount_range, price_range, mapping_function, coin, pairing, n_points=1000): 7 | 8 | self.amounts = amount_range 9 | self.prices = price_range 10 | self.function = mapping_function 11 | self.coin = coin 12 | self.pairing = pairing 13 | 14 | self.check_inputs() 15 | 16 | self.n_points = n_points 17 | 18 | def check_inputs(self): 19 | 20 | if len(self.prices) != 2: 21 | error_string = 'PRICE_RANGE should be a list of 2 elements: min and max price.' 22 | raise Exception(error_string) 23 | if len(self.amounts) != 2: 24 | error_string = 'AMOUNT should be a list of 2 elements: min and max amount.' 25 | raise Exception(error_string) 26 | if self.function not in ['linear', 'exponential', 'constant']: 27 | error_string = 'Valid mapping functions are: "linear" and "exponential".' 28 | raise Exception(error_string) 29 | 30 | 31 | def plot(self): 32 | # increase upper limit to show in the graph 33 | upperlimit_price = self.prices[1] + 0.1*self.prices[1] 34 | upperlimit_amount = self.amounts[1] + 0.1 * self.amounts[1] 35 | x = np.linspace(0,upperlimit_price, num=self.n_points) 36 | y = [] 37 | for i in x: 38 | y.append(self.get_amount(i)) 39 | 40 | plt.rcParams.update({'font.size': 12}) 41 | 42 | fig, ax = plt.subplots() 43 | 44 | color_text_lines = '#6c6c72' 45 | 46 | plt.axvline(x=self.prices[1], color='#da6517', linestyle='--', linewidth=0.8) 47 | plt.plot(x,y, '-', color='#6f7a8f', linewidth=1.1) 48 | 49 | 50 | plt.title(f'{self.coin} | Buy Conditions', 51 | color=color_text_lines) 52 | ax.ticklabel_format(useOffset=False, style='plain') 53 | plt.ylabel(f"Amount {self.pairing}", color=color_text_lines) 54 | plt.xlabel(f"Price {self.coin}", color=color_text_lines) 55 | 56 | 57 | ax.grid('on', linestyle='--', linewidth=0.5, alpha = 0.5) 58 | ax.xaxis.set_tick_params(size=0) 59 | ax.yaxis.set_tick_params(size=0) 60 | ax.tick_params(axis='y', colors=color_text_lines) 61 | ax.tick_params(axis='x', colors=color_text_lines) 62 | 63 | # ax.spines['bottom'].set_color('#6c6c72') 64 | # ax.spines['left'].set_color('#6c6c72') 65 | 66 | ax.spines['right'].set_visible(False) 67 | ax.spines['top'].set_visible(False) 68 | 69 | plt.xlim([0, upperlimit_price]) 70 | plt.ylim([0, upperlimit_amount]) 71 | 72 | leg = plt.legend(['Buy limit', 'Amount']) 73 | leg.get_frame().set_linewidth(0.0) 74 | for text in leg.get_texts(): 75 | text.set_color(color_text_lines) 76 | 77 | plt.tight_layout() 78 | 79 | plt.savefig(f'trades/graph_{self.coin}_buy_conditions.png') 80 | plt.close() 81 | 82 | 83 | def get_amount(self, price): 84 | if self.function == 'linear': 85 | return self.linear(price) 86 | elif self.function == 'exponential': 87 | return self.exponential(price) 88 | elif self.function == 'constant': 89 | return self.constant(price) 90 | else: 91 | error_string = 'Unrecognized mapping function' 92 | logging.error(error_string) 93 | raise Exception(error_string) 94 | 95 | def linear(self, price): 96 | # storing variables in letters for readability 97 | A = self.amounts[0] 98 | B = self.amounts[1] 99 | 100 | C = self.prices[0] 101 | D = self.prices[1] 102 | 103 | if price < C: 104 | amount = B 105 | elif price > D: 106 | amount = 0 107 | else: 108 | amount = B + ( (B - A) / (D - C)) * (C - price) 109 | return amount 110 | 111 | def exponential(self, price): 112 | # storing variables in letters for readability 113 | A = self.amounts[0] 114 | B = self.amounts[1] 115 | 116 | C = self.prices[0] 117 | D = self.prices[1] 118 | 119 | r = (np.log(A) - np.log(B)) / ( D - C) 120 | k = A * np.exp(- D * r) 121 | 122 | if price < C: 123 | amount = B 124 | elif price > D: 125 | amount = 0 126 | else: 127 | amount = k * np.exp(r * price) 128 | return amount 129 | 130 | def constant(self, price): 131 | # storing variables in letters for readability 132 | A = self.amounts[0] 133 | B = self.amounts[1] 134 | 135 | C = self.prices[0] 136 | D = self.prices[1] 137 | 138 | if price < C: 139 | amount = 0 140 | elif price > D: 141 | amount = 0 142 | else: 143 | amount = B 144 | return amount 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /utils/exchange.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import datetime 3 | import ccxt 4 | import logging 5 | 6 | 7 | class ExceededAmountLimits(Exception): 8 | """Raised when amount is not within the limits""" 9 | def __init__(self, symbol, min_, max_): 10 | if min_ is None: 11 | self.message = f"Amount for {symbol} should be less than {max_}" 12 | elif max_ is None: 13 | self.message = f"Amount for {symbol} should be greater than {min_}" 14 | else: 15 | self.message = f"Amount for {symbol} should be comprise between {min_} and {max_} (excluding extreme values)" 16 | super().__init__(self.message) 17 | 18 | 19 | def check_cost_limits(exchange, coins): 20 | """Check if the cost is within the exchange limits (min and max)""" 21 | exchange.load_markets() 22 | 23 | for coin in coins: 24 | symbol = coins[coin]['SYMBOL'] 25 | cost = coins[coin]['AMOUNT'] 26 | market = exchange.market(symbol) 27 | if type(cost) is dict: 28 | min_ = market['limits']['cost']['min'] 29 | max_ = market['limits']['cost']['max'] 30 | if (min_ is not None and cost['RANGE'][0] <= min_) or (max_ is not None and cost['RANGE'][1] >= max_): 31 | raise ExceededAmountLimits(symbol, min_, max_) 32 | else: 33 | min_ = market['limits']['cost']['min'] 34 | max_ = market['limits']['cost']['max'] 35 | if (min_ is not None and cost <= min_) or (max_ is not None and cost >= max_): 36 | raise ExceededAmountLimits(symbol, min_, max_) 37 | 38 | def get_non_zero_balance(exchange, sort_by='total', ascending=False ): 39 | """Get non zero balance (total,free and used). Use "sort_by" to sort 40 | according to the type of balance""" 41 | balance = exchange.fetch_balance() 42 | coin_name = [] 43 | coin_list = [] 44 | for key in balance['total']: 45 | if balance['total'][key] > 0: 46 | coin_list.append(balance[key]) 47 | coin_name.append(key) 48 | df = pd.DataFrame.from_records(coin_list) 49 | df.index = coin_name 50 | # sort df 51 | if df.shape[0] > 0: 52 | df.sort_values(sort_by, axis=0, ascending=ascending, inplace=True) 53 | return df 54 | 55 | 56 | def get_price(exchange, symbol): 57 | exchange.load_markets() 58 | last_price = exchange.fetch_ticker(symbol)['last'] 59 | return last_price 60 | 61 | 62 | def get_quantity_to_buy(exchange, amount, symbol): 63 | exchange.load_markets() 64 | last_price = exchange.fetch_ticker(symbol)['last'] 65 | amount = exchange.amount_to_precision(symbol, amount / float(last_price)) 66 | return amount 67 | 68 | 69 | def order_to_dataframe(exchange, order, coin): 70 | 71 | data = {'datetime (local)': datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S"), 72 | 'datetime (exchange)': order['datetime'], 73 | 'timestamp': order['timestamp'], 74 | 'coin': coin, 75 | 'symbol': order['symbol'], 76 | 'status': order['status'], 77 | 'filled': order['filled'], 78 | 'price': order['average'], 79 | 'cost': order['cost'], 80 | 'remaining': order['remaining'], 81 | 'fee': 'N.A.', 82 | 'fee currency': 'N.A.', 83 | 'fee rate': 'N.A.', 84 | } 85 | 86 | if not order['fee']: 87 | # some exchanges return None. Let's try to retrieve fees from the trade history 88 | try: 89 | if exchange.has['fetchOrderTrades']: 90 | trades = exchange.fetch_order_trades(order['id'], order['symbol'], since=None, limit=None, params={}) 91 | fees = [trade['fee'] for trade in trades] 92 | # In case the order is split into multiple trades: 93 | fees = exchange.reduce_fees_by_currency(fees) 94 | order['fee'] = fees[0] # We assume fees are paid in the same currency (may not always be true, e.g., 95 | # a portion paid in BNB and a portion in USDT. We are discarding this case) 96 | except Exception as e: 97 | logging.warning(f"Couldn't retrieve fees due to: {type(e).__name__} {str(e)}") 98 | 99 | if order['fee']: 100 | if 'currency' in order['fee']: 101 | data['fee currency'] = order['fee']['currency'] 102 | if 'cost' in order['fee']: 103 | data['fee'] = order['fee']['cost'] 104 | if 'rate' in order['fee']: 105 | data['fee rate'] = order['fee']['rate'] 106 | 107 | df = pd.DataFrame(data, index=[0]) 108 | return df 109 | 110 | 111 | def connect_to_exchange(cfg, api): 112 | """ 113 | Connect to the exchange using the cfg info and the api (both already loaded) 114 | """ 115 | api_test_selector = 'TEST' if cfg['TEST'] else 'REAL' 116 | 117 | exchange_id = cfg['EXCHANGE'].upper() 118 | if 'PASSPHRASE' in api[exchange_id][api_test_selector]: 119 | exchange_class = getattr(ccxt, exchange_id.lower()) 120 | exchange = exchange_class({ 121 | 'apiKey': api[exchange_id][api_test_selector]['APIKEY'], 122 | 'secret': api[exchange_id][api_test_selector]['SECRET'], 123 | 'password': api[exchange_id][api_test_selector]['PASSPHRASE'], 124 | 'enableRateLimit': True, 125 | 'options': {'adjustForTimeDifference': True} 126 | }) 127 | else: 128 | exchange_class = getattr(ccxt, exchange_id.lower()) 129 | exchange = exchange_class({ 130 | 'apiKey': api[exchange_id][api_test_selector]['APIKEY'], 131 | 'secret': api[exchange_id][api_test_selector]['SECRET'], 132 | 'enableRateLimit': True, 133 | 'options': {'adjustForTimeDifference': True} 134 | }) 135 | 136 | if 'TEST' in api_test_selector: 137 | logging.info(f"Connected to {exchange_id} in TEST mode!") 138 | exchange.set_sandbox_mode(True) 139 | else: 140 | logging.info(f"Connected to {exchange_id}!") 141 | #are_you_sure_to_continue() 142 | 143 | return exchange 144 | -------------------------------------------------------------------------------- /trades_example/log.txt: -------------------------------------------------------------------------------- 1 | 2022-05-01 20:20:08 Program started. Initializing variables... 2 | 2022-05-01 20:20:08 Connected to binance in TEST mode! 3 | 2022-05-01 20:20:08 Getting your balance from the exchange: 4 | 2022-05-01 20:20:10 5 | free used total 6 | TRX 634899.900000 0.0 634899.900000 7 | XRP 57924.300000 0.0 57924.300000 8 | USDT 3473.306174 0.0 3473.306174 9 | BUSD 2807.888812 0.0 2807.888812 10 | BNB 1000.000000 0.0 1000.000000 11 | LTC 505.644760 0.0 505.644760 12 | ETH 100.000000 0.0 100.000000 13 | BTC 0.931099 0.0 0.931099 14 | 2022-05-01 20:20:13 Everything up and running! 15 | 2022-05-01 20:20:13 Initializing next order... 16 | 2022-05-01 20:20:13 Next purchase: BTC (25 USDT) on 2022-05-01 20:20. 17 | Time remaining: 0 s 18 | 2022-05-01 20:20:14 -> Bought 0.000652 BTC at price 38326.39 USDT (Cost = 24.98880628 USDT) 19 | 2022-05-01 20:20:16 Initializing next order... 20 | 2022-05-01 20:20:17 Next purchase: XRP (15 BUSD) on 2022-05-01 20:20. 21 | Time remaining: 0 s 22 | 2022-05-01 20:20:17 -> Bought 19.6 XRP at price 0.7634 BUSD (Cost = 14.96264 BUSD) 23 | 2022-05-01 20:20:20 Initializing next order... 24 | 2022-05-01 20:20:21 Next purchase: LTC (25 BUSD) on 2022-05-01 20:20. 25 | Time remaining: 0 s 26 | 2022-05-01 20:20:21 -> Bought 0.2485 LTC at price 100.6 BUSD (Cost = 24.9991 BUSD) 27 | 2022-05-01 20:20:24 Initializing next order... 28 | 2022-05-01 20:20:24 Next purchase: BTC (25 USDT) on 2022-05-01 20:21. 29 | Time remaining: 45 s 30 | 2022-05-01 20:21:11 -> Bought 0.000653 BTC at price 38252.67 USDT (Cost = 24.97899351 USDT) 31 | 2022-05-01 20:21:13 Initializing next order... 32 | 2022-05-01 20:21:14 Next purchase: XRP (15 BUSD) on 2022-05-01 20:21. 33 | Time remaining: 0 s 34 | 2022-05-01 20:21:14 -> Bought 19.6 XRP at price 0.7634 BUSD (Cost = 14.96264 BUSD) 35 | 2022-05-01 20:21:17 Initializing next order... 36 | 2022-05-01 20:21:18 Next purchase: LTC (25 BUSD) on 2022-05-01 20:21. 37 | Time remaining: 0 s 38 | 2022-05-01 20:21:18 -> Bought 0.2485 LTC at price 100.6 BUSD (Cost = 24.9991 BUSD) 39 | 2022-05-01 20:21:20 Initializing next order... 40 | 2022-05-01 20:21:21 Next purchase: BTC (25 USDT) on 2022-05-01 20:22. 41 | Time remaining: 49 s 42 | 2022-05-01 20:22:11 -> Bought 0.000653 BTC at price 38255.12 USDT (Cost = 24.98059336 USDT) 43 | 2022-05-01 20:22:13 Initializing next order... 44 | 2022-05-01 20:22:14 Next purchase: XRP (15 BUSD) on 2022-05-01 20:22. 45 | Time remaining: 0 s 46 | 2022-05-01 20:22:14 -> Bought 19.6 XRP at price 0.7634 BUSD (Cost = 14.96264 BUSD) 47 | 2022-05-01 20:22:17 Initializing next order... 48 | 2022-05-01 20:22:17 Next purchase: LTC (25 BUSD) on 2022-05-01 20:22. 49 | Time remaining: 0 s 50 | 2022-05-01 20:22:18 -> Bought 0.2485 LTC at price 100.6 BUSD (Cost = 24.9991 BUSD) 51 | 2022-05-01 20:22:20 Initializing next order... 52 | 2022-05-01 20:22:21 Next purchase: BTC (25 USDT) on 2022-05-01 20:23. 53 | Time remaining: 49 s 54 | 2022-05-01 20:23:11 -> Bought 0.000653 BTC at price 38240.61 USDT (Cost = 24.97111833 USDT) 55 | 2022-05-01 20:23:14 Initializing next order... 56 | 2022-05-01 20:23:14 Next purchase: XRP (15 BUSD) on 2022-05-01 20:23. 57 | Time remaining: 0 s 58 | 2022-05-01 20:23:15 -> Bought 19.6 XRP at price 0.7634 BUSD (Cost = 14.96264 BUSD) 59 | 2022-05-01 20:23:17 Initializing next order... 60 | 2022-05-01 20:23:18 Next purchase: LTC (25 BUSD) on 2022-05-01 20:23. 61 | Time remaining: 0 s 62 | 2022-05-01 20:23:19 -> Bought 0.2485 LTC at price 100.6 BUSD (Cost = 24.9991 BUSD) 63 | 2022-05-01 20:23:21 Initializing next order... 64 | 2022-05-01 20:23:22 Next purchase: BTC (25 USDT) on 2022-05-01 20:24. 65 | Time remaining: 48 s 66 | 2022-05-01 20:24:11 -> Bought 0.000653 BTC at price 38255.15 USDT (Cost = 24.98061295 USDT) 67 | 2022-05-01 20:24:13 Initializing next order... 68 | 2022-05-01 20:24:14 Next purchase: XRP (15 BUSD) on 2022-05-01 20:24. 69 | Time remaining: 0 s 70 | 2022-05-01 20:24:14 -> Bought 19.6 XRP at price 0.7634 BUSD (Cost = 14.96264 BUSD) 71 | 2022-05-01 20:24:17 Initializing next order... 72 | 2022-05-01 20:24:17 Next purchase: LTC (25 BUSD) on 2022-05-01 20:24. 73 | Time remaining: 0 s 74 | 2022-05-01 20:24:18 -> Bought 0.2485 LTC at price 100.6 BUSD (Cost = 24.9991 BUSD) 75 | 2022-05-01 20:24:20 Initializing next order... 76 | 2022-05-01 20:24:20 Next purchase: BTC (25 USDT) on 2022-05-01 20:25. 77 | Time remaining: 49 s 78 | 2022-05-01 20:25:11 -> Bought 0.000653 BTC at price 38231.33 USDT (Cost = 24.96505849 USDT) 79 | 2022-05-01 20:25:13 Initializing next order... 80 | 2022-05-01 20:25:14 Next purchase: XRP (15 BUSD) on 2022-05-01 20:25. 81 | Time remaining: 0 s 82 | 2022-05-01 20:25:14 -> Bought 19.6 XRP at price 0.7634 BUSD (Cost = 14.96264 BUSD) 83 | 2022-05-01 20:25:16 WARNING SEND MAIL SMTPServerDisconnected Connection unexpectedly closed 84 | 2022-05-01 20:25:16 Initializing next order... 85 | 2022-05-01 20:25:17 Next purchase: LTC (25 BUSD) on 2022-05-01 20:25. 86 | Time remaining: 0 s 87 | 2022-05-01 20:25:17 -> Bought 0.2485 LTC at price 100.6 BUSD (Cost = 24.9991 BUSD) 88 | 2022-05-01 20:25:19 WARNING SEND MAIL SMTPServerDisconnected Connection unexpectedly closed 89 | 2022-05-01 20:25:19 Initializing next order... 90 | 2022-05-01 20:25:19 Next purchase: BTC (25 USDT) on 2022-05-01 20:26. 91 | Time remaining: 51 s 92 | 2022-05-01 20:26:11 -> Bought 0.000654 BTC at price 38225.29 USDT (Cost = 24.99933966 USDT) 93 | 2022-05-01 20:26:12 WARNING SEND MAIL SMTPServerDisconnected Connection unexpectedly closed 94 | 2022-05-01 20:26:12 Initializing next order... 95 | 2022-05-01 20:26:13 Next purchase: XRP (15 BUSD) on 2022-05-01 20:26. 96 | Time remaining: 0 s 97 | 2022-05-01 20:26:14 -> Bought 19.6 XRP at price 0.7634 BUSD (Cost = 14.96264 BUSD) 98 | 2022-05-01 20:26:15 WARNING SEND MAIL SMTPServerDisconnected Connection unexpectedly closed 99 | 2022-05-01 20:26:15 Initializing next order... 100 | 2022-05-01 20:26:16 Next purchase: LTC (25 BUSD) on 2022-05-01 20:26. 101 | Time remaining: 0 s 102 | 2022-05-01 20:26:17 -> Bought 0.2485 LTC at price 100.6 BUSD (Cost = 24.9991 BUSD) 103 | 2022-05-01 20:26:18 WARNING SEND MAIL SMTPServerDisconnected Connection unexpectedly closed 104 | 2022-05-01 20:26:18 Initializing next order... 105 | 2022-05-01 20:26:19 Next purchase: BTC (25 USDT) on 2022-05-01 20:27. 106 | Time remaining: 51 s 107 | -------------------------------------------------------------------------------- /utils/mail_notifier.py: -------------------------------------------------------------------------------- 1 | from email.mime.multipart import MIMEMultipart 2 | from email.mime.text import MIMEText 3 | from email.mime.image import MIMEImage 4 | import logging 5 | import smtplib 6 | import ssl 7 | from string import Template 8 | from utils.misc import round_price 9 | 10 | 11 | class Notifier(object): 12 | def __init__(self, cfg): 13 | 14 | self.smtp_server = cfg['SMTP_SERVER'] 15 | self.sent_from = cfg['EMAIL_ADDRESS_FROM'] 16 | self.to = cfg['EMAIL_ADDRESS_TO'] 17 | self.password = cfg['EMAIL_PASSWORD'] 18 | self.port = 465 # For SSL 19 | 20 | self.exchage = cfg['EXCHANGE'].lower() 21 | 22 | if cfg['TEST']: 23 | self.exchage += ' (test mode)' 24 | 25 | def info(self, info): 26 | with open('utils/mail_template/info.html', 'r', encoding='utf-8') as file: 27 | body = file.read() 28 | 29 | body = Template(body) 30 | body = body.substitute(info=info, 31 | exchange=self.exchage) 32 | 33 | subject = f'DCA: info' 34 | 35 | self.send(subject, body) 36 | 37 | def success(self, 38 | df, 39 | cycle, 40 | next_purchase, 41 | bought_on, 42 | pairing, 43 | stats, 44 | extra): 45 | 46 | coin = df['coin'][0] 47 | 48 | if 'N.A.' in df['fee'].tolist(): 49 | fee_currency = '' 50 | fee_rate = '' 51 | fee = df['fee'][0] 52 | else: 53 | fee_currency = df['fee currency'][0] 54 | fee = round_price(df['fee'][0]) 55 | if 'N.A.' in df['fee rate'].tolist(): 56 | fee_rate = '' 57 | else: 58 | fee_rate = "(" + round_price(df['fee rate'][0]*100) + " %)" 59 | 60 | with open('utils/mail_template/success.html', 'r', encoding='utf-8') as file: 61 | body = file.read() 62 | 63 | body = Template(body) 64 | body = body.substitute(coin=coin, 65 | exchange=self.exchage, 66 | cycle=cycle, 67 | bought_on=bought_on, 68 | next_purchase=next_purchase, 69 | filled=df['filled'][0], 70 | price=df['price'][0], 71 | pairing=pairing, 72 | cost=df['cost'][0], 73 | fee=fee, 74 | fee_currency=fee_currency, 75 | fee_rate=fee_rate, 76 | total_asset=round_price(stats['Quantity']), 77 | avg_price=round_price(stats['AvgPrice']), 78 | N=int(stats['N']), 79 | total_cost=round_price(stats['TotalCost']), 80 | gain=round_price(stats['ROI']), 81 | ROI=round_price(stats['ROI%']), 82 | extra=extra) 83 | 84 | # Attach graph 85 | graph_path = 'trades/graph_' + coin + '.png' 86 | with open(graph_path, 'rb') as img: 87 | msgimg = MIMEImage(img.read()) 88 | msgimg.add_header('Content-ID', '') 89 | # replace src image with src="cid:graph" 90 | 91 | subject = f'DCA: {coin} purchase complete' 92 | 93 | self.send(subject, body, msgimg) 94 | 95 | def warning_funds(self, 96 | coin, 97 | next_purchase, 98 | pairing, 99 | cost, 100 | balance): 101 | 102 | with open('utils/mail_template/insufficientFundsWarning.html', 'r', encoding='utf-8') as file: 103 | body = file.read() 104 | 105 | body = Template(body) 106 | body = body.substitute(coin=coin, 107 | exchange=self.exchage, 108 | cost=cost, 109 | pairing=pairing, 110 | balance=balance, 111 | next_purchase=next_purchase) 112 | 113 | subject = f'DCA: {coin} warning' 114 | 115 | self.send(subject,body) 116 | 117 | def error(self, 118 | coin, 119 | retry_dict, 120 | error): 121 | """These errors are not critical and should be recoverable""" 122 | with open('utils/mail_template/error.html', 'r', encoding='utf-8') as file: 123 | body = file.read() 124 | 125 | error_type = type(error).__name__ 126 | 127 | retry_time = retry_dict[1] 128 | attempts = retry_dict[0] 129 | 130 | body = Template(body) 131 | body = body.substitute(coin=coin, 132 | exchange=self.exchage, 133 | error_type=error_type, 134 | error=str(error), 135 | retry_time=retry_time, 136 | attempts=attempts) 137 | 138 | subject = f'DCA: {coin} purchase failed' 139 | 140 | self.send(subject, body) 141 | 142 | def critical(self, 143 | error, 144 | when): 145 | """These errors are critical and the program is terminated after sending one of these""" 146 | 147 | with open('utils/mail_template/critical.html', 'r', encoding='utf-8') as file: 148 | body = file.read() 149 | 150 | error_type = type(error).__name__ 151 | 152 | body = Template(body) 153 | body = body.substitute(when=when, 154 | exchange=self.exchage, 155 | error_type=error_type, 156 | error=str(error)) 157 | 158 | subject = f'DCA: Critical Error' 159 | 160 | self.send(subject, body) 161 | 162 | def send(self, subject, body, img=None): 163 | 164 | # Create message container - the correct MIME type is multipart/alternative here! 165 | msg = MIMEMultipart('alternative') 166 | msg['subject'] = subject 167 | msg['To'] = self.to 168 | msg['From'] = self.sent_from 169 | msg.preamble = """ 170 | Your mail reader does not support the report format. 171 | """ 172 | 173 | # Record the MIME type text/html. 174 | html_body = MIMEText(body, 'html') 175 | 176 | # Attach parts into message container. 177 | # According to RFC 2046, the last part of a multipart message, in this case 178 | # the HTML message, is best and preferred. 179 | msg.attach(html_body) 180 | 181 | if img: 182 | msg.attach(img) 183 | 184 | try: 185 | context = ssl.create_default_context() 186 | with smtplib.SMTP_SSL(self.smtp_server, self.port, context=context) as server: 187 | server.login(self.sent_from, self.password) 188 | server.sendmail(self.sent_from, self.to, msg.as_string()) 189 | 190 | except Exception as e: 191 | logging.warning("SEND MAIL " + type(e).__name__ + ' ' + str(e)) 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![DCA-bot banner](./utils/banner.jpg) 2 | 3 | _DCA-bot_ is a Python-based program for making recurring and automatic cryptocurrency purchases. 4 | Being developed using the [ccxt](https://github.com/ccxt/ccxt) library it can work on almost every exchange (although, it has only been tested on Binance, FTX and Kucoin) and on every crypto/crypto or crypto/fiat pair available on the chosen exchange. It was designed to run on 24/7 servers that are as light as a Raspberry Pi. 5 | 6 | The bot can operate in three DCA modalities: 7 | - **Classic**: buy a fixed dollar amount regardless of price conditions 8 | - **BuyBelow**: buy only if the price is below a price level 9 | - **VariableAmount**: map a price range onto an amount range, so that when the price goes down the amount of dollars invested increases 10 | 11 | 12 | ### What is DCA? 13 | Dollar-Cost-Averaging (DCA) is a popular investing strategy in which a fixed dollar amount of a desired asset is bought at regular time intervals, regardless of market conditions. It is particularly suitable for markets with high volatility such as cryptocurrency markets. 14 | While DCA might not provide the best possible returns, it has the added benefit of making you sleep well at night, even during bear market periods. 15 | 16 | ### Why use this bot? 17 | Almost every exchange has the possibility of doing DCA (sometimes referred to as _recurrent buy_). So, why should you use this bot? Well, there are a few caveats you should consider when using the exchanges' recurring buy services. First, exchanges usually charge extra fees for recurrent buys. For example, the auto-invest service of Binance charges you double the regular fee. Second, the spread is sometimes higher than that of a normal market order. This means you can end up with a price that you cannot even trace back to the chart. Third, exchanges do not notify you with the outcome of the transaction or with a summary of your investment plan. 18 | 19 | The bot was developed to overcome the above limitations. In particular, the bot 20 | - makes recurrent purchases through market orders. Thus, fees are limited to the commission that exchanges charge for market orders. In the case of Binance, the market fees are 0.1%, and they can be further reduced by holding small amounts of their token (i.e., BNB) 21 | - notifies you of every single transaction 22 | - notifies you of a summary of your investment plan 23 | - reminds you to top up your account in case the balance is not enough for the next purchase. 24 | 25 | In addition, the bot introduces two non-standard DCA variants that are not usually available on recurring buy services: 26 | - *BuyBelow*: buy only if the price is below a price level 27 | - *VariableAmount*: map a price range onto an amount range, so that when the price goes down the amount of dollars invested increases. 28 | 29 | ## Getting started 30 | 31 | ### Requirements 32 | You will need a server that can run continuously (i.e., 24/7). Due to its extreme low power consumption, a Raspberry Pi is an ideal candidate for the job. 33 | You will need **python 3.8** and the following packages: 34 | - ccxt 35 | - numpy 36 | - pandas 37 | - matplotlib 38 | - pyyaml 39 | 40 | If you opted for a Raspberry Pi, you might find useful to follow this [installation tutorial](https://gist.github.com/CodingCryptoTrading/005fc6dc23e1d6012ac5ad74e2e55851). 41 | 42 | ### Set the API key 43 | After choosing one of the [ccxt](https://github.com/ccxt/ccxt) supported exchanges, you will have to generate an API key with "reading" and "trading" permissions. For instance, the binance procedure is described [here](https://www.binance.com/en/support/faq/360002502072). 44 | 45 | Then, input the generated key and secret in [auth/API_keys_example.yml](auth/API_keys_example.yml) and save it as `auth.yml`. Maintain the syntax as in the example file. In particular, after the name of the exchange be sure to specify if it is a *REAL* or *TEST* account (more on test accounts [here](#running-the-bot-in-test-mode)) 46 | 47 | Some exchanges (e.g., *coinbasepro*) may require an additional API parameter, sometimes called passphrase or password. If that is the case, put it under the voice *PASSPHRASE*. 48 | 49 | ### Configure the bot 50 | The bot settings are stored in [config/config_example.yml](config/config_example.yml). Make a copy of the file and rename it as `config.yml`. Fill the file with your own settings. 51 | 52 | Under _COINS_ put all the currencies that you wish to buy. Here is an example that will purchase 25 USDT of BTC every day at 19:30: 53 | ``` 54 | COINS: # Choose the coins to buy: 55 | BTC: 56 | PAIRING: USDT # Choose the currency to use to buy the coin 57 | AMOUNT: 25 # Quantity to buy in your pairing currency 58 | CYCLE: 'daily' # Recurring Cycle can be: 'daily', 'weekly', 'bi-weekly', 'monthly' 59 | ON_WEEKDAY: 6 # [only for weekly and bi-weekly] Repeats on 0-6 (0=Monday...6=Sunday) 60 | ON_DAY: 1 # [only for monthly]. Date of the month [1-28] 61 | AT_TIME: '19:30' # Format: 0 <= hour <= 23, 0 <= minute <= 59 62 | ``` 63 | Note that `ON_WEEKDAY` and `ON_DAY` are not considered in a daily `cycle`. There is also a "minutely" cycle (buy every minute) but is disabled with real accounts, it is only available in test mode for debugging purposes (see [Running the bot in test mode](#running-the-bot-in-test-mode)). 64 | 65 | Next, you have to specify what exchange to use, and whether it is a test account (`TEST: True`) or a real account (`TEST: False`): 66 | 67 | ``` 68 | ### Exchange section ### 69 | EXCHANGE: 'binance' 70 | TEST: True 71 | ``` 72 | The exchange name has to match the labelling in the [ccxt](https://github.com/ccxt/ccxt) library (e.g., if you are in the US, you should replace `binance` with `binanceus`) 73 | 74 | Finally, if you want to receive notifications (e.g., purchase reports, warnings, errors and others) fill the last section in the config file: 75 | ``` 76 | ### Notification section ### 77 | SEND_NOTIFICATIONS: True 78 | 79 | # email sender info 80 | EMAIL_ADDRESS_FROM: 'sender@email.com' 81 | EMAIL_PASSWORD: 'sender email password' 82 | SMTP_SERVER: 'smtp.mail.yahoo.com' # sender email SMTP server 83 | 84 | # email recipient 85 | EMAIL_ADDRESS_TO: 'recipient@email.com' 86 | ``` 87 | The recipient address and the sender address can be the same. However, it is not wise to store the password of your main email on a server. Therefore, we suggest that you create an ad hoc email for this purpose. Gmail has too many restrictions, so it's not recommended. Yahoo mail is a good alternative (yahoo will ask you to define an "application" password, so you will not really enter the email password). 88 | 89 | ### Configure the bot (DCA variants) 90 | The above configuration file describes the classic DCA approach. Here we describe two additional variants: BuyBelow and VariableAmount. 91 | 92 | #### BuyBelow 93 | BuyBelow is just like the classic DCA, but it doesn't buy when the price is above a given threshold. To use this modality, simply add the variable `BUYBELOW` in the config file as shown here: 94 | ``` 95 | COINS: # Choose the coins to buy: 96 | BTC: 97 | PAIRING: USDT # Choose the currency to use to buy the coin 98 | AMOUNT: 25 # Quantity to buy in your pairing currency 99 | CYCLE: 'daily' # Recurring Cycle can be: 'daily', 'weekly', 'bi-weekly', 'monthly' 100 | AT_TIME: '19:30' # Format: 0 <= hour <= 23, 0 <= minute <= 59 101 | BUYBELOW: 40000 # Don't buy above this price. 102 | ``` 103 | In the example, the bot will not buy BTC if the price is above 40000 USDT. 104 | 105 | #### VariableAmount 106 | In VariableAmount a price range is mapped onto an amount range, with an exponential or linear function. Prices outside the range will either result in no purchase (above the upper price range) or saturate the dollar amount (below the lower price range). To clarify this modality, take a look at the below chart that illustrates how many USDT are invested depending on the BTC price. In the example, the price range [10000-40000] USDT is mapped exponentially onto the amount range [10-100] USDT. When the price is above 40000 USDT, the bot doesn't buy (as in *BuyBelow* modality). As the price decreases, the bot uses an increasing amount of USDT, until the price reaches the lower price range and the amount of USDT saturates. 107 | 108 | ![DCA-bot banner](./utils/graph_BTC_buy_conditions.png) 109 | 110 | To use the VariableAmount modality the `AMOUNT` in the config file has to be modified as follows: 111 | 112 | ``` 113 | COINS: # Choose the coins to buy: 114 | BTC: 115 | PAIRING: USDT # Choose the currency to use to buy the coin 116 | AMOUNT: # Now AMOUNT is a dictionary 117 | RANGE: [10.1,100] # Range of quantity to buy in your pairing currency 118 | PRICE_RANGE: [10000,40000] # Range of prices 119 | MAPPING: 'exponential' # Mapping function: 'exponential' or 'linear' 120 | CYCLE: 'daily' # Recurring Cycle can be: 'daily', 'weekly', 'bi-weekly', 'monthly' 121 | AT_TIME: '19:30' # Format: 0 <= hour <= 23, 0 <= minute <= 59 122 | ``` 123 | Once the bot starts, a buy-conditions chart, similar to the above, will be saved in the trades folder. We suggest playing a bit with `RANGE`/`PRICE_RANGE`/`MAPPING` and inspecting the chart until you get a buy-condition curve that satisfies your needs. 124 | 125 | ### Run the bot 126 | Now that everything has been set up, we are ready to run the bot. Just navigate to the folder where you stored the bot and run: 127 | ``` 128 | python3.8 dca_bot.py 129 | ``` 130 | The bot is able to recover from where it left in case it is interrupted, the system crashes or is rebooted abruptly. Therefore, we recommend running the bot automatically every time the server starts. If you are running the bot on a linux server, such as in the case of a Raspberry Pi, you can do so by defining a cronjob. Simply run: 131 | ``` 132 | crontab -e 133 | ``` 134 | And add the following line: 135 | ``` 136 | @reboot cd /path/where/theBootIsStored && python3.8 dca_bot.py & 137 | ``` 138 | To check that everything is working, try restarting the server. Once rebooted you should be notified that the bot has just started. Now you can enjoy recurring purchases at minimal cost and without any effort! 139 | 140 | ## Check the bot 141 | If you have activated the notification system, you will receive all the relevant information by mail, including buy outcomes, investment summaries, general info, warnings and errors. 142 | 143 | Even if you have disabled notifications, you can still get all the information by accessing the `trades` folder that will be created once the bot is started. An example of a `trades` folder is available in [trades_example](trades_example/). Inside you will find: 144 | 145 | - `log.txt` : records everything is happening with the bot 146 | - `graph_COIN.png` : chart of all the purchases of a given COIN 147 | - `graph_COIN_buy_conditions.png` : buy-condition chart (only in *VariableAmount* mode) 148 | - `orders.json` : json file containing every filled order exactly as returned by the exchange 149 | - `orders.csv` : a more readable version of the above (with only the most essential information) 150 | - `stats.csv` : summary statistics of your investment plans 151 | - `next_purchases.csv` : a list of the next purchases 152 | 153 | 154 | If at any time you wish to create a new accumulation plan from scratch (not considering previous purchases), you can do so by deleting the `trades` folder and restarting the bot. 155 | 156 | 157 | ## Running the bot in test mode 158 | 159 | If a sendbox is available in the chosen exchange, the bot can operate in testing mode (e.g., binance offers this service). Thus, you can use the test mode in case you want to try the bot with fake money first. The test mode can also be helpful for debugging purposes. 160 | 161 | To set the bot to run in test mode, set `TEST: True` in your `config/config.yml` file. Then, you need to generate a test account on the exchange, get the API key and secret and put them in your `auth/API_keys.yml` file under `TEST` (i.e., follow the syntax as in [auth/API_keys_example.yml](auth/API_keys_example.yml)). 162 | 163 | Getting a test account in Binance is straightforward. Just log in [the binance testnet](https://testnet.binance.vision/) with a GitHub account and then click on generate API keys. 164 | 165 | ## Contributing 166 | Any contribution to the bot is welcome. If you have a suggestion or find a bug, please create an [issue](https://github.com/CodingCryptoTrading/dca-crypto-bot/issues). 167 | 168 | ## Disclaimer 169 | The investment in cryptocurrency can lead to loss of money over short or even long periods. Use DCA-bot at your own risk. 170 | -------------------------------------------------------------------------------- /utils/mail_template/info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 209 | 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /utils/mail_template/insufficientFundsWarning.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /utils/mail_template/critical.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /utils/mail_template/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /utils/mail_template/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 267 | 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /dca_bot.py: -------------------------------------------------------------------------------- 1 | from utils.timing import * 2 | from utils.exchange import * 3 | from utils.stats_and_plots import * 4 | from utils.mail_notifier import Notifier 5 | from utils.trade_strategies import PriceMapper 6 | 7 | import ccxt 8 | import logging 9 | import time 10 | from dateutil.relativedelta import relativedelta 11 | import pandas as pd 12 | from pathlib import Path 13 | import os 14 | 15 | 16 | class Dca(object): 17 | def __init__(self, cfg_path, api_path): 18 | # create logger 19 | log_file = Path('trades/log.txt') 20 | log_file.parent.mkdir(parents=True, exist_ok=True) 21 | register_logger(log_file=log_file) 22 | logging.info('Program started. Initializing variables...') 23 | 24 | # loads local configuration 25 | cfg = load_config(cfg_path) 26 | api = load_config(api_path) 27 | 28 | # Store cfg 29 | self.cfg = cfg 30 | 31 | # initialize notifier 32 | if self.cfg['SEND_NOTIFICATIONS']: 33 | self.notify = Notifier(self.cfg) 34 | 35 | try: 36 | self.exchange = connect_to_exchange(self.cfg, api) 37 | except Exception as e: 38 | if self.cfg['SEND_NOTIFICATIONS']: 39 | self.notify.critical(e, "lunching the both running") 40 | raise e 41 | 42 | # Show balance 43 | try: 44 | balance = get_non_zero_balance(self.exchange, sort_by='total') 45 | if balance.shape[0] == 0: 46 | balance_str = 'No coin found in your wallet!' # is it worth going on? 47 | else: 48 | balance_str = balance.to_string() 49 | logging.info("Your balance from the exchange:\n" + balance_str + "\n") 50 | except Exception as e: 51 | logging.warning("Balance checking failed: " + type(e).__name__ + " " + str(e)) 52 | 53 | # Store coin info into a local variable 54 | self.coin = {} 55 | for coin in cfg['COINS']: 56 | self.coin[coin.upper()] = cfg['COINS'][coin] 57 | 58 | self.order_book = {} 59 | self.coin_to_buy = [] 60 | self.next_order = [] 61 | 62 | # create trade folder and define csv filepath (for orders) 63 | self.csv_path = Path('trades/orders.csv') 64 | if Path(self.csv_path).is_file(): 65 | self.df_orders = read_csv_custom(self.csv_path) 66 | else: 67 | self.df_orders = pd.DataFrame() 68 | 69 | # define csv filepath for stats 70 | self.stats_path = Path('trades/stats.csv') 71 | if Path(self.stats_path).is_file(): 72 | self.df_stats = read_csv_custom(self.stats_path) 73 | else: 74 | self.df_stats = pd.DataFrame([], columns=['Coin', 'N', 'Quantity', 'AvgPrice', 'TotalCost', 'ROI', 'ROI%']) 75 | self.df_stats.set_index(['Coin'], inplace=True) 76 | 77 | # define json filepath (for orders) 78 | self.json_path = Path('trades/orders.json') 79 | 80 | # define path for order_book (next_purchases) 81 | self.order_book_path = Path('trades/next_purchases.csv') 82 | 83 | # check if the amount is fixed or is variable depending on the price range 84 | self.get_dca_strategy() 85 | 86 | # Get the 'SCHEDULE' time for each coin and initialize order_book 87 | self.initialize_order_book() 88 | df = self.update_order_book() # ensure the order book is written to disk and the set the next coin to buy 89 | logging.info("Summary of the investment plans:\n" + df.to_string() + "\n") 90 | 91 | # get retry times for errors 92 | self.retry_for_funds, self.retry_for_network = retry_info() 93 | 94 | # Check coin limits 95 | check_cost_limits(self.exchange, self.coin) 96 | 97 | if self.cfg['SEND_NOTIFICATIONS']: 98 | info = 'DCA bot has just been started' 99 | self.notify.info(info) 100 | 101 | logging.info('Everything up and running!') 102 | 103 | while True: 104 | 105 | #logging.info('Initializing next order...') 106 | #self.find_next_order() 107 | 108 | # do not check funds if last attempt failed due to insufficient Funds 109 | if not isinstance(self.coin[self.coin_to_buy]['LASTERROR'], ccxt.InsufficientFunds): 110 | self.check_funds() 111 | 112 | self.wait() 113 | 114 | self.buy() 115 | 116 | self.update_order_book() 117 | 118 | def update_order_book(self): 119 | """ 120 | Write to disk the order_book. The order_book is required to identify the correct bi-weekly purchase time 121 | in case the bot is restarted. 122 | Also, Find (and set) the closest coin to buy. 123 | """ 124 | 125 | # first element is the coin, second element the time 126 | self.next_order = min(self.order_book.items(), key=lambda x: x[1]) 127 | self.coin_to_buy = self.next_order[0] 128 | 129 | # Save the order book to disk 130 | ordered_order_book = dict(sorted(self.order_book.items(), key=lambda item: item[1])) 131 | df = pd.DataFrame([ordered_order_book]).T.rename_axis('Coin').rename(columns={0: 'Purchase Time'}) 132 | cycle = [] 133 | strategy = [] 134 | for coin in df.index: 135 | cycle.append(self.coin[coin]['CYCLE'].lower()) 136 | strategy.append(self.coin[coin]['STRATEGY_STRING']) 137 | df['Cycle'] = cycle 138 | df['Strategy'] = strategy 139 | df.to_csv(self.order_book_path) 140 | return df 141 | 142 | def get_dca_strategy(self): 143 | for coin in self.coin: 144 | # to avoid confusion, remove any buy condition plot 145 | if os.path.exists(f"trades/graph_{coin}_buy_conditions.png"): 146 | os.remove(f"trades/graph_{coin}_buy_conditions.png") 147 | if type(self.coin[coin]['AMOUNT']) is dict: 148 | 149 | if 'RANGE' not in self.coin[coin]['AMOUNT'] or 'PRICE_RANGE' not in self.coin[coin]['AMOUNT'] or 'MAPPING' not in self.coin[coin]['AMOUNT']: 150 | raise Exception('If AMOUNT is a dictionary the following keys are required: ' 151 | '"AMOUNT", "PRICE_RANGE", "MAPPING".') 152 | self.coin[coin]['MAPPER'] = PriceMapper(self.coin[coin]['AMOUNT']['RANGE'], 153 | self.coin[coin]['AMOUNT']['PRICE_RANGE'], 154 | self.coin[coin]['AMOUNT']['MAPPING'], 155 | coin, 156 | self.coin[coin]['PAIRING']) 157 | self.coin[coin]['MAPPER'].plot() 158 | self.coin[coin]['STRATEGY'] = 'VariableAmount' 159 | cost = f"{self.coin[coin]['AMOUNT']['RANGE'][0]}-" \ 160 | f"{self.coin[coin]['AMOUNT']['RANGE'][1]}" 161 | price_range = f"{self.coin[coin]['AMOUNT']['PRICE_RANGE'][0]}-" \ 162 | f"{self.coin[coin]['AMOUNT']['PRICE_RANGE'][1]}" 163 | self.coin[coin]['STRATEGY_STRING'] = f"{cost} {self.coin[coin]['PAIRING']} to {price_range} {coin} {self.coin[coin]['AMOUNT']['MAPPING'][0:3]}." 164 | if 'BUYBELOW' in self.coin[coin] and self.coin[coin]['BUYBELOW'] is not None: 165 | logging.warning('Option "BUYBELOW" is not compatible with a range of AMOUNT values. ' 166 | 'Disabling it') 167 | self.coin[coin]['BUYBELOW'] = None 168 | elif 'BUYBELOW' in self.coin[coin] and self.coin[coin]['BUYBELOW'] is not None: 169 | # in this case the mapper is only used for plotting 170 | self.coin[coin]['MAPPER'] = PriceMapper([0, self.coin[coin]['AMOUNT']], 171 | [0, self.coin[coin]['BUYBELOW']], 172 | 'constant', 173 | coin, 174 | self.coin[coin]['PAIRING']) 175 | self.coin[coin]['MAPPER'].plot() 176 | self.coin[coin]['STRATEGY'] = 'BuyBelow' 177 | self.coin[coin]['STRATEGY_STRING'] = f"BuyBelow {self.coin[coin]['BUYBELOW']} {self.coin[coin]['PAIRING']}" 178 | else: 179 | self.coin[coin]['STRATEGY'] = 'Classic' 180 | self.coin[coin]['STRATEGY_STRING'] = f"Classic" 181 | 182 | # def find_next_order(self): 183 | # 184 | # # first element is the coin, second element the time 185 | # self.next_order = min(self.order_book.items(), key=lambda x: x[1]) 186 | # self.coin_to_buy = self.next_order[0] 187 | # 188 | # # Also, write to disk the order_book 189 | # ordered_order_book = dict(sorted(self.order_book.items(), key=lambda item: item[1])) 190 | # df = pd.DataFrame([ordered_order_book]).T.rename_axis('Coin').rename(columns={0: 'Purchase Time'}) 191 | # cycle = [] 192 | # strategy = [] 193 | # for coin in df.index: 194 | # cycle.append(self.coin[coin]['CYCLE'].lower()) 195 | # strategy.append(self.coin[coin]['STRATEGY_STRING']) 196 | # df['Cycle'] = cycle 197 | # df['Strategy'] = strategy 198 | # df.to_csv(self.order_book_path) 199 | 200 | def check_funds(self): 201 | """ 202 | Check if there is sufficient money for the next purchase 203 | """ 204 | cost = self.coin[self.coin_to_buy]['AMOUNT'] 205 | if type(cost) is dict: 206 | cost = cost['RANGE'][1] # In this case we check for the maximum possible amount 207 | pairing = self.coin[self.coin_to_buy]['PAIRING'] 208 | try: 209 | balance = self.exchange.fetch_balance() 210 | except: 211 | balance = [] 212 | logging.warning("Balance checking failed.") 213 | 214 | if balance: 215 | # the Kraken API returns total only 216 | balance_type = 'total' if self.exchange.id == 'kraken' else 'free' 217 | if pairing in balance[balance_type]: 218 | coin_balance = balance[balance_type][pairing] 219 | else: 220 | coin_balance = 0 221 | if cost > coin_balance: 222 | logging.warning(f"Insufficient funds for the next {self.coin_to_buy} purchase. Top up your account!") 223 | if self.cfg['SEND_NOTIFICATIONS']: 224 | next_purchase = self.next_order[1].strftime('%d %b %Y at %H:%M') 225 | self.notify.warning_funds(self.coin_to_buy, 226 | next_purchase, 227 | pairing, 228 | cost, 229 | coin_balance) 230 | 231 | def wait(self): 232 | """ 233 | wait for the next purchase 234 | """ 235 | time_remaining = (self.next_order[1] - datetime.datetime.today()).total_seconds() 236 | if time_remaining < 0: 237 | time_remaining = 0 238 | if self.coin[self.next_order[0]]['STRATEGY'] == 'VariableAmount': 239 | cost = f"{self.coin[self.next_order[0]]['AMOUNT']['RANGE'][0]}-" \ 240 | f"{self.coin[self.next_order[0]]['AMOUNT']['RANGE'][1]}" 241 | else: 242 | cost = self.coin[self.next_order[0]]['AMOUNT'] 243 | logging.info(f"Next purchase: {self.next_order[0]} ({cost} " 244 | f"{self.coin[self.next_order[0]]['PAIRING']}) on {self.next_order[1].strftime('%Y-%m-%d %H:%M')}." 245 | f"\nTime remaining: {int(time_remaining)} s") 246 | 247 | time.sleep(time_remaining) 248 | 249 | def buy(self): 250 | 251 | order = self.execute_order(self.coin_to_buy) 252 | # print and save order info: 253 | if order: 254 | store_json_order(self.json_path, order) 255 | df = order_to_dataframe(self.exchange, order, self.coin_to_buy) 256 | string_order = f"Bought {df['filled'][0]} {self.coin_to_buy} at price {df['price'][0]} {self.coin[self.coin_to_buy]['PAIRING']} (Cost = {df['cost'][0]} {self.coin[self.coin_to_buy]['PAIRING']})" 257 | logging.info("-> " + string_order) 258 | self.df_orders = pd.concat([self.df_orders, df]).reset_index(drop=True) 259 | self.df_orders.index.names = ['N'] 260 | self.df_orders.to_csv(self.csv_path) 261 | plot_purchases(self.coin_to_buy, self.df_orders, self.coin[self.coin_to_buy]['PAIRING']) 262 | self.df_stats = calculate_stats(self.coin_to_buy, self.df_orders, self.df_stats, self.stats_path) 263 | if self.cfg['SEND_NOTIFICATIONS']: 264 | next_purchase = self.coin[self.coin_to_buy]['SCHEDULE'].strftime('%d %b %Y at %H:%M') 265 | self.notify.success(df, 266 | self.coin[self.coin_to_buy]['CYCLE'], 267 | next_purchase, 268 | datetime.datetime.now().strftime('%d %b %Y at %H:%M'), 269 | self.coin[self.coin_to_buy]['PAIRING'], 270 | self.df_stats.loc[self.coin_to_buy], 271 | f"Mode: {self.coin[self.coin_to_buy]['STRATEGY_STRING']}") 272 | 273 | def execute_order(self, coin): 274 | type_order = 'market' 275 | side = 'buy' 276 | symbol = self.coin[coin]['SYMBOL'] 277 | price = None 278 | 279 | try: 280 | if self.coin[coin]['STRATEGY'] == 'BuyBelow' or self.coin[coin]['STRATEGY'] == 'VariableAmount': 281 | # check if the condition is met 282 | price = get_price(self.exchange, self.coin[coin]['SYMBOL']) 283 | amount = self.coin[coin]['MAPPER'].get_amount(price) 284 | if amount == 0: 285 | string_order = f"{coin} price above buy condition ({price} {self.coin[coin]['PAIRING']})." \ 286 | f" This iteration will be skipped." 287 | self.handle_successful_trade(coin, string_order) 288 | return False 289 | else: 290 | amount = self.coin[coin]['AMOUNT'] 291 | 292 | if 'binance' in self.exchange.id: 293 | # this order strategy should take care of everything (precision and lot size) 294 | params = { 295 | 'quoteOrderQty': amount, 296 | } 297 | order = self.exchange.create_order(symbol, type_order, side, amount, price, params) 298 | else: 299 | # In case the above is not available on the exchange use the following 300 | amount = get_quantity_to_buy(self.exchange, amount, symbol) 301 | order = self.exchange.create_order(symbol, type_order, side, amount, price) 302 | # for some exchanges (as FTX) the order must be retrieved to be updated 303 | waiting_time = 0.25; total_time = 0 304 | while order['status'] != 'closed': 305 | if total_time > 1: 306 | raise Exception("The exchange did not return a closed order") 307 | time.sleep(waiting_time) # let's give the exchange some time to fill the order 308 | order = self.exchange.fetch_order(order['id'], symbol) 309 | total_time += waiting_time 310 | self.handle_successful_trade(coin) 311 | return order 312 | # Network errors: these are non-critical errors (recoverable) 313 | except (ccxt.DDoSProtection, ccxt.ExchangeNotAvailable, 314 | ccxt.InvalidNonce, ccxt.RequestTimeout, ccxt.NetworkError) as e: 315 | self.handle_recoverable_errors(coin, e) 316 | # send only on first occurrence 317 | if self.cfg['SEND_NOTIFICATIONS'] and self.coin[coin]['ERROR_ATTEMPT'] == 1: 318 | # if there is a network error, it is likely that this message will not be transmitted 319 | self.notify.error(coin, self.retry_for_network[self.coin[coin]['CYCLE']], e) 320 | except ccxt.InsufficientFunds as e: # This is an ExchangeError but we will treat it as recoverable 321 | self.handle_recoverable_errors(coin, e) 322 | # send only on first occurrence 323 | if self.cfg['SEND_NOTIFICATIONS'] and self.coin[coin]['ERROR_ATTEMPT'] == 1: 324 | self.notify.error(coin, self.retry_for_funds[self.coin[coin]['CYCLE']], e) 325 | # Not recoverable errors (Exchange errors): 326 | except ccxt.ExchangeError as e: 327 | logging.error(type(e).__name__ + ' ' + str(e)) 328 | if self.cfg['SEND_NOTIFICATIONS']: 329 | when = f"attempting to purchase {coin}" 330 | self.notify.critical(e, when) 331 | raise e 332 | except Exception as e: # raise all other exceptions 333 | logging.error(type(e).__name__ + ' ' + str(e)) 334 | when = f"attempting to purchase {coin}" 335 | if self.cfg['SEND_NOTIFICATIONS']: 336 | self.notify.critical(e, when) 337 | raise e 338 | return False 339 | 340 | def handle_successful_trade(self, coin, string=None): 341 | # This steps are common to all dca strategy 342 | self.update_next_datetime(coin) 343 | # reset error variable 344 | self.coin[coin]['LASTERROR'] = [] 345 | self.coin[coin]['ERROR_ATTEMPT'] = 0 346 | if string: 347 | logging.info("" + string) 348 | 349 | def handle_recoverable_errors(self, coin, e): 350 | # wait (variable on cycle frequency) and retry 351 | retry_after = self.get_retry_time(coin, e) 352 | self.update_next_datetime(coin, retry_after=retry_after) 353 | if retry_after: 354 | error_msg = f"{type(e).__name__} {str(e)}\nNext attempt will be in {retry_after} s" 355 | logging.warning(error_msg) 356 | else: 357 | error_msg = f"{type(e).__name__} {str(e)}\nToo many attempts. Skipping this iteration." 358 | logging.error(error_msg) 359 | self.coin[coin]['LASTERROR'] = e 360 | 361 | def get_retry_time(self, coin, error): 362 | """ 363 | For a given error get the appropriate retry time for the next buy attempt. 364 | Args: 365 | coin: coin to update (str) 366 | error: error returned during buy time 367 | """ 368 | self.coin[coin]['ERROR_ATTEMPT'] += 1 369 | 370 | if isinstance(error, ccxt.InsufficientFunds): 371 | max_attempt = self.retry_for_funds[self.coin[coin]['CYCLE']][0] 372 | if self.coin[coin]['ERROR_ATTEMPT'] <= max_attempt: 373 | retry_time = self.retry_for_funds[self.coin[coin]['CYCLE']][1] 374 | return retry_time 375 | else: 376 | # too many attempts, skip this buying iteration 377 | self.coin[coin]['ERROR_ATTEMPT'] = 0 378 | return False 379 | elif isinstance(error, (ccxt.DDoSProtection, ccxt.ExchangeNotAvailable, ccxt.InvalidNonce, ccxt.RequestTimeout, ccxt.NetworkError)): 380 | max_attempt = self.retry_for_network[self.coin[coin]['CYCLE']][0] 381 | if self.coin[coin]['ERROR_ATTEMPT'] <= max_attempt: 382 | retry_time = self.retry_for_network[self.coin[coin]['CYCLE']][1] 383 | return retry_time 384 | else: 385 | # too many attempts, skip this buying iteration 386 | self.coin[coin]['ERROR_ATTEMPT'] = 0 387 | return False 388 | 389 | def update_next_datetime(self,coin,retry_after=False): 390 | """ 391 | For a given coin, update the next buy time and the order book 392 | Args: 393 | coin: coin to update (str) 394 | retry_after: time in seconds to wait for the next buy attempt (in case previous failed) 395 | """ 396 | if retry_after: # this means that an error occurred 397 | self.order_book[coin] = datetime.datetime.today() + datetime.timedelta(seconds=retry_after) 398 | else: 399 | if self.coin[coin]['CYCLE'].lower() == 'minutely': # only for testing purpose 400 | self.coin[coin]['SCHEDULE'] = self.coin[coin]['SCHEDULE'] + datetime.timedelta(minutes=1) 401 | elif self.coin[coin]['CYCLE'].lower() == 'daily': 402 | self.coin[coin]['SCHEDULE'] = self.coin[coin]['SCHEDULE'] + datetime.timedelta(days=1) 403 | elif self.coin[coin]['CYCLE'].lower() == 'bi-weekly': 404 | self.coin[coin]['SCHEDULE'] = self.coin[coin]['SCHEDULE'] + datetime.timedelta(days=14) 405 | elif self.coin[coin]['CYCLE'].lower() == 'weekly': 406 | self.coin[coin]['SCHEDULE'] = self.coin[coin]['SCHEDULE'] + datetime.timedelta(days=7) 407 | elif self.coin[coin]['CYCLE'].lower() == 'monthly': 408 | self.coin[coin]['SCHEDULE'] = self.coin[coin]['SCHEDULE'] + relativedelta(months=1) 409 | # update the order book: 410 | self.order_book[coin] = self.coin[coin]['SCHEDULE'] 411 | 412 | def initialize_order_book(self): 413 | """ 414 | Initialize the schedule time for each coin depending on current time and config settings. 415 | Also, initialize the order_book 416 | """ 417 | for coin in self.coin: 418 | if self.coin[coin]['CYCLE'].lower() == 'minutely': 419 | 420 | # only for testing purpose 421 | if not self.cfg['TEST']: 422 | error_string = 'Cycle "minutely" is only available in TEST mode.' 423 | logging.error(error_string) 424 | raise Exception(error_string) 425 | 426 | # this is for testing mode only, buy every minute starting from now! 427 | self.coin[coin]['SCHEDULE'] = datetime.datetime.now() 428 | 429 | elif self.coin[coin]['CYCLE'].lower() == 'daily': 430 | at_time = get_hour_minute(self.coin[coin]['AT_TIME']) 431 | scheduled_datetime = datetime.datetime.combine(datetime.date.today(), 432 | datetime.time(at_time[0], at_time[1])) 433 | # Check if scheduled datetime has passed 434 | if scheduled_datetime < datetime.datetime.today(): 435 | scheduled_datetime = scheduled_datetime + datetime.timedelta(days=1) # Add one day 436 | self.coin[coin]['SCHEDULE'] = scheduled_datetime 437 | 438 | elif 'weekly' in self.coin[coin]['CYCLE'].lower(): 439 | at_time = get_hour_minute(self.coin[coin]['AT_TIME']) 440 | on_weekday = get_on_weekday(self.coin[coin]['ON_WEEKDAY']) 441 | today = datetime.date.today() 442 | today + datetime.timedelta((on_weekday - today.weekday()) % 7) 443 | scheduled_datetime = datetime.datetime.combine(today + datetime.timedelta((on_weekday - today.weekday()) % 7), 444 | datetime.time(at_time[0], at_time[1])) 445 | if scheduled_datetime < datetime.datetime.today(): 446 | # if 'bi-weekly' in self.coin[coin]['CYCLE']: 447 | # scheduled_datetime = scheduled_datetime + datetime.timedelta(days=14) 448 | # else: 449 | # the above block is commented. In this way you wait 1 week in the worst case scenario even 450 | # for the bi-weekly 451 | scheduled_datetime = scheduled_datetime + datetime.timedelta(days=7) 452 | 453 | # we have a little complication with the bi-weekly cycle. We have to consult the order_book (if exists) 454 | # to decide which week to use (in case the bot was restarted) 455 | if 'bi-weekly' in self.coin[coin]['CYCLE'].lower() and self.order_book_path.exists(): 456 | df = read_csv_custom(self.order_book_path) 457 | previously = None 458 | for cn in df.index: 459 | if cn == coin and df.loc[cn]['Cycle'] == 'bi-weekly': 460 | previously = df.loc[cn]['Purchase Time'] 461 | if previously: 462 | previously = datetime.datetime.strptime(previously, '%Y-%m-%d %H:%M:%S') 463 | if previously == scheduled_datetime + datetime.timedelta(days=7): 464 | scheduled_datetime = previously 465 | self.coin[coin]['SCHEDULE'] = scheduled_datetime 466 | 467 | elif self.coin[coin]['CYCLE'].lower() == 'monthly': 468 | at_time = get_hour_minute(self.coin[coin]['AT_TIME']) 469 | on_day = get_on_day(self.coin[coin]['ON_DAY']) 470 | today = datetime.datetime.today() 471 | scheduled_datetime = datetime.datetime.combine(datetime.datetime(today.year, today.month, on_day), 472 | datetime.time(at_time[0], at_time[1])) 473 | if scheduled_datetime < datetime.datetime.today(): 474 | scheduled_datetime = scheduled_datetime + relativedelta(months=1) 475 | self.coin[coin]['SCHEDULE'] = scheduled_datetime 476 | 477 | else: 478 | error_string = 'Cycle not recognized. Valid cycle strings are: "daily", "weekly", ' \ 479 | '"bi-weekly" and "monthly".' 480 | logging.error(error_string) 481 | raise Exception(error_string) 482 | 483 | for coin in self.coin: 484 | # create the order book 485 | self.order_book[coin] = self.coin[coin]['SCHEDULE'] 486 | 487 | # Define symbol variable 488 | self.coin[coin]['SYMBOL'] = coin + '/' + self.coin[coin]['PAIRING'] 489 | 490 | # set the error variables 491 | self.coin[coin]['LASTERROR'] = [] 492 | self.coin[coin]['ERROR_ATTEMPT'] = 0 493 | 494 | 495 | if __name__ == "__main__": 496 | 497 | cfg_path = 'config/config.yml' 498 | api_path = 'auth/API_keys.yml' 499 | 500 | # Run the bot 501 | Dca(cfg_path, api_path) 502 | -------------------------------------------------------------------------------- /trades_example/orders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "info": { 4 | "symbol": "BTCUSDT", 5 | "orderId": "11407420", 6 | "orderListId": "-1", 7 | "clientOrderId": "x-R4BD3S823416419e79ab20214824f1", 8 | "transactTime": "1651429214545", 9 | "price": "0.00000000", 10 | "origQty": "0.00065200", 11 | "executedQty": "0.00065200", 12 | "cummulativeQuoteQty": "24.98880628", 13 | "status": "FILLED", 14 | "timeInForce": "GTC", 15 | "type": "MARKET", 16 | "side": "BUY", 17 | "fills": [ 18 | { 19 | "price": "38326.39000000", 20 | "qty": "0.00065200", 21 | "commission": "0.00000000", 22 | "commissionAsset": "BTC", 23 | "tradeId": "3289726" 24 | } 25 | ] 26 | }, 27 | "id": "11407420", 28 | "clientOrderId": "x-R4BD3S823416419e79ab20214824f1", 29 | "timestamp": 1651429214545, 30 | "datetime": "2022-05-01T18:20:14.545Z", 31 | "lastTradeTimestamp": null, 32 | "symbol": "BTC/USDT", 33 | "type": "market", 34 | "timeInForce": "IOC", 35 | "postOnly": false, 36 | "side": "buy", 37 | "price": 38326.39, 38 | "stopPrice": null, 39 | "amount": 0.000652, 40 | "cost": 24.98880628, 41 | "average": 38326.39, 42 | "filled": 0.000652, 43 | "remaining": 0.0, 44 | "status": "closed", 45 | "fee": { 46 | "currency": "BTC", 47 | "cost": 0.0 48 | }, 49 | "trades": [ 50 | { 51 | "info": { 52 | "price": "38326.39000000", 53 | "qty": "0.00065200", 54 | "commission": "0.00000000", 55 | "commissionAsset": "BTC", 56 | "tradeId": "3289726" 57 | }, 58 | "timestamp": null, 59 | "datetime": null, 60 | "symbol": "BTC/USDT", 61 | "id": "3289726", 62 | "order": "11407420", 63 | "type": "market", 64 | "side": "buy", 65 | "takerOrMaker": null, 66 | "price": 38326.39, 67 | "amount": 0.000652, 68 | "cost": 24.98880628, 69 | "fee": { 70 | "cost": 0.0, 71 | "currency": "BTC" 72 | }, 73 | "fees": [ 74 | { 75 | "currency": "BTC", 76 | "cost": "0.00000000" 77 | } 78 | ] 79 | } 80 | ], 81 | "fees": [ 82 | { 83 | "currency": "BTC", 84 | "cost": 0.0 85 | } 86 | ] 87 | }, 88 | { 89 | "info": { 90 | "symbol": "XRPBUSD", 91 | "orderId": "484596", 92 | "orderListId": "-1", 93 | "clientOrderId": "x-R4BD3S82c3fe3d583119cc3bb3fa85", 94 | "transactTime": "1651429217912", 95 | "price": "0.00000000", 96 | "origQty": "19.60000000", 97 | "executedQty": "19.60000000", 98 | "cummulativeQuoteQty": "14.96264000", 99 | "status": "FILLED", 100 | "timeInForce": "GTC", 101 | "type": "MARKET", 102 | "side": "BUY", 103 | "fills": [ 104 | { 105 | "price": "0.76340000", 106 | "qty": "19.60000000", 107 | "commission": "0.00000000", 108 | "commissionAsset": "XRP", 109 | "tradeId": "68426" 110 | } 111 | ] 112 | }, 113 | "id": "484596", 114 | "clientOrderId": "x-R4BD3S82c3fe3d583119cc3bb3fa85", 115 | "timestamp": 1651429217912, 116 | "datetime": "2022-05-01T18:20:17.912Z", 117 | "lastTradeTimestamp": null, 118 | "symbol": "XRP/BUSD", 119 | "type": "market", 120 | "timeInForce": "IOC", 121 | "postOnly": false, 122 | "side": "buy", 123 | "price": 0.7634, 124 | "stopPrice": null, 125 | "amount": 19.6, 126 | "cost": 14.96264, 127 | "average": 0.7634, 128 | "filled": 19.6, 129 | "remaining": 0.0, 130 | "status": "closed", 131 | "fee": { 132 | "currency": "XRP", 133 | "cost": 0.0 134 | }, 135 | "trades": [ 136 | { 137 | "info": { 138 | "price": "0.76340000", 139 | "qty": "19.60000000", 140 | "commission": "0.00000000", 141 | "commissionAsset": "XRP", 142 | "tradeId": "68426" 143 | }, 144 | "timestamp": null, 145 | "datetime": null, 146 | "symbol": "XRP/BUSD", 147 | "id": "68426", 148 | "order": "484596", 149 | "type": "market", 150 | "side": "buy", 151 | "takerOrMaker": null, 152 | "price": 0.7634, 153 | "amount": 19.6, 154 | "cost": 14.96264, 155 | "fee": { 156 | "cost": 0.0, 157 | "currency": "XRP" 158 | }, 159 | "fees": [ 160 | { 161 | "currency": "XRP", 162 | "cost": "0.00000000" 163 | } 164 | ] 165 | } 166 | ], 167 | "fees": [ 168 | { 169 | "currency": "XRP", 170 | "cost": 0.0 171 | } 172 | ] 173 | }, 174 | { 175 | "info": { 176 | "symbol": "LTCBUSD", 177 | "orderId": "61033", 178 | "orderListId": "-1", 179 | "clientOrderId": "x-R4BD3S82ae98cea1545e0985692a87", 180 | "transactTime": "1651429221804", 181 | "price": "0.00000000", 182 | "origQty": "0.24850000", 183 | "executedQty": "0.24850000", 184 | "cummulativeQuoteQty": "24.99910000", 185 | "status": "FILLED", 186 | "timeInForce": "GTC", 187 | "type": "MARKET", 188 | "side": "BUY", 189 | "fills": [ 190 | { 191 | "price": "100.60000000", 192 | "qty": "0.24850000", 193 | "commission": "0.00000000", 194 | "commissionAsset": "LTC", 195 | "tradeId": "10655" 196 | } 197 | ] 198 | }, 199 | "id": "61033", 200 | "clientOrderId": "x-R4BD3S82ae98cea1545e0985692a87", 201 | "timestamp": 1651429221804, 202 | "datetime": "2022-05-01T18:20:21.804Z", 203 | "lastTradeTimestamp": null, 204 | "symbol": "LTC/BUSD", 205 | "type": "market", 206 | "timeInForce": "IOC", 207 | "postOnly": false, 208 | "side": "buy", 209 | "price": 100.6, 210 | "stopPrice": null, 211 | "amount": 0.2485, 212 | "cost": 24.9991, 213 | "average": 100.6, 214 | "filled": 0.2485, 215 | "remaining": 0.0, 216 | "status": "closed", 217 | "fee": { 218 | "currency": "LTC", 219 | "cost": 0.0 220 | }, 221 | "trades": [ 222 | { 223 | "info": { 224 | "price": "100.60000000", 225 | "qty": "0.24850000", 226 | "commission": "0.00000000", 227 | "commissionAsset": "LTC", 228 | "tradeId": "10655" 229 | }, 230 | "timestamp": null, 231 | "datetime": null, 232 | "symbol": "LTC/BUSD", 233 | "id": "10655", 234 | "order": "61033", 235 | "type": "market", 236 | "side": "buy", 237 | "takerOrMaker": null, 238 | "price": 100.6, 239 | "amount": 0.2485, 240 | "cost": 24.9991, 241 | "fee": { 242 | "cost": 0.0, 243 | "currency": "LTC" 244 | }, 245 | "fees": [ 246 | { 247 | "currency": "LTC", 248 | "cost": "0.00000000" 249 | } 250 | ] 251 | } 252 | ], 253 | "fees": [ 254 | { 255 | "currency": "LTC", 256 | "cost": 0.0 257 | } 258 | ] 259 | }, 260 | { 261 | "info": { 262 | "symbol": "BTCUSDT", 263 | "orderId": "11407686", 264 | "orderListId": "-1", 265 | "clientOrderId": "x-R4BD3S829ece2292e67b8584406731", 266 | "transactTime": "1651429271674", 267 | "price": "0.00000000", 268 | "origQty": "0.00065300", 269 | "executedQty": "0.00065300", 270 | "cummulativeQuoteQty": "24.97899351", 271 | "status": "FILLED", 272 | "timeInForce": "GTC", 273 | "type": "MARKET", 274 | "side": "BUY", 275 | "fills": [ 276 | { 277 | "price": "38252.67000000", 278 | "qty": "0.00065300", 279 | "commission": "0.00000000", 280 | "commissionAsset": "BTC", 281 | "tradeId": "3289833" 282 | } 283 | ] 284 | }, 285 | "id": "11407686", 286 | "clientOrderId": "x-R4BD3S829ece2292e67b8584406731", 287 | "timestamp": 1651429271674, 288 | "datetime": "2022-05-01T18:21:11.674Z", 289 | "lastTradeTimestamp": null, 290 | "symbol": "BTC/USDT", 291 | "type": "market", 292 | "timeInForce": "IOC", 293 | "postOnly": false, 294 | "side": "buy", 295 | "price": 38252.67, 296 | "stopPrice": null, 297 | "amount": 0.000653, 298 | "cost": 24.97899351, 299 | "average": 38252.67, 300 | "filled": 0.000653, 301 | "remaining": 0.0, 302 | "status": "closed", 303 | "fee": { 304 | "currency": "BTC", 305 | "cost": 0.0 306 | }, 307 | "trades": [ 308 | { 309 | "info": { 310 | "price": "38252.67000000", 311 | "qty": "0.00065300", 312 | "commission": "0.00000000", 313 | "commissionAsset": "BTC", 314 | "tradeId": "3289833" 315 | }, 316 | "timestamp": null, 317 | "datetime": null, 318 | "symbol": "BTC/USDT", 319 | "id": "3289833", 320 | "order": "11407686", 321 | "type": "market", 322 | "side": "buy", 323 | "takerOrMaker": null, 324 | "price": 38252.67, 325 | "amount": 0.000653, 326 | "cost": 24.97899351, 327 | "fee": { 328 | "cost": 0.0, 329 | "currency": "BTC" 330 | }, 331 | "fees": [ 332 | { 333 | "currency": "BTC", 334 | "cost": "0.00000000" 335 | } 336 | ] 337 | } 338 | ], 339 | "fees": [ 340 | { 341 | "currency": "BTC", 342 | "cost": 0.0 343 | } 344 | ] 345 | }, 346 | { 347 | "info": { 348 | "symbol": "XRPBUSD", 349 | "orderId": "484597", 350 | "orderListId": "-1", 351 | "clientOrderId": "x-R4BD3S82d5ac0be80ac96e5fff797d", 352 | "transactTime": "1651429275046", 353 | "price": "0.00000000", 354 | "origQty": "19.60000000", 355 | "executedQty": "19.60000000", 356 | "cummulativeQuoteQty": "14.96264000", 357 | "status": "FILLED", 358 | "timeInForce": "GTC", 359 | "type": "MARKET", 360 | "side": "BUY", 361 | "fills": [ 362 | { 363 | "price": "0.76340000", 364 | "qty": "19.60000000", 365 | "commission": "0.00000000", 366 | "commissionAsset": "XRP", 367 | "tradeId": "68427" 368 | } 369 | ] 370 | }, 371 | "id": "484597", 372 | "clientOrderId": "x-R4BD3S82d5ac0be80ac96e5fff797d", 373 | "timestamp": 1651429275046, 374 | "datetime": "2022-05-01T18:21:15.046Z", 375 | "lastTradeTimestamp": null, 376 | "symbol": "XRP/BUSD", 377 | "type": "market", 378 | "timeInForce": "IOC", 379 | "postOnly": false, 380 | "side": "buy", 381 | "price": 0.7634, 382 | "stopPrice": null, 383 | "amount": 19.6, 384 | "cost": 14.96264, 385 | "average": 0.7634, 386 | "filled": 19.6, 387 | "remaining": 0.0, 388 | "status": "closed", 389 | "fee": { 390 | "currency": "XRP", 391 | "cost": 0.0 392 | }, 393 | "trades": [ 394 | { 395 | "info": { 396 | "price": "0.76340000", 397 | "qty": "19.60000000", 398 | "commission": "0.00000000", 399 | "commissionAsset": "XRP", 400 | "tradeId": "68427" 401 | }, 402 | "timestamp": null, 403 | "datetime": null, 404 | "symbol": "XRP/BUSD", 405 | "id": "68427", 406 | "order": "484597", 407 | "type": "market", 408 | "side": "buy", 409 | "takerOrMaker": null, 410 | "price": 0.7634, 411 | "amount": 19.6, 412 | "cost": 14.96264, 413 | "fee": { 414 | "cost": 0.0, 415 | "currency": "XRP" 416 | }, 417 | "fees": [ 418 | { 419 | "currency": "XRP", 420 | "cost": "0.00000000" 421 | } 422 | ] 423 | } 424 | ], 425 | "fees": [ 426 | { 427 | "currency": "XRP", 428 | "cost": 0.0 429 | } 430 | ] 431 | }, 432 | { 433 | "info": { 434 | "symbol": "LTCBUSD", 435 | "orderId": "61034", 436 | "orderListId": "-1", 437 | "clientOrderId": "x-R4BD3S821bfd18548a8610ae8d8ec7", 438 | "transactTime": "1651429278792", 439 | "price": "0.00000000", 440 | "origQty": "0.24850000", 441 | "executedQty": "0.24850000", 442 | "cummulativeQuoteQty": "24.99910000", 443 | "status": "FILLED", 444 | "timeInForce": "GTC", 445 | "type": "MARKET", 446 | "side": "BUY", 447 | "fills": [ 448 | { 449 | "price": "100.60000000", 450 | "qty": "0.24850000", 451 | "commission": "0.00000000", 452 | "commissionAsset": "LTC", 453 | "tradeId": "10656" 454 | } 455 | ] 456 | }, 457 | "id": "61034", 458 | "clientOrderId": "x-R4BD3S821bfd18548a8610ae8d8ec7", 459 | "timestamp": 1651429278792, 460 | "datetime": "2022-05-01T18:21:18.792Z", 461 | "lastTradeTimestamp": null, 462 | "symbol": "LTC/BUSD", 463 | "type": "market", 464 | "timeInForce": "IOC", 465 | "postOnly": false, 466 | "side": "buy", 467 | "price": 100.6, 468 | "stopPrice": null, 469 | "amount": 0.2485, 470 | "cost": 24.9991, 471 | "average": 100.6, 472 | "filled": 0.2485, 473 | "remaining": 0.0, 474 | "status": "closed", 475 | "fee": { 476 | "currency": "LTC", 477 | "cost": 0.0 478 | }, 479 | "trades": [ 480 | { 481 | "info": { 482 | "price": "100.60000000", 483 | "qty": "0.24850000", 484 | "commission": "0.00000000", 485 | "commissionAsset": "LTC", 486 | "tradeId": "10656" 487 | }, 488 | "timestamp": null, 489 | "datetime": null, 490 | "symbol": "LTC/BUSD", 491 | "id": "10656", 492 | "order": "61034", 493 | "type": "market", 494 | "side": "buy", 495 | "takerOrMaker": null, 496 | "price": 100.6, 497 | "amount": 0.2485, 498 | "cost": 24.9991, 499 | "fee": { 500 | "cost": 0.0, 501 | "currency": "LTC" 502 | }, 503 | "fees": [ 504 | { 505 | "currency": "LTC", 506 | "cost": "0.00000000" 507 | } 508 | ] 509 | } 510 | ], 511 | "fees": [ 512 | { 513 | "currency": "LTC", 514 | "cost": 0.0 515 | } 516 | ] 517 | }, 518 | { 519 | "info": { 520 | "symbol": "BTCUSDT", 521 | "orderId": "11407951", 522 | "orderListId": "-1", 523 | "clientOrderId": "x-R4BD3S82d5bbe48dcee30f865f5bfe", 524 | "transactTime": "1651429331654", 525 | "price": "0.00000000", 526 | "origQty": "0.00065300", 527 | "executedQty": "0.00065300", 528 | "cummulativeQuoteQty": "24.98059336", 529 | "status": "FILLED", 530 | "timeInForce": "GTC", 531 | "type": "MARKET", 532 | "side": "BUY", 533 | "fills": [ 534 | { 535 | "price": "38255.12000000", 536 | "qty": "0.00065300", 537 | "commission": "0.00000000", 538 | "commissionAsset": "BTC", 539 | "tradeId": "3289980" 540 | } 541 | ] 542 | }, 543 | "id": "11407951", 544 | "clientOrderId": "x-R4BD3S82d5bbe48dcee30f865f5bfe", 545 | "timestamp": 1651429331654, 546 | "datetime": "2022-05-01T18:22:11.654Z", 547 | "lastTradeTimestamp": null, 548 | "symbol": "BTC/USDT", 549 | "type": "market", 550 | "timeInForce": "IOC", 551 | "postOnly": false, 552 | "side": "buy", 553 | "price": 38255.12, 554 | "stopPrice": null, 555 | "amount": 0.000653, 556 | "cost": 24.98059336, 557 | "average": 38255.12, 558 | "filled": 0.000653, 559 | "remaining": 0.0, 560 | "status": "closed", 561 | "fee": { 562 | "currency": "BTC", 563 | "cost": 0.0 564 | }, 565 | "trades": [ 566 | { 567 | "info": { 568 | "price": "38255.12000000", 569 | "qty": "0.00065300", 570 | "commission": "0.00000000", 571 | "commissionAsset": "BTC", 572 | "tradeId": "3289980" 573 | }, 574 | "timestamp": null, 575 | "datetime": null, 576 | "symbol": "BTC/USDT", 577 | "id": "3289980", 578 | "order": "11407951", 579 | "type": "market", 580 | "side": "buy", 581 | "takerOrMaker": null, 582 | "price": 38255.12, 583 | "amount": 0.000653, 584 | "cost": 24.98059336, 585 | "fee": { 586 | "cost": 0.0, 587 | "currency": "BTC" 588 | }, 589 | "fees": [ 590 | { 591 | "currency": "BTC", 592 | "cost": "0.00000000" 593 | } 594 | ] 595 | } 596 | ], 597 | "fees": [ 598 | { 599 | "currency": "BTC", 600 | "cost": 0.0 601 | } 602 | ] 603 | }, 604 | { 605 | "info": { 606 | "symbol": "XRPBUSD", 607 | "orderId": "484598", 608 | "orderListId": "-1", 609 | "clientOrderId": "x-R4BD3S82663cd562b0e386f2dd3f78", 610 | "transactTime": "1651429335023", 611 | "price": "0.00000000", 612 | "origQty": "19.60000000", 613 | "executedQty": "19.60000000", 614 | "cummulativeQuoteQty": "14.96264000", 615 | "status": "FILLED", 616 | "timeInForce": "GTC", 617 | "type": "MARKET", 618 | "side": "BUY", 619 | "fills": [ 620 | { 621 | "price": "0.76340000", 622 | "qty": "19.60000000", 623 | "commission": "0.00000000", 624 | "commissionAsset": "XRP", 625 | "tradeId": "68428" 626 | } 627 | ] 628 | }, 629 | "id": "484598", 630 | "clientOrderId": "x-R4BD3S82663cd562b0e386f2dd3f78", 631 | "timestamp": 1651429335023, 632 | "datetime": "2022-05-01T18:22:15.023Z", 633 | "lastTradeTimestamp": null, 634 | "symbol": "XRP/BUSD", 635 | "type": "market", 636 | "timeInForce": "IOC", 637 | "postOnly": false, 638 | "side": "buy", 639 | "price": 0.7634, 640 | "stopPrice": null, 641 | "amount": 19.6, 642 | "cost": 14.96264, 643 | "average": 0.7634, 644 | "filled": 19.6, 645 | "remaining": 0.0, 646 | "status": "closed", 647 | "fee": { 648 | "currency": "XRP", 649 | "cost": 0.0 650 | }, 651 | "trades": [ 652 | { 653 | "info": { 654 | "price": "0.76340000", 655 | "qty": "19.60000000", 656 | "commission": "0.00000000", 657 | "commissionAsset": "XRP", 658 | "tradeId": "68428" 659 | }, 660 | "timestamp": null, 661 | "datetime": null, 662 | "symbol": "XRP/BUSD", 663 | "id": "68428", 664 | "order": "484598", 665 | "type": "market", 666 | "side": "buy", 667 | "takerOrMaker": null, 668 | "price": 0.7634, 669 | "amount": 19.6, 670 | "cost": 14.96264, 671 | "fee": { 672 | "cost": 0.0, 673 | "currency": "XRP" 674 | }, 675 | "fees": [ 676 | { 677 | "currency": "XRP", 678 | "cost": "0.00000000" 679 | } 680 | ] 681 | } 682 | ], 683 | "fees": [ 684 | { 685 | "currency": "XRP", 686 | "cost": 0.0 687 | } 688 | ] 689 | }, 690 | { 691 | "info": { 692 | "symbol": "LTCBUSD", 693 | "orderId": "61035", 694 | "orderListId": "-1", 695 | "clientOrderId": "x-R4BD3S828c7ed8680ec1f3aa83a38a", 696 | "transactTime": "1651429338681", 697 | "price": "0.00000000", 698 | "origQty": "0.24850000", 699 | "executedQty": "0.24850000", 700 | "cummulativeQuoteQty": "24.99910000", 701 | "status": "FILLED", 702 | "timeInForce": "GTC", 703 | "type": "MARKET", 704 | "side": "BUY", 705 | "fills": [ 706 | { 707 | "price": "100.60000000", 708 | "qty": "0.24850000", 709 | "commission": "0.00000000", 710 | "commissionAsset": "LTC", 711 | "tradeId": "10657" 712 | } 713 | ] 714 | }, 715 | "id": "61035", 716 | "clientOrderId": "x-R4BD3S828c7ed8680ec1f3aa83a38a", 717 | "timestamp": 1651429338681, 718 | "datetime": "2022-05-01T18:22:18.681Z", 719 | "lastTradeTimestamp": null, 720 | "symbol": "LTC/BUSD", 721 | "type": "market", 722 | "timeInForce": "IOC", 723 | "postOnly": false, 724 | "side": "buy", 725 | "price": 100.6, 726 | "stopPrice": null, 727 | "amount": 0.2485, 728 | "cost": 24.9991, 729 | "average": 100.6, 730 | "filled": 0.2485, 731 | "remaining": 0.0, 732 | "status": "closed", 733 | "fee": { 734 | "currency": "LTC", 735 | "cost": 0.0 736 | }, 737 | "trades": [ 738 | { 739 | "info": { 740 | "price": "100.60000000", 741 | "qty": "0.24850000", 742 | "commission": "0.00000000", 743 | "commissionAsset": "LTC", 744 | "tradeId": "10657" 745 | }, 746 | "timestamp": null, 747 | "datetime": null, 748 | "symbol": "LTC/BUSD", 749 | "id": "10657", 750 | "order": "61035", 751 | "type": "market", 752 | "side": "buy", 753 | "takerOrMaker": null, 754 | "price": 100.6, 755 | "amount": 0.2485, 756 | "cost": 24.9991, 757 | "fee": { 758 | "cost": 0.0, 759 | "currency": "LTC" 760 | }, 761 | "fees": [ 762 | { 763 | "currency": "LTC", 764 | "cost": "0.00000000" 765 | } 766 | ] 767 | } 768 | ], 769 | "fees": [ 770 | { 771 | "currency": "LTC", 772 | "cost": 0.0 773 | } 774 | ] 775 | }, 776 | { 777 | "info": { 778 | "symbol": "BTCUSDT", 779 | "orderId": "11408221", 780 | "orderListId": "-1", 781 | "clientOrderId": "x-R4BD3S82f73d2401363d85c403bd46", 782 | "transactTime": "1651429391674", 783 | "price": "0.00000000", 784 | "origQty": "0.00065300", 785 | "executedQty": "0.00065300", 786 | "cummulativeQuoteQty": "24.97111833", 787 | "status": "FILLED", 788 | "timeInForce": "GTC", 789 | "type": "MARKET", 790 | "side": "BUY", 791 | "fills": [ 792 | { 793 | "price": "38240.61000000", 794 | "qty": "0.00065300", 795 | "commission": "0.00000000", 796 | "commissionAsset": "BTC", 797 | "tradeId": "3290094" 798 | } 799 | ] 800 | }, 801 | "id": "11408221", 802 | "clientOrderId": "x-R4BD3S82f73d2401363d85c403bd46", 803 | "timestamp": 1651429391674, 804 | "datetime": "2022-05-01T18:23:11.674Z", 805 | "lastTradeTimestamp": null, 806 | "symbol": "BTC/USDT", 807 | "type": "market", 808 | "timeInForce": "IOC", 809 | "postOnly": false, 810 | "side": "buy", 811 | "price": 38240.61, 812 | "stopPrice": null, 813 | "amount": 0.000653, 814 | "cost": 24.97111833, 815 | "average": 38240.61, 816 | "filled": 0.000653, 817 | "remaining": 0.0, 818 | "status": "closed", 819 | "fee": { 820 | "currency": "BTC", 821 | "cost": 0.0 822 | }, 823 | "trades": [ 824 | { 825 | "info": { 826 | "price": "38240.61000000", 827 | "qty": "0.00065300", 828 | "commission": "0.00000000", 829 | "commissionAsset": "BTC", 830 | "tradeId": "3290094" 831 | }, 832 | "timestamp": null, 833 | "datetime": null, 834 | "symbol": "BTC/USDT", 835 | "id": "3290094", 836 | "order": "11408221", 837 | "type": "market", 838 | "side": "buy", 839 | "takerOrMaker": null, 840 | "price": 38240.61, 841 | "amount": 0.000653, 842 | "cost": 24.97111833, 843 | "fee": { 844 | "cost": 0.0, 845 | "currency": "BTC" 846 | }, 847 | "fees": [ 848 | { 849 | "currency": "BTC", 850 | "cost": "0.00000000" 851 | } 852 | ] 853 | } 854 | ], 855 | "fees": [ 856 | { 857 | "currency": "BTC", 858 | "cost": 0.0 859 | } 860 | ] 861 | }, 862 | { 863 | "info": { 864 | "symbol": "XRPBUSD", 865 | "orderId": "484599", 866 | "orderListId": "-1", 867 | "clientOrderId": "x-R4BD3S82438999a157d193b97de876", 868 | "transactTime": "1651429395490", 869 | "price": "0.00000000", 870 | "origQty": "19.60000000", 871 | "executedQty": "19.60000000", 872 | "cummulativeQuoteQty": "14.96264000", 873 | "status": "FILLED", 874 | "timeInForce": "GTC", 875 | "type": "MARKET", 876 | "side": "BUY", 877 | "fills": [ 878 | { 879 | "price": "0.76340000", 880 | "qty": "19.60000000", 881 | "commission": "0.00000000", 882 | "commissionAsset": "XRP", 883 | "tradeId": "68429" 884 | } 885 | ] 886 | }, 887 | "id": "484599", 888 | "clientOrderId": "x-R4BD3S82438999a157d193b97de876", 889 | "timestamp": 1651429395490, 890 | "datetime": "2022-05-01T18:23:15.490Z", 891 | "lastTradeTimestamp": null, 892 | "symbol": "XRP/BUSD", 893 | "type": "market", 894 | "timeInForce": "IOC", 895 | "postOnly": false, 896 | "side": "buy", 897 | "price": 0.7634, 898 | "stopPrice": null, 899 | "amount": 19.6, 900 | "cost": 14.96264, 901 | "average": 0.7634, 902 | "filled": 19.6, 903 | "remaining": 0.0, 904 | "status": "closed", 905 | "fee": { 906 | "currency": "XRP", 907 | "cost": 0.0 908 | }, 909 | "trades": [ 910 | { 911 | "info": { 912 | "price": "0.76340000", 913 | "qty": "19.60000000", 914 | "commission": "0.00000000", 915 | "commissionAsset": "XRP", 916 | "tradeId": "68429" 917 | }, 918 | "timestamp": null, 919 | "datetime": null, 920 | "symbol": "XRP/BUSD", 921 | "id": "68429", 922 | "order": "484599", 923 | "type": "market", 924 | "side": "buy", 925 | "takerOrMaker": null, 926 | "price": 0.7634, 927 | "amount": 19.6, 928 | "cost": 14.96264, 929 | "fee": { 930 | "cost": 0.0, 931 | "currency": "XRP" 932 | }, 933 | "fees": [ 934 | { 935 | "currency": "XRP", 936 | "cost": "0.00000000" 937 | } 938 | ] 939 | } 940 | ], 941 | "fees": [ 942 | { 943 | "currency": "XRP", 944 | "cost": 0.0 945 | } 946 | ] 947 | }, 948 | { 949 | "info": { 950 | "symbol": "LTCBUSD", 951 | "orderId": "61036", 952 | "orderListId": "-1", 953 | "clientOrderId": "x-R4BD3S82aa45c905369f66d0d2a36f", 954 | "transactTime": "1651429399317", 955 | "price": "0.00000000", 956 | "origQty": "0.24850000", 957 | "executedQty": "0.24850000", 958 | "cummulativeQuoteQty": "24.99910000", 959 | "status": "FILLED", 960 | "timeInForce": "GTC", 961 | "type": "MARKET", 962 | "side": "BUY", 963 | "fills": [ 964 | { 965 | "price": "100.60000000", 966 | "qty": "0.24850000", 967 | "commission": "0.00000000", 968 | "commissionAsset": "LTC", 969 | "tradeId": "10658" 970 | } 971 | ] 972 | }, 973 | "id": "61036", 974 | "clientOrderId": "x-R4BD3S82aa45c905369f66d0d2a36f", 975 | "timestamp": 1651429399317, 976 | "datetime": "2022-05-01T18:23:19.317Z", 977 | "lastTradeTimestamp": null, 978 | "symbol": "LTC/BUSD", 979 | "type": "market", 980 | "timeInForce": "IOC", 981 | "postOnly": false, 982 | "side": "buy", 983 | "price": 100.6, 984 | "stopPrice": null, 985 | "amount": 0.2485, 986 | "cost": 24.9991, 987 | "average": 100.6, 988 | "filled": 0.2485, 989 | "remaining": 0.0, 990 | "status": "closed", 991 | "fee": { 992 | "currency": "LTC", 993 | "cost": 0.0 994 | }, 995 | "trades": [ 996 | { 997 | "info": { 998 | "price": "100.60000000", 999 | "qty": "0.24850000", 1000 | "commission": "0.00000000", 1001 | "commissionAsset": "LTC", 1002 | "tradeId": "10658" 1003 | }, 1004 | "timestamp": null, 1005 | "datetime": null, 1006 | "symbol": "LTC/BUSD", 1007 | "id": "10658", 1008 | "order": "61036", 1009 | "type": "market", 1010 | "side": "buy", 1011 | "takerOrMaker": null, 1012 | "price": 100.6, 1013 | "amount": 0.2485, 1014 | "cost": 24.9991, 1015 | "fee": { 1016 | "cost": 0.0, 1017 | "currency": "LTC" 1018 | }, 1019 | "fees": [ 1020 | { 1021 | "currency": "LTC", 1022 | "cost": "0.00000000" 1023 | } 1024 | ] 1025 | } 1026 | ], 1027 | "fees": [ 1028 | { 1029 | "currency": "LTC", 1030 | "cost": 0.0 1031 | } 1032 | ] 1033 | }, 1034 | { 1035 | "info": { 1036 | "symbol": "BTCUSDT", 1037 | "orderId": "11408483", 1038 | "orderListId": "-1", 1039 | "clientOrderId": "x-R4BD3S8245fa7cb80d84672cdab3f8", 1040 | "transactTime": "1651429451656", 1041 | "price": "0.00000000", 1042 | "origQty": "0.00065300", 1043 | "executedQty": "0.00065300", 1044 | "cummulativeQuoteQty": "24.98061295", 1045 | "status": "FILLED", 1046 | "timeInForce": "GTC", 1047 | "type": "MARKET", 1048 | "side": "BUY", 1049 | "fills": [ 1050 | { 1051 | "price": "38255.15000000", 1052 | "qty": "0.00065300", 1053 | "commission": "0.00000000", 1054 | "commissionAsset": "BTC", 1055 | "tradeId": "3290152" 1056 | } 1057 | ] 1058 | }, 1059 | "id": "11408483", 1060 | "clientOrderId": "x-R4BD3S8245fa7cb80d84672cdab3f8", 1061 | "timestamp": 1651429451656, 1062 | "datetime": "2022-05-01T18:24:11.656Z", 1063 | "lastTradeTimestamp": null, 1064 | "symbol": "BTC/USDT", 1065 | "type": "market", 1066 | "timeInForce": "IOC", 1067 | "postOnly": false, 1068 | "side": "buy", 1069 | "price": 38255.15, 1070 | "stopPrice": null, 1071 | "amount": 0.000653, 1072 | "cost": 24.98061295, 1073 | "average": 38255.15, 1074 | "filled": 0.000653, 1075 | "remaining": 0.0, 1076 | "status": "closed", 1077 | "fee": { 1078 | "currency": "BTC", 1079 | "cost": 0.0 1080 | }, 1081 | "trades": [ 1082 | { 1083 | "info": { 1084 | "price": "38255.15000000", 1085 | "qty": "0.00065300", 1086 | "commission": "0.00000000", 1087 | "commissionAsset": "BTC", 1088 | "tradeId": "3290152" 1089 | }, 1090 | "timestamp": null, 1091 | "datetime": null, 1092 | "symbol": "BTC/USDT", 1093 | "id": "3290152", 1094 | "order": "11408483", 1095 | "type": "market", 1096 | "side": "buy", 1097 | "takerOrMaker": null, 1098 | "price": 38255.15, 1099 | "amount": 0.000653, 1100 | "cost": 24.98061295, 1101 | "fee": { 1102 | "cost": 0.0, 1103 | "currency": "BTC" 1104 | }, 1105 | "fees": [ 1106 | { 1107 | "currency": "BTC", 1108 | "cost": "0.00000000" 1109 | } 1110 | ] 1111 | } 1112 | ], 1113 | "fees": [ 1114 | { 1115 | "currency": "BTC", 1116 | "cost": 0.0 1117 | } 1118 | ] 1119 | }, 1120 | { 1121 | "info": { 1122 | "symbol": "XRPBUSD", 1123 | "orderId": "484600", 1124 | "orderListId": "-1", 1125 | "clientOrderId": "x-R4BD3S82ab5a3898147087df422499", 1126 | "transactTime": "1651429454828", 1127 | "price": "0.00000000", 1128 | "origQty": "19.60000000", 1129 | "executedQty": "19.60000000", 1130 | "cummulativeQuoteQty": "14.96264000", 1131 | "status": "FILLED", 1132 | "timeInForce": "GTC", 1133 | "type": "MARKET", 1134 | "side": "BUY", 1135 | "fills": [ 1136 | { 1137 | "price": "0.76340000", 1138 | "qty": "19.60000000", 1139 | "commission": "0.00000000", 1140 | "commissionAsset": "XRP", 1141 | "tradeId": "68430" 1142 | } 1143 | ] 1144 | }, 1145 | "id": "484600", 1146 | "clientOrderId": "x-R4BD3S82ab5a3898147087df422499", 1147 | "timestamp": 1651429454828, 1148 | "datetime": "2022-05-01T18:24:14.828Z", 1149 | "lastTradeTimestamp": null, 1150 | "symbol": "XRP/BUSD", 1151 | "type": "market", 1152 | "timeInForce": "IOC", 1153 | "postOnly": false, 1154 | "side": "buy", 1155 | "price": 0.7634, 1156 | "stopPrice": null, 1157 | "amount": 19.6, 1158 | "cost": 14.96264, 1159 | "average": 0.7634, 1160 | "filled": 19.6, 1161 | "remaining": 0.0, 1162 | "status": "closed", 1163 | "fee": { 1164 | "currency": "XRP", 1165 | "cost": 0.0 1166 | }, 1167 | "trades": [ 1168 | { 1169 | "info": { 1170 | "price": "0.76340000", 1171 | "qty": "19.60000000", 1172 | "commission": "0.00000000", 1173 | "commissionAsset": "XRP", 1174 | "tradeId": "68430" 1175 | }, 1176 | "timestamp": null, 1177 | "datetime": null, 1178 | "symbol": "XRP/BUSD", 1179 | "id": "68430", 1180 | "order": "484600", 1181 | "type": "market", 1182 | "side": "buy", 1183 | "takerOrMaker": null, 1184 | "price": 0.7634, 1185 | "amount": 19.6, 1186 | "cost": 14.96264, 1187 | "fee": { 1188 | "cost": 0.0, 1189 | "currency": "XRP" 1190 | }, 1191 | "fees": [ 1192 | { 1193 | "currency": "XRP", 1194 | "cost": "0.00000000" 1195 | } 1196 | ] 1197 | } 1198 | ], 1199 | "fees": [ 1200 | { 1201 | "currency": "XRP", 1202 | "cost": 0.0 1203 | } 1204 | ] 1205 | }, 1206 | { 1207 | "info": { 1208 | "symbol": "LTCBUSD", 1209 | "orderId": "61037", 1210 | "orderListId": "-1", 1211 | "clientOrderId": "x-R4BD3S82881dc5ce1571b8a68648b", 1212 | "transactTime": "1651429458087", 1213 | "price": "0.00000000", 1214 | "origQty": "0.24850000", 1215 | "executedQty": "0.24850000", 1216 | "cummulativeQuoteQty": "24.99910000", 1217 | "status": "FILLED", 1218 | "timeInForce": "GTC", 1219 | "type": "MARKET", 1220 | "side": "BUY", 1221 | "fills": [ 1222 | { 1223 | "price": "100.60000000", 1224 | "qty": "0.24850000", 1225 | "commission": "0.00000000", 1226 | "commissionAsset": "LTC", 1227 | "tradeId": "10659" 1228 | } 1229 | ] 1230 | }, 1231 | "id": "61037", 1232 | "clientOrderId": "x-R4BD3S82881dc5ce1571b8a68648b", 1233 | "timestamp": 1651429458087, 1234 | "datetime": "2022-05-01T18:24:18.087Z", 1235 | "lastTradeTimestamp": null, 1236 | "symbol": "LTC/BUSD", 1237 | "type": "market", 1238 | "timeInForce": "IOC", 1239 | "postOnly": false, 1240 | "side": "buy", 1241 | "price": 100.6, 1242 | "stopPrice": null, 1243 | "amount": 0.2485, 1244 | "cost": 24.9991, 1245 | "average": 100.6, 1246 | "filled": 0.2485, 1247 | "remaining": 0.0, 1248 | "status": "closed", 1249 | "fee": { 1250 | "currency": "LTC", 1251 | "cost": 0.0 1252 | }, 1253 | "trades": [ 1254 | { 1255 | "info": { 1256 | "price": "100.60000000", 1257 | "qty": "0.24850000", 1258 | "commission": "0.00000000", 1259 | "commissionAsset": "LTC", 1260 | "tradeId": "10659" 1261 | }, 1262 | "timestamp": null, 1263 | "datetime": null, 1264 | "symbol": "LTC/BUSD", 1265 | "id": "10659", 1266 | "order": "61037", 1267 | "type": "market", 1268 | "side": "buy", 1269 | "takerOrMaker": null, 1270 | "price": 100.6, 1271 | "amount": 0.2485, 1272 | "cost": 24.9991, 1273 | "fee": { 1274 | "cost": 0.0, 1275 | "currency": "LTC" 1276 | }, 1277 | "fees": [ 1278 | { 1279 | "currency": "LTC", 1280 | "cost": "0.00000000" 1281 | } 1282 | ] 1283 | } 1284 | ], 1285 | "fees": [ 1286 | { 1287 | "currency": "LTC", 1288 | "cost": 0.0 1289 | } 1290 | ] 1291 | }, 1292 | { 1293 | "info": { 1294 | "symbol": "BTCUSDT", 1295 | "orderId": "11408746", 1296 | "orderListId": "-1", 1297 | "clientOrderId": "x-R4BD3S82d7de31d60e19fa3ba3cd32", 1298 | "transactTime": "1651429511659", 1299 | "price": "0.00000000", 1300 | "origQty": "0.00065300", 1301 | "executedQty": "0.00065300", 1302 | "cummulativeQuoteQty": "24.96505849", 1303 | "status": "FILLED", 1304 | "timeInForce": "GTC", 1305 | "type": "MARKET", 1306 | "side": "BUY", 1307 | "fills": [ 1308 | { 1309 | "price": "38231.33000000", 1310 | "qty": "0.00037000", 1311 | "commission": "0.00000000", 1312 | "commissionAsset": "BTC", 1313 | "tradeId": "3290256" 1314 | }, 1315 | { 1316 | "price": "38231.33000000", 1317 | "qty": "0.00028300", 1318 | "commission": "0.00000000", 1319 | "commissionAsset": "BTC", 1320 | "tradeId": "3290257" 1321 | } 1322 | ] 1323 | }, 1324 | "id": "11408746", 1325 | "clientOrderId": "x-R4BD3S82d7de31d60e19fa3ba3cd32", 1326 | "timestamp": 1651429511659, 1327 | "datetime": "2022-05-01T18:25:11.659Z", 1328 | "lastTradeTimestamp": null, 1329 | "symbol": "BTC/USDT", 1330 | "type": "market", 1331 | "timeInForce": "IOC", 1332 | "postOnly": false, 1333 | "side": "buy", 1334 | "price": 38231.33, 1335 | "stopPrice": null, 1336 | "amount": 0.000653, 1337 | "cost": 24.96505849, 1338 | "average": 38231.33, 1339 | "filled": 0.000653, 1340 | "remaining": 0.0, 1341 | "status": "closed", 1342 | "fee": { 1343 | "currency": "BTC", 1344 | "cost": 0.0 1345 | }, 1346 | "trades": [ 1347 | { 1348 | "info": { 1349 | "price": "38231.33000000", 1350 | "qty": "0.00037000", 1351 | "commission": "0.00000000", 1352 | "commissionAsset": "BTC", 1353 | "tradeId": "3290256" 1354 | }, 1355 | "timestamp": null, 1356 | "datetime": null, 1357 | "symbol": "BTC/USDT", 1358 | "id": "3290256", 1359 | "order": "11408746", 1360 | "type": "market", 1361 | "side": "buy", 1362 | "takerOrMaker": null, 1363 | "price": 38231.33, 1364 | "amount": 0.00037, 1365 | "cost": 14.1455921, 1366 | "fee": { 1367 | "cost": 0.0, 1368 | "currency": "BTC" 1369 | }, 1370 | "fees": [ 1371 | { 1372 | "currency": "BTC", 1373 | "cost": "0.00000000" 1374 | } 1375 | ] 1376 | }, 1377 | { 1378 | "info": { 1379 | "price": "38231.33000000", 1380 | "qty": "0.00028300", 1381 | "commission": "0.00000000", 1382 | "commissionAsset": "BTC", 1383 | "tradeId": "3290257" 1384 | }, 1385 | "timestamp": null, 1386 | "datetime": null, 1387 | "symbol": "BTC/USDT", 1388 | "id": "3290257", 1389 | "order": "11408746", 1390 | "type": "market", 1391 | "side": "buy", 1392 | "takerOrMaker": null, 1393 | "price": 38231.33, 1394 | "amount": 0.000283, 1395 | "cost": 10.81946639, 1396 | "fee": { 1397 | "cost": 0.0, 1398 | "currency": "BTC" 1399 | }, 1400 | "fees": [ 1401 | { 1402 | "currency": "BTC", 1403 | "cost": "0.00000000" 1404 | } 1405 | ] 1406 | } 1407 | ], 1408 | "fees": [ 1409 | { 1410 | "currency": "BTC", 1411 | "cost": 0.0 1412 | } 1413 | ] 1414 | }, 1415 | { 1416 | "info": { 1417 | "symbol": "XRPBUSD", 1418 | "orderId": "484601", 1419 | "orderListId": "-1", 1420 | "clientOrderId": "x-R4BD3S82ba7727041043eb93815ce7", 1421 | "transactTime": "1651429514919", 1422 | "price": "0.00000000", 1423 | "origQty": "19.60000000", 1424 | "executedQty": "19.60000000", 1425 | "cummulativeQuoteQty": "14.96264000", 1426 | "status": "FILLED", 1427 | "timeInForce": "GTC", 1428 | "type": "MARKET", 1429 | "side": "BUY", 1430 | "fills": [ 1431 | { 1432 | "price": "0.76340000", 1433 | "qty": "19.60000000", 1434 | "commission": "0.00000000", 1435 | "commissionAsset": "XRP", 1436 | "tradeId": "68431" 1437 | } 1438 | ] 1439 | }, 1440 | "id": "484601", 1441 | "clientOrderId": "x-R4BD3S82ba7727041043eb93815ce7", 1442 | "timestamp": 1651429514919, 1443 | "datetime": "2022-05-01T18:25:14.919Z", 1444 | "lastTradeTimestamp": null, 1445 | "symbol": "XRP/BUSD", 1446 | "type": "market", 1447 | "timeInForce": "IOC", 1448 | "postOnly": false, 1449 | "side": "buy", 1450 | "price": 0.7634, 1451 | "stopPrice": null, 1452 | "amount": 19.6, 1453 | "cost": 14.96264, 1454 | "average": 0.7634, 1455 | "filled": 19.6, 1456 | "remaining": 0.0, 1457 | "status": "closed", 1458 | "fee": { 1459 | "currency": "XRP", 1460 | "cost": 0.0 1461 | }, 1462 | "trades": [ 1463 | { 1464 | "info": { 1465 | "price": "0.76340000", 1466 | "qty": "19.60000000", 1467 | "commission": "0.00000000", 1468 | "commissionAsset": "XRP", 1469 | "tradeId": "68431" 1470 | }, 1471 | "timestamp": null, 1472 | "datetime": null, 1473 | "symbol": "XRP/BUSD", 1474 | "id": "68431", 1475 | "order": "484601", 1476 | "type": "market", 1477 | "side": "buy", 1478 | "takerOrMaker": null, 1479 | "price": 0.7634, 1480 | "amount": 19.6, 1481 | "cost": 14.96264, 1482 | "fee": { 1483 | "cost": 0.0, 1484 | "currency": "XRP" 1485 | }, 1486 | "fees": [ 1487 | { 1488 | "currency": "XRP", 1489 | "cost": "0.00000000" 1490 | } 1491 | ] 1492 | } 1493 | ], 1494 | "fees": [ 1495 | { 1496 | "currency": "XRP", 1497 | "cost": 0.0 1498 | } 1499 | ] 1500 | }, 1501 | { 1502 | "info": { 1503 | "symbol": "LTCBUSD", 1504 | "orderId": "61038", 1505 | "orderListId": "-1", 1506 | "clientOrderId": "x-R4BD3S82b7009f1a3b2a1f02680e45", 1507 | "transactTime": "1651429517945", 1508 | "price": "0.00000000", 1509 | "origQty": "0.24850000", 1510 | "executedQty": "0.24850000", 1511 | "cummulativeQuoteQty": "24.99910000", 1512 | "status": "FILLED", 1513 | "timeInForce": "GTC", 1514 | "type": "MARKET", 1515 | "side": "BUY", 1516 | "fills": [ 1517 | { 1518 | "price": "100.60000000", 1519 | "qty": "0.24850000", 1520 | "commission": "0.00000000", 1521 | "commissionAsset": "LTC", 1522 | "tradeId": "10660" 1523 | } 1524 | ] 1525 | }, 1526 | "id": "61038", 1527 | "clientOrderId": "x-R4BD3S82b7009f1a3b2a1f02680e45", 1528 | "timestamp": 1651429517945, 1529 | "datetime": "2022-05-01T18:25:17.945Z", 1530 | "lastTradeTimestamp": null, 1531 | "symbol": "LTC/BUSD", 1532 | "type": "market", 1533 | "timeInForce": "IOC", 1534 | "postOnly": false, 1535 | "side": "buy", 1536 | "price": 100.6, 1537 | "stopPrice": null, 1538 | "amount": 0.2485, 1539 | "cost": 24.9991, 1540 | "average": 100.6, 1541 | "filled": 0.2485, 1542 | "remaining": 0.0, 1543 | "status": "closed", 1544 | "fee": { 1545 | "currency": "LTC", 1546 | "cost": 0.0 1547 | }, 1548 | "trades": [ 1549 | { 1550 | "info": { 1551 | "price": "100.60000000", 1552 | "qty": "0.24850000", 1553 | "commission": "0.00000000", 1554 | "commissionAsset": "LTC", 1555 | "tradeId": "10660" 1556 | }, 1557 | "timestamp": null, 1558 | "datetime": null, 1559 | "symbol": "LTC/BUSD", 1560 | "id": "10660", 1561 | "order": "61038", 1562 | "type": "market", 1563 | "side": "buy", 1564 | "takerOrMaker": null, 1565 | "price": 100.6, 1566 | "amount": 0.2485, 1567 | "cost": 24.9991, 1568 | "fee": { 1569 | "cost": 0.0, 1570 | "currency": "LTC" 1571 | }, 1572 | "fees": [ 1573 | { 1574 | "currency": "LTC", 1575 | "cost": "0.00000000" 1576 | } 1577 | ] 1578 | } 1579 | ], 1580 | "fees": [ 1581 | { 1582 | "currency": "LTC", 1583 | "cost": 0.0 1584 | } 1585 | ] 1586 | }, 1587 | { 1588 | "info": { 1589 | "symbol": "BTCUSDT", 1590 | "orderId": "11409004", 1591 | "orderListId": "-1", 1592 | "clientOrderId": "x-R4BD3S82de0370254559791f2cb3a2", 1593 | "transactTime": "1651429571220", 1594 | "price": "0.00000000", 1595 | "origQty": "0.00065400", 1596 | "executedQty": "0.00065400", 1597 | "cummulativeQuoteQty": "24.99933966", 1598 | "status": "FILLED", 1599 | "timeInForce": "GTC", 1600 | "type": "MARKET", 1601 | "side": "BUY", 1602 | "fills": [ 1603 | { 1604 | "price": "38225.29000000", 1605 | "qty": "0.00065400", 1606 | "commission": "0.00000000", 1607 | "commissionAsset": "BTC", 1608 | "tradeId": "3290300" 1609 | } 1610 | ] 1611 | }, 1612 | "id": "11409004", 1613 | "clientOrderId": "x-R4BD3S82de0370254559791f2cb3a2", 1614 | "timestamp": 1651429571220, 1615 | "datetime": "2022-05-01T18:26:11.220Z", 1616 | "lastTradeTimestamp": null, 1617 | "symbol": "BTC/USDT", 1618 | "type": "market", 1619 | "timeInForce": "IOC", 1620 | "postOnly": false, 1621 | "side": "buy", 1622 | "price": 38225.29, 1623 | "stopPrice": null, 1624 | "amount": 0.000654, 1625 | "cost": 24.99933966, 1626 | "average": 38225.29, 1627 | "filled": 0.000654, 1628 | "remaining": 0.0, 1629 | "status": "closed", 1630 | "fee": { 1631 | "currency": "BTC", 1632 | "cost": 0.0 1633 | }, 1634 | "trades": [ 1635 | { 1636 | "info": { 1637 | "price": "38225.29000000", 1638 | "qty": "0.00065400", 1639 | "commission": "0.00000000", 1640 | "commissionAsset": "BTC", 1641 | "tradeId": "3290300" 1642 | }, 1643 | "timestamp": null, 1644 | "datetime": null, 1645 | "symbol": "BTC/USDT", 1646 | "id": "3290300", 1647 | "order": "11409004", 1648 | "type": "market", 1649 | "side": "buy", 1650 | "takerOrMaker": null, 1651 | "price": 38225.29, 1652 | "amount": 0.000654, 1653 | "cost": 24.99933966, 1654 | "fee": { 1655 | "cost": 0.0, 1656 | "currency": "BTC" 1657 | }, 1658 | "fees": [ 1659 | { 1660 | "currency": "BTC", 1661 | "cost": "0.00000000" 1662 | } 1663 | ] 1664 | } 1665 | ], 1666 | "fees": [ 1667 | { 1668 | "currency": "BTC", 1669 | "cost": 0.0 1670 | } 1671 | ] 1672 | }, 1673 | { 1674 | "info": { 1675 | "symbol": "XRPBUSD", 1676 | "orderId": "484602", 1677 | "orderListId": "-1", 1678 | "clientOrderId": "x-R4BD3S82accb20ff5cb0183415a013", 1679 | "transactTime": "1651429574231", 1680 | "price": "0.00000000", 1681 | "origQty": "19.60000000", 1682 | "executedQty": "19.60000000", 1683 | "cummulativeQuoteQty": "14.96264000", 1684 | "status": "FILLED", 1685 | "timeInForce": "GTC", 1686 | "type": "MARKET", 1687 | "side": "BUY", 1688 | "fills": [ 1689 | { 1690 | "price": "0.76340000", 1691 | "qty": "19.60000000", 1692 | "commission": "0.00000000", 1693 | "commissionAsset": "XRP", 1694 | "tradeId": "68432" 1695 | } 1696 | ] 1697 | }, 1698 | "id": "484602", 1699 | "clientOrderId": "x-R4BD3S82accb20ff5cb0183415a013", 1700 | "timestamp": 1651429574231, 1701 | "datetime": "2022-05-01T18:26:14.231Z", 1702 | "lastTradeTimestamp": null, 1703 | "symbol": "XRP/BUSD", 1704 | "type": "market", 1705 | "timeInForce": "IOC", 1706 | "postOnly": false, 1707 | "side": "buy", 1708 | "price": 0.7634, 1709 | "stopPrice": null, 1710 | "amount": 19.6, 1711 | "cost": 14.96264, 1712 | "average": 0.7634, 1713 | "filled": 19.6, 1714 | "remaining": 0.0, 1715 | "status": "closed", 1716 | "fee": { 1717 | "currency": "XRP", 1718 | "cost": 0.0 1719 | }, 1720 | "trades": [ 1721 | { 1722 | "info": { 1723 | "price": "0.76340000", 1724 | "qty": "19.60000000", 1725 | "commission": "0.00000000", 1726 | "commissionAsset": "XRP", 1727 | "tradeId": "68432" 1728 | }, 1729 | "timestamp": null, 1730 | "datetime": null, 1731 | "symbol": "XRP/BUSD", 1732 | "id": "68432", 1733 | "order": "484602", 1734 | "type": "market", 1735 | "side": "buy", 1736 | "takerOrMaker": null, 1737 | "price": 0.7634, 1738 | "amount": 19.6, 1739 | "cost": 14.96264, 1740 | "fee": { 1741 | "cost": 0.0, 1742 | "currency": "XRP" 1743 | }, 1744 | "fees": [ 1745 | { 1746 | "currency": "XRP", 1747 | "cost": "0.00000000" 1748 | } 1749 | ] 1750 | } 1751 | ], 1752 | "fees": [ 1753 | { 1754 | "currency": "XRP", 1755 | "cost": 0.0 1756 | } 1757 | ] 1758 | }, 1759 | { 1760 | "info": { 1761 | "symbol": "LTCBUSD", 1762 | "orderId": "61039", 1763 | "orderListId": "-1", 1764 | "clientOrderId": "x-R4BD3S822202238ec09c1b7c81efde", 1765 | "transactTime": "1651429577262", 1766 | "price": "0.00000000", 1767 | "origQty": "0.24850000", 1768 | "executedQty": "0.24850000", 1769 | "cummulativeQuoteQty": "24.99910000", 1770 | "status": "FILLED", 1771 | "timeInForce": "GTC", 1772 | "type": "MARKET", 1773 | "side": "BUY", 1774 | "fills": [ 1775 | { 1776 | "price": "100.60000000", 1777 | "qty": "0.24850000", 1778 | "commission": "0.00000000", 1779 | "commissionAsset": "LTC", 1780 | "tradeId": "10661" 1781 | } 1782 | ] 1783 | }, 1784 | "id": "61039", 1785 | "clientOrderId": "x-R4BD3S822202238ec09c1b7c81efde", 1786 | "timestamp": 1651429577262, 1787 | "datetime": "2022-05-01T18:26:17.262Z", 1788 | "lastTradeTimestamp": null, 1789 | "symbol": "LTC/BUSD", 1790 | "type": "market", 1791 | "timeInForce": "IOC", 1792 | "postOnly": false, 1793 | "side": "buy", 1794 | "price": 100.6, 1795 | "stopPrice": null, 1796 | "amount": 0.2485, 1797 | "cost": 24.9991, 1798 | "average": 100.6, 1799 | "filled": 0.2485, 1800 | "remaining": 0.0, 1801 | "status": "closed", 1802 | "fee": { 1803 | "currency": "LTC", 1804 | "cost": 0.0 1805 | }, 1806 | "trades": [ 1807 | { 1808 | "info": { 1809 | "price": "100.60000000", 1810 | "qty": "0.24850000", 1811 | "commission": "0.00000000", 1812 | "commissionAsset": "LTC", 1813 | "tradeId": "10661" 1814 | }, 1815 | "timestamp": null, 1816 | "datetime": null, 1817 | "symbol": "LTC/BUSD", 1818 | "id": "10661", 1819 | "order": "61039", 1820 | "type": "market", 1821 | "side": "buy", 1822 | "takerOrMaker": null, 1823 | "price": 100.6, 1824 | "amount": 0.2485, 1825 | "cost": 24.9991, 1826 | "fee": { 1827 | "cost": 0.0, 1828 | "currency": "LTC" 1829 | }, 1830 | "fees": [ 1831 | { 1832 | "currency": "LTC", 1833 | "cost": "0.00000000" 1834 | } 1835 | ] 1836 | } 1837 | ], 1838 | "fees": [ 1839 | { 1840 | "currency": "LTC", 1841 | "cost": 0.0 1842 | } 1843 | ] 1844 | } 1845 | ] --------------------------------------------------------------------------------