10
10
import sys
11
11
import time
12
12
13
- import gdax
13
+ import cbpro
14
14
15
15
from decimal import Decimal
16
16
@@ -21,9 +21,14 @@ def get_timestamp():
21
21
22
22
23
23
"""
24
- Basic Coinbase Pro DCA buy/sell bot that pulls the current market price, subtracts a
25
- small spread to generate a valid price (see note below), then submits the trade as
26
- a limit order.
24
+ Basic Coinbase Pro DCA buy/sell bot that executes a market order.
25
+ * CB Pro does not incentivize maker vs taker trading unless you trade over $50k in
26
+ a 30 day period (0.25% taker, 0.15% maker). Current fees are 0.50% if you make
27
+ less than $10k worth of trades over the last 30 days. Drops to 0.35% if you're
28
+ above $10k but below $50k in trades.
29
+ * Market orders can be issued for as little as $5 of value versus limit orders which
30
+ must be 0.001 BTC (e.g. $50 min if btc is at $50k). BTC-denominated market
31
+ orders must be at least 0.0001 BTC.
27
32
28
33
This is meant to be run as a crontab to make regular buys/sells on a set schedule.
29
34
"""
@@ -63,7 +68,7 @@ def get_timestamp():
63
68
help = "Run against sandbox, skips user confirmation prompt" )
64
69
65
70
parser .add_argument ('-warn_after' ,
66
- default = 3600 ,
71
+ default = 300 ,
67
72
action = "store" ,
68
73
type = int ,
69
74
dest = "warn_after" ,
@@ -84,7 +89,7 @@ def get_timestamp():
84
89
85
90
if __name__ == "__main__" :
86
91
args = parser .parse_args ()
87
- print ("%s : STARTED: %s" % ( get_timestamp (), args ) )
92
+ print (f" { get_timestamp () } : STARTED: { args } " )
88
93
89
94
market_name = args .market_name
90
95
order_side = args .order_side .lower ()
@@ -121,16 +126,16 @@ def get_timestamp():
121
126
122
127
# Instantiate public and auth API clients
123
128
if not args .sandbox_mode :
124
- auth_client = gdax .AuthenticatedClient (key , secret , passphrase )
129
+ auth_client = cbpro .AuthenticatedClient (key , secret , passphrase )
125
130
else :
126
131
# Use the sandbox API (requires a different set of API access credentials)
127
- auth_client = gdax .AuthenticatedClient (
132
+ auth_client = cbpro .AuthenticatedClient (
128
133
key ,
129
134
secret ,
130
135
passphrase ,
131
136
api_url = "https://api-public.sandbox.pro.coinbase.com" )
132
137
133
- public_client = gdax .PublicClient ()
138
+ public_client = cbpro .PublicClient ()
134
139
135
140
# Retrieve dict list of all trading pairs
136
141
products = public_client .get_products ()
@@ -149,12 +154,11 @@ def get_timestamp():
149
154
elif amount_currency == product .get ("base_currency" ):
150
155
amount_currency_is_quote_currency = False
151
156
else :
152
- raise Exception ("amount_currency %s not in market %s" % (amount_currency ,
153
- market_name ))
154
- print (product )
157
+ raise Exception (f"amount_currency { amount_currency } not in market { market_name } " )
158
+ print (json .dumps (product , indent = 2 ))
155
159
156
- print ("base_min_size: %s" % base_min_size )
157
- print ("quote_increment: %s" % quote_increment )
160
+ print (f "base_min_size: { base_min_size } " )
161
+ print (f "quote_increment: { quote_increment } " )
158
162
159
163
# Prep boto SNS client for email notifications
160
164
sns = boto3 .client (
@@ -164,156 +168,53 @@ def get_timestamp():
164
168
region_name = "us-east-1" # N. Virginia
165
169
)
166
170
171
+ if amount_currency_is_quote_currency :
172
+ result = auth_client .place_market_order (
173
+ product_id = market_name ,
174
+ side = order_side ,
175
+ funds = float (amount .quantize (quote_increment ))
176
+ )
177
+ else :
178
+ result = auth_client .place_market_order (
179
+ product_id = market_name ,
180
+ side = order_side ,
181
+ size = float (amount .quantize (base_increment ))
182
+ )
167
183
168
- """
169
- Buy orders will be rejected if they are at or above the lowest sell order
170
- (think: too far right on the order book). When the price is plummeting
171
- this is likely to happen. In this case pause for Y amount of time and
172
- then grab the latest price and re-place the order. Attempt X times before
173
- giving up.
174
-
175
- *Longer pauses are probably advantageous--if the price is crashing, you
176
- don't want to be rushing in.
177
-
178
- see: https://stackoverflow.com/a/47447663
179
- """
180
- max_attempts = 100
181
- attempt_wait = 60
182
- cur_attempt = 1
183
- result = None
184
- while cur_attempt <= max_attempts :
185
- # Get the current price...
186
- ticker = public_client .get_product_ticker (product_id = market_name )
187
- if 'price' not in ticker :
188
- # Cannot proceed with order. Coinbase Pro is likely under maintenance.
189
- if 'message' in ticker :
190
- sns .publish (
191
- TopicArn = sns_topic ,
192
- Subject = "%s order aborted" % (market_name ),
193
- Message = ticker .get ('message' )
194
- )
195
- print (ticker .get ('message' ))
196
- print ("%s order aborted" % (market_name ))
197
- exit ()
198
-
199
- current_price = Decimal (ticker ['price' ])
200
- bid = Decimal (ticker ['bid' ])
201
- ask = Decimal (ticker ['ask' ])
202
- if order_side == "buy" :
203
- rounding = decimal .ROUND_DOWN
204
- else :
205
- rounding = decimal .ROUND_UP
206
- midmarket_price = ((ask + bid ) / Decimal ('2.0' )).quantize (quote_increment ,
207
- rounding )
208
- print ("bid: %s %s" % (bid , quote_currency ))
209
- print ("ask: %s %s" % (ask , quote_currency ))
210
- print ("midmarket_price: %s %s" % (midmarket_price , quote_currency ))
211
-
212
- offer_price = midmarket_price
213
- print ("offer_price: %s %s" % (offer_price , quote_currency ))
214
-
215
- # Quantize by the base_increment limitation (in some cases this is as large as 1)
216
- if amount_currency_is_quote_currency :
217
- # Convert 'amount' of the quote_currency to equivalent in base_currency
218
- base_currency_amount = (amount / current_price ).quantize (base_increment )
219
- else :
220
- # Already in base_currency
221
- base_currency_amount = amount .quantize (base_increment )
222
-
223
- print ("base_currency_amount: %s %s" % (base_currency_amount , base_currency ))
224
-
225
- if order_side == "buy" :
226
- result = auth_client .buy (type = 'limit' ,
227
- post_only = True , # Ensure that it's treated as a limit order
228
- price = float (offer_price ), # price in quote_currency
229
- size = float (base_currency_amount ), # quantity of base_currency to buy
230
- product_id = market_name )
231
-
232
- elif order_side == "sell" :
233
- result = auth_client .sell (type = 'limit' ,
234
- post_only = True , # Ensure that it's treated as a limit order
235
- price = float (offer_price ), # price in quote_currency
236
- size = float (base_currency_amount ), # quantity of base_currency to sell
237
- product_id = market_name )
238
-
239
- print (json .dumps (result , sort_keys = True , indent = 4 ))
240
-
241
- if "message" in result and "Post only mode" in result .get ("message" ):
242
- # Price moved away from valid order
243
- print ("Post only mode at %f %s" % (offer_price , quote_currency ))
244
-
245
- elif "message" in result :
246
- # Something went wrong if there's a 'message' field in response
247
- sns .publish (
248
- TopicArn = sns_topic ,
249
- Subject = "Could not place %s %s order for %s %s" % (market_name ,
250
- order_side ,
251
- amount ,
252
- amount_currency ),
253
- Message = json .dumps (result , sort_keys = True , indent = 4 )
254
- )
255
- exit ()
256
-
257
- if result and "status" in result and result ["status" ] != "rejected" :
258
- break
259
-
260
- if result and "status" in result and result ["status" ] == "rejected" :
261
- # Rejected - usually because price was above lowest sell offer. Try
262
- # again in the next loop.
263
- print ("%s: %s Order rejected @ %f %s" % (get_timestamp (),
264
- market_name ,
265
- current_price ,
266
- quote_currency ))
267
-
268
- time .sleep (attempt_wait )
269
- cur_attempt += 1
270
-
184
+ print (json .dumps (result , sort_keys = True , indent = 4 ))
271
185
272
- if cur_attempt > max_attempts :
273
- # Was never able to place an order
186
+ if "message" in result :
187
+ # Something went wrong if there's a 'message' field in response
274
188
sns .publish (
275
189
TopicArn = sns_topic ,
276
- Subject = "Could not place %s %s order for %f %s after %d attempts" % (
277
- market_name , order_side , amount , amount_currency , max_attempts
278
- ),
190
+ Subject = f"Could not place { market_name } { order_side } order" ,
279
191
Message = json .dumps (result , sort_keys = True , indent = 4 )
280
192
)
281
193
exit ()
282
194
195
+ if result and "status" in result and result ["status" ] == "rejected" :
196
+ print (f"{ get_timestamp ()} : { market_name } Order rejected" )
283
197
284
198
order = result
285
199
order_id = order ["id" ]
286
- print ("order_id: " + order_id )
287
-
200
+ print (f"order_id: { order_id } " )
288
201
289
202
'''
290
- Wait to see if the limit order was fulfilled.
203
+ Wait to see if the order was fulfilled.
291
204
'''
292
- wait_time = 60
205
+ wait_time = 5
293
206
total_wait_time = 0
294
207
while "status" in order and \
295
208
(order ["status" ] == "pending" or order ["status" ] == "open" ):
296
209
if total_wait_time > warn_after :
297
210
sns .publish (
298
211
TopicArn = sns_topic ,
299
- Subject = "%s %s order of %s %s OPEN/UNFILLED @ %s %s" % (
300
- market_name ,
301
- order_side ,
302
- amount ,
303
- amount_currency ,
304
- offer_price ,
305
- quote_currency
306
- ),
212
+ Subject = f"{ market_name } { order_side } order of { amount } { amount_currency } OPEN/UNFILLED" ,
307
213
Message = json .dumps (order , sort_keys = True , indent = 4 )
308
214
)
309
215
exit ()
310
216
311
- print ("%s: Order %s still %s. Sleeping for %d (total %d)" % (
312
- get_timestamp (),
313
- order_id ,
314
- order ["status" ],
315
- wait_time ,
316
- total_wait_time ))
217
+ print (f"{ get_timestamp ()} : Order { order_id } still { order ['status' ]} . Sleeping for { wait_time } (total { total_wait_time } )" )
317
218
time .sleep (wait_time )
318
219
total_wait_time += wait_time
319
220
order = auth_client .get_order (order_id )
@@ -323,40 +224,21 @@ def get_timestamp():
323
224
# Most likely the order was manually cancelled in the UI
324
225
sns .publish (
325
226
TopicArn = sns_topic ,
326
- Subject = "%s %s order of %s %s CANCELED @ %s %s" % (
327
- market_name ,
328
- order_side ,
329
- amount ,
330
- amount_currency ,
331
- offer_price ,
332
- quote_currency
333
- ),
227
+ Subject = f"{ market_name } { order_side } order of { amount } { amount_currency } CANCELLED" ,
334
228
Message = json .dumps (result , sort_keys = True , indent = 4 )
335
229
)
336
230
exit ()
337
231
338
-
339
232
# Order status is no longer pending!
233
+ print (json .dumps (order , indent = 2 ))
234
+
235
+ market_price = (Decimal (order ["executed_value" ])/ Decimal (order ["filled_size" ])).quantize (quote_increment )
236
+
237
+ subject = f"{ market_name } { order_side } order of { amount } { amount_currency } { order ['status' ]} @ { market_price } { quote_currency } "
238
+ print (subject )
340
239
sns .publish (
341
240
TopicArn = sns_topic ,
342
- Subject = "%s %s order of %s %s %s @ %s %s" % (
343
- market_name ,
344
- order_side ,
345
- amount ,
346
- amount_currency ,
347
- order ["status" ],
348
- offer_price ,
349
- quote_currency
350
- ),
241
+ Subject = subject ,
351
242
Message = json .dumps (order , sort_keys = True , indent = 4 )
352
243
)
353
244
354
- print ("%s: DONE: %s %s order of %s %s %s @ %s %s" % (
355
- get_timestamp (),
356
- market_name ,
357
- order_side ,
358
- amount ,
359
- amount_currency ,
360
- order ["status" ],
361
- offer_price ,
362
- quote_currency ))
0 commit comments