├── README.md ├── authenticated_client.py └── coinbase_algo.py /README.md: -------------------------------------------------------------------------------- 1 | # Coinbase Execution Algorithm 2 | 3 | ##### Please use with caution! 4 | *Note: this algorithm is still being worked on and may encounter some unexpected bugs* 5 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 7 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 8 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 9 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | ## Description 13 | A VWAP algorithm that attempts to beat the VWAP price (i.e tries to get a price for you that is better than what the market is executing at) 14 | The following user parameters must be defined: 15 | - ```product_id``` - the ticker you want to trade (i.e BTC-USD) 16 | - ```side``` - either 'buy' or 'sell' 17 | - ```hours``` - how many hours you want the execute the order over 18 | - ```orderqty``` - size (i.e 1 BTC) 19 | - ```limit``` - a hard limit on all orders 20 | When running the code, the following inputs are entered via the user from the command prompt 21 | 22 | ## Getting Started 23 | It's pretty simple, you'll need to get an authenticated client setup first on your Coinbase Pro account so you're "hooked-in" appropriately. 24 | - The ```authenticated_client.py``` script is where you'll need to put your relevant keys and is a nice & easy way to switch between virtual trading and live trading. If you need help on how to do this, please refer to the following repo (https://github.com/danpaquin/coinbasepro-python) 25 | - Now run ```python coinbase_algo.py``` 26 | 27 | 28 | ## Credits 29 | Credit to the following repo detailing an easy way to interface with the [Coinbase API via Python] (https://github.com/danpaquin/coinbasepro-python) 30 | 31 | Credit to this awesome dude Andrés Berejnoi, check out his [YouTube Channel] (https://www.youtube.com/channel/UCvPE2QkDtQnl652gAXNOrgg) 32 | -------------------------------------------------------------------------------- /authenticated_client.py: -------------------------------------------------------------------------------- 1 | import cbpro 2 | 3 | # Are you using a live trading environment or not?! Be careful! 4 | LIVE = False 5 | 6 | # This is for the sandbox 7 | passphrase_sandbox = "INSERT_KEY" 8 | b62secret_sandbox = "INSERT_KEY" 9 | key_sandbox = "INSERT_KEY" 10 | 11 | # These credentials are used for LIVE trading 12 | passphrase = 'INSERT_KEY' 13 | b64secret = 'INSERT_KEY' 14 | key = 'INSERT_KEY' 15 | 16 | # Create an authenticated client using the above API keys 17 | if LIVE: 18 | print('Using live trading environment') 19 | auth_client = cbpro.AuthenticatedClient(key, b64secret, passphrase) 20 | else: 21 | print('Using sandbox environment') 22 | auth_client = cbpro.AuthenticatedClient(key_sandbox, b62secret_sandbox, passphrase_sandbox, api_url="https://api-public.sandbox.pro.coinbase.com") 23 | -------------------------------------------------------------------------------- /coinbase_algo.py: -------------------------------------------------------------------------------- 1 | import cbpro 2 | import numpy as np 3 | import datetime 4 | import dateutil.parser 5 | from authenticated_client import auth_client, LIVE 6 | 7 | # Product to trade 8 | product_id = 'BTC-USD' 9 | side = input('Enter side [buy/sell] ') 10 | hours = input('Enter hours to execute over [i.e 2] ') 11 | orderqty = input('Enter quantity in BTC [i.e 1] ') 12 | limit = input('Enter limit [i.e 65000 OR 0 if no limit] ') 13 | 14 | 15 | # Init global values to store L1 orderbook information 16 | current_last = 0 17 | current_bid_price = 0 18 | current_ask_price = 0 19 | current_spread = 0 20 | current_volume = 0 21 | 22 | 23 | # Initiate empty arrays to track volume & price 24 | volume_array = [] 25 | price_array = [] 26 | 27 | 28 | # Define the websocket to connect to 29 | class TextWebsocketClient(cbpro.WebsocketClient): 30 | def on_open(self): 31 | if LIVE: 32 | self.url = 'wss://ws-feed.pro.coinbase.com' 33 | else: 34 | self.url = 'wss://ws-feed.pro.coinbase.com' # 'wss://ws-feed-public.sandbox.pro.coinbase.com' 35 | 36 | self.message_count = 0 37 | self.initial_date = datetime.datetime.now() 38 | self.vwap_algo = VWAP_Execution_Algorithm( 39 | side=side, 40 | hours=hours, 41 | orderqty=orderqty, 42 | limit=limit, 43 | ) 44 | 45 | def on_message(self,msg): 46 | 47 | self.message_count += 1 48 | msg_type = msg.get('type',None) 49 | if msg_type == 'ticker': 50 | time_val = msg.get('time',('-'*27)) 51 | price_val = msg.get('price',None) 52 | bid_val = msg.get('best_bid', None) 53 | ask_val = msg.get('best_ask', None) 54 | volume = msg.get('last_size', None) 55 | 56 | if price_val is not None: 57 | price_val = float(price_val) 58 | if bid_val is not None: 59 | bid_val = float(bid_val) 60 | if ask_val is not None: 61 | ask_val = float(ask_val) 62 | 63 | spread_val = ask_val - bid_val 64 | product_id = msg.get('product_id',None) 65 | 66 | print('Product %s | Time_val %s | Price %s | Bid %s | Ask %s | Volume %s' % (product_id, time_val, price_val, bid_val, ask_val, volume)) 67 | 68 | # Gathering global variables 69 | volume_array.append(volume) 70 | price_array.append(price_val) 71 | 72 | # ---------------------------------------------------------------- 73 | # ---------------- VWAP EXECUTION ALGO --------------------------- 74 | # ---------------------------------------------------------------- 75 | 76 | # Updating info in the VWAP class 77 | self.vwap_algo.update_info(open=None, 78 | bid=bid_val, 79 | ask=ask_val, 80 | last=price_val, 81 | spread=spread_val) 82 | # Run the executor 83 | self.vwap_algo.Execute() 84 | 85 | 86 | def on_close(self): 87 | print(f"<---Websocket connection closed--->\n\tTotal messages: {self.message_count}") 88 | 89 | 90 | # VWAP Execution Algorithm 91 | class VWAP_Execution_Algorithm(): 92 | 93 | def __init__(self, side, hours, orderqty, limit): 94 | # Initiate some useful variables that we'll need to monitor 95 | self.open_positions = 0 96 | self.best_bid = 0 97 | self.best_ask = 0 98 | self.last_trade = 0 99 | self.spread = 0 100 | self.account_balance = 0 101 | self.urgency = 0 102 | self.vwap = 0 103 | 104 | # Initiate user parameters 105 | self.side = side 106 | self.hours = float(hours) 107 | self.size = float(orderqty) #Bitcoin size 108 | self.limit = float(limit) 109 | self.min_size = self.size*0.01 110 | 111 | # Tracking our fills 112 | self.fills = [] 113 | self.QuantityExecuted = 0 114 | self.average_executed_price = 0 115 | self.average_fees = 0 116 | 117 | # Tracking how much size we have exposed 118 | self.exposed_size = 0 119 | 120 | # Tracking our orders 121 | self.orders = [] 122 | 123 | # Get current time 124 | self.start_time = datetime.datetime.now() 125 | self.start_timestamp = datetime.datetime.timestamp(self.start_time) 126 | print(' ----------------> Schedule starting at %s' % (self.start_time)) 127 | 128 | # Get end time 129 | self.end_timestamp = self.start_timestamp + (self.hours * 3600) 130 | self.end_time = datetime.datetime.fromtimestamp(self.end_timestamp) 131 | print(' ----------------> Schedule ending at %s' % (self.end_time)) 132 | 133 | def update_info(self, open, bid, ask, last, spread): 134 | self.open_positions = open 135 | self.best_bid = bid 136 | self.best_ask = ask 137 | self.last_trade = last 138 | self.spread = spread 139 | self.account_balance = 0 140 | self.calculate_vwap(volume_array, price_array) 141 | 142 | # Print some useful logging info 143 | print('\n') 144 | print('**************************************************************') 145 | print('************************* LOGGING ****************************') 146 | print('**************************************************************') 147 | print('\n') 148 | print('------------------------- FILLS INFO -------------------------') 149 | print('Average Executed Price = %s' % (self.average_executed_price)) 150 | print('No. Of Fills So Far = %s' % (len(self.fills))) 151 | print('--------------------------------------------------------------') 152 | print('\n') 153 | 154 | def calculate_vwap(self, volume, price): 155 | # Here we are calculating the current Intraday VWAP price based on incoming volume & price data Σ PiVi / Σ Vi 156 | # @param volume = volume array 157 | # @param price = price array 158 | # @return vwap price 159 | 160 | volume = np.asarray(volume).astype(float) 161 | price = np.asarray(price).astype(float) 162 | assert(len(volume) == len(price)) 163 | 164 | if len(volume) > 1: 165 | current_vwap = np.sum(price * volume)/np.sum(volume) 166 | self.vwap = current_vwap 167 | 168 | return current_vwap 169 | else: 170 | return 171 | 172 | def time_complete(self): 173 | current_time = datetime.datetime.now() 174 | current_timestamp = datetime.datetime.timestamp(current_time) 175 | time_elapsed = (current_timestamp - self.start_timestamp)/(self.end_timestamp - self.start_timestamp) * 100 176 | 177 | return time_elapsed 178 | 179 | def Execute(self): 180 | 'Execution model that attempts to beat the vwap price' 181 | 182 | pct_to_complete = self.time_complete() 183 | 184 | # total_filled_size, remaining_quantity, order_pct_complete = self.GetRemainingQuantity() 185 | 186 | should_have_executed = np.round(pct_to_complete/100*self.size,decimals=3) 187 | executed_so_far = self.QuantityExecuted 188 | execution_slice = np.round(should_have_executed - executed_so_far - self.exposed_size,decimals=7) # Need to round this up or you'll get into a sizing issue 189 | 190 | # Print some useful execution logs info 191 | print('------------------------- EXECUTION INFO -------------------------') 192 | print('Order progress --------------------------------> %d/100' %(pct_to_complete)) 193 | print('Order Qty = %s' % (self.size)) 194 | print('Should have executed so far = %s' % (should_have_executed)) 195 | print('Executed so far = %s' % (self.QuantityExecuted)) 196 | print('Exposed size in the market = %s' % (self.exposed_size)) 197 | print('\n') 198 | print('Order slice queuing up = %s' % (execution_slice)) 199 | print('Min execution size set to = %s' % (self.min_size)) 200 | print('-------------------------------------------------------------------') 201 | print('\n') 202 | print('------------------------- ORDER BOOK INFO -------------------------') 203 | print('VWAP Price = %s' % (self.vwap)) 204 | print('L1 Order Book -----> Bid %s | Ask %s' % (self.best_bid, self.best_ask)) 205 | print('-------------------------------------------------------------------') 206 | print('\n') 207 | 208 | 209 | # Check we are not going over 210 | if (pct_to_complete <= 100) & (executed_so_far < self.size): 211 | 212 | # Check that the order size is above our min size 213 | if execution_slice < self.min_size: 214 | print('[MIN_SIZE_BLOCK] Minimum size blocker kicking in | Currently set to %s' % (self.min_size)) 215 | elif execution_slice > self.min_size: 216 | # Check if the price is favorable 217 | if self.PriceIsFavorable() and self.limit == 0: 218 | order = auth_client.place_market_order(product_id=product_id, 219 | side='buy', 220 | size=execution_slice) 221 | print(order) 222 | self.orders.append(order) 223 | # Update the fills post execution 224 | self.UpdateFills() 225 | 226 | elif self.PriceIsFavorable() and (self.limit != 0): 227 | order = auth_client.place_limit_order(product_id=product_id, 228 | side='buy', 229 | size=execution_slice, 230 | price=self.limit) 231 | print(order) 232 | self.orders.append(order) 233 | # Update the fills post execution 234 | self.UpdateFills() 235 | 236 | elif (pct_to_complete > 100) & (executed_so_far < self.size): 237 | remaining_quantity = np.round(self.size - executed_so_far,decimals=4) 238 | print('------------------------- ORDER SUMMARY -------------------------') 239 | print('Executed = %s' % (self.QuantityExecuted)) 240 | print('Average Executed Price = %s' % (self.average_executed_price)) 241 | print('Remaining quantity = %s ------> You can finish this off manually on Coinbase Pro' % (remaining_quantity)) 242 | print('Now cancelling all open orders.....') 243 | print('----------------------------------------------------------------') 244 | print('\n') 245 | auth_client.cancel_all(product_id=product_id) 246 | raise ValueError('********************* \n [Order Complete] Current time has now passed the scheduled end time \n *********************') 247 | 248 | 249 | def PriceIsFavorable(self, threshold=3): 250 | """ 251 | Checks if the price is more favourable than VWAP 252 | :param self 253 | :param threshold: a value in basis points of how passive to be vs VWAP 254 | :return: bool 255 | """ 256 | 257 | if (self.side == 'buy') & (self.vwap != 0): 258 | if self.best_ask < self.vwap*(1-threshold/10000): 259 | print('--------------------->>> [BUY ORDER] Current ask is more favorable than VWAP') 260 | return True 261 | else: 262 | return False 263 | elif (self.side == 'sell') & (self.vwap != 0): 264 | if self.best_bid > self.vwap*((1+threshold/10000)): 265 | ('--------------------->>> [SELL ORDER] Current bid is more favorable than VWAP') 266 | return True 267 | else: 268 | return False 269 | else: 270 | return False 271 | 272 | 273 | def UpdateFills(self): 274 | 'This function will be used to track all our fills so far' 275 | 276 | # Example of fill dictionary from Coinbase 277 | # {'created_at': '2021-05-21T06:32:21.912Z', 'trade_id': 29877009, 'product_id': 'BTC-GBP', 278 | # 'order_id': '1b7f96a9-95da-4511-b6d2-a4b18829742e', 'user_id': '6097b5f91b6ace17ba390d66', 279 | # 'profile_id': '4a687ef0-818f-4475-826c-c2e9585a106c', 'liquidity': 'T', 'price': '39844.21000000', 280 | # 'size': '1.00000000', 'fee': '199.2210500000000000', 'side': 'buy', 'settled': True, 'usd_volume': '39844.2100000000000000'} 281 | 282 | # Retrieve all the fills so far 283 | self.fills = [] 284 | fills = auth_client.get_fills(product_id=product_id) 285 | for fill in fills: 286 | fill_time = fill['created_at'] 287 | fill_time = dateutil.parser.isoparse(fill_time) 288 | fill_timestamp = datetime.datetime.timestamp(fill_time) 289 | if fill_timestamp > self.start_timestamp: 290 | self.fills.append(fill) 291 | 292 | # Update any exposed positions 293 | self.CheckOpenOrders() 294 | 295 | # Update the average executed price 296 | self.AverageExecutedPrice() 297 | 298 | 299 | def CheckOpenOrders(self): 300 | 301 | open_orders_array = [] 302 | 303 | open_orders = auth_client.get_orders() 304 | for o in open_orders: 305 | if o['filled_size'] == '0': 306 | open_orders_array.append(o['size']) 307 | 308 | self.exposed_size = np.sum(np.asarray(open_orders_array).astype(float)) 309 | 310 | 311 | def AverageExecutedPrice(self): 312 | filled_prices = [] 313 | filled_size = [] 314 | 315 | for fill in self.fills: 316 | filled_prices.append(fill['price']) 317 | filled_size.append(fill['size']) 318 | 319 | # Force arrays into floats 320 | filled_prices = np.asarray(filled_prices).astype(float) 321 | filled_size = np.asarray(filled_size).astype(float) 322 | self.QuantityExecuted = np.sum(filled_size) 323 | 324 | # Calculate AVG executed price so far 325 | self.average_executed_price = np.sum(filled_prices*filled_size)/np.sum(filled_size) 326 | 327 | 328 | def GetRemainingQuantity(self): 329 | 'Gets the remaining quantity left from the order' 330 | 331 | filled_size = [] 332 | 333 | for fill in self.fills: 334 | filled_size.append(fill.size) 335 | 336 | total_filled_size = np.sum(np.asarray(filled_size).astype(float)) 337 | remaining_quantity = self.size - total_filled_size 338 | pct_complete = total_filled_size/self.size * 100 339 | 340 | return total_filled_size, remaining_quantity, pct_complete 341 | 342 | 343 | if __name__ == '__main__': 344 | 345 | # ------------------------ MAIN ------------------------ # 346 | auth_client.cancel_all(product_id=product_id) # Make sure there is no existing orders in the market 347 | stream = TextWebsocketClient(products=[product_id],channels=['ticker']) 348 | stream.start() 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | --------------------------------------------------------------------------------