Skip to content

Commit 219d719

Browse files
committed
Changed to place market orders, switch to cbpro library
1 parent 0b74ae3 commit 219d719

File tree

2 files changed

+67
-169
lines changed

2 files changed

+67
-169
lines changed

gdax_bot.py

Lines changed: 49 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import sys
1111
import time
1212

13-
import gdax
13+
import cbpro
1414

1515
from decimal import Decimal
1616

@@ -21,9 +21,14 @@ def get_timestamp():
2121

2222

2323
"""
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.
2732
2833
This is meant to be run as a crontab to make regular buys/sells on a set schedule.
2934
"""
@@ -63,7 +68,7 @@ def get_timestamp():
6368
help="Run against sandbox, skips user confirmation prompt")
6469

6570
parser.add_argument('-warn_after',
66-
default=3600,
71+
default=300,
6772
action="store",
6873
type=int,
6974
dest="warn_after",
@@ -84,7 +89,7 @@ def get_timestamp():
8489

8590
if __name__ == "__main__":
8691
args = parser.parse_args()
87-
print("%s: STARTED: %s" % (get_timestamp(), args))
92+
print(f"{get_timestamp()}: STARTED: {args}")
8893

8994
market_name = args.market_name
9095
order_side = args.order_side.lower()
@@ -121,16 +126,16 @@ def get_timestamp():
121126

122127
# Instantiate public and auth API clients
123128
if not args.sandbox_mode:
124-
auth_client = gdax.AuthenticatedClient(key, secret, passphrase)
129+
auth_client = cbpro.AuthenticatedClient(key, secret, passphrase)
125130
else:
126131
# Use the sandbox API (requires a different set of API access credentials)
127-
auth_client = gdax.AuthenticatedClient(
132+
auth_client = cbpro.AuthenticatedClient(
128133
key,
129134
secret,
130135
passphrase,
131136
api_url="https://api-public.sandbox.pro.coinbase.com")
132137

133-
public_client = gdax.PublicClient()
138+
public_client = cbpro.PublicClient()
134139

135140
# Retrieve dict list of all trading pairs
136141
products = public_client.get_products()
@@ -149,12 +154,11 @@ def get_timestamp():
149154
elif amount_currency == product.get("base_currency"):
150155
amount_currency_is_quote_currency = False
151156
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))
155159

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}")
158162

159163
# Prep boto SNS client for email notifications
160164
sns = boto3.client(
@@ -164,156 +168,53 @@ def get_timestamp():
164168
region_name="us-east-1" # N. Virginia
165169
)
166170

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+
)
167183

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))
271185

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
274188
sns.publish(
275189
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",
279191
Message=json.dumps(result, sort_keys=True, indent=4)
280192
)
281193
exit()
282194

195+
if result and "status" in result and result["status"] == "rejected":
196+
print(f"{get_timestamp()}: {market_name} Order rejected")
283197

284198
order = result
285199
order_id = order["id"]
286-
print("order_id: " + order_id)
287-
200+
print(f"order_id: {order_id}")
288201

289202
'''
290-
Wait to see if the limit order was fulfilled.
203+
Wait to see if the order was fulfilled.
291204
'''
292-
wait_time = 60
205+
wait_time = 5
293206
total_wait_time = 0
294207
while "status" in order and \
295208
(order["status"] == "pending" or order["status"] == "open"):
296209
if total_wait_time > warn_after:
297210
sns.publish(
298211
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",
307213
Message=json.dumps(order, sort_keys=True, indent=4)
308214
)
309215
exit()
310216

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})")
317218
time.sleep(wait_time)
318219
total_wait_time += wait_time
319220
order = auth_client.get_order(order_id)
@@ -323,40 +224,21 @@ def get_timestamp():
323224
# Most likely the order was manually cancelled in the UI
324225
sns.publish(
325226
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",
334228
Message=json.dumps(result, sort_keys=True, indent=4)
335229
)
336230
exit()
337231

338-
339232
# 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)
340239
sns.publish(
341240
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,
351242
Message=json.dumps(order, sort_keys=True, indent=4)
352243
)
353244

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))

requirements.txt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
1+
appnope==0.1.2
2+
backcall==0.2.0
13
backports.ssl-match-hostname==3.5.0.1
24
bintrees==2.0.7
35
boto3==1.14.38
46
botocore==1.17.38
7+
cbpro==1.1.4
58
certifi==2018.10.15
69
chardet==3.0.4
10+
decorator==4.4.2
711
docutils==0.14
812
futures==3.1.1
9-
gdax==1.0.6
1013
idna==2.7
14+
ipython==7.20.0
15+
ipython-genutils==0.2.0
16+
jedi==0.18.0
1117
jmespath==0.9.3
18+
parso==0.8.1
19+
pexpect==4.8.0
20+
pickleshare==0.7.5
21+
prompt-toolkit==3.0.16
22+
ptyprocess==0.7.0
23+
Pygments==2.7.4
24+
pymongo==3.5.1
1225
python-dateutil==2.6.1
13-
requests==2.24.0
26+
requests==2.25.1
1427
s3transfer==0.3.3
1528
six==1.10.0
29+
sortedcontainers==2.3.0
30+
traitlets==5.0.5
1631
urllib3==1.25.10
32+
wcwidth==0.2.5
1733
websocket-client==0.40.0

0 commit comments

Comments
 (0)