-
Notifications
You must be signed in to change notification settings - Fork 1
/
coinbase_dca.py
255 lines (218 loc) · 8.83 KB
/
coinbase_dca.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# /bin/env python3
#
# pip install pyexch
#
from datetime import datetime, timezone
# from json import dumps
from time import sleep
from pyexch.exchange import Exchange # , data_toDict
HOUR = 60 * 60 # one-hr in seconds
DEPOSIT = True
CANCEL_OPEN = True
TAKER = 0.01 # do some taker action
SPREAD = 0.05 # do buys 1% above to 5% below (peanut butter spread)
THRESHOLD = 0.95 # Percent of holds to clear before starting
MKRFEE = 0.0060 # fee for maker limit orders
TKRFEE = 0.0080 # fee for taker limit orders
DCAUSD = 10.00 # USD to deposit on our DCAs
DEPSOIT_DELAY = 12 * HOUR # If we've deposited in the last 12hrs, skip
MAXCNT = 500 # 500 The maximum number of orders allowed
MAXCAN = 100 # 100 The maximum number of orders to cancel
MIXFEE = TAKER / SPREAD * TKRFEE + (1 - TAKER / SPREAD) * MKRFEE
PMTTYP = "ACH"
PRODID = "BTC-USD"
WALTID = "USD Wallet"
ISOFMT = "%Y-%m-%dT%H:%M:%S%z" # Time parse formatter
def main():
current = datetime.now().astimezone(timezone.utc)
dcausd = DCAUSD + current.day / 100 # set pennies to day of month
cbv3 = Exchange.create("keystore.json", "coinbase.v3_api")
cbv3.keystore.close() # Trezor devices should only have one handle at a time
cboa = Exchange.create("keystore.json", "coinbase.oauth2")
account_id = cboa.keystore.get("coinbase.state.usd_wallet")
pmt_method_id = cboa.keystore.get("coinbase.state.ach_payment")
# Get my account_id of WALTID (USD Wallet)
#
if not account_id:
resp = cbv3.v3_client.get_accounts()
for account in resp["accounts"]:
if (
account["available_balance"]["currency"] == "USD"
and account["name"] == WALTID
):
account_id = account["uuid"]
assert account_id
cboa.keystore.set("coinbase.state.usd_wallet", account_id)
cboa.keystore.save()
# print(f"DBG: account['name:{WALTID}']:", account_id)
# Determine 90% funding for waitclock
#
balance = get_balance(cbv3, account_id)
hold = float(cbv3._response["account"]["hold"]["value"])
target = THRESHOLD * (hold + dcausd) + balance
if DEPOSIT:
# Check to see if we've deposited today
#
need_deposit = True
# bugbug: none of the pagination arguments are working on the list depsoits V2 URI
resp = cboa.oa2_client.get_deposits(account_id)
for deposit in resp["data"]:
# if completed or created, process
if "canceled" == deposit["status"] or not deposit["committed"]:
continue # else skip
created = datetime.strptime(deposit["created_at"], ISOFMT).astimezone(
timezone.utc
)
# print(f"DBG: deposit['age_sec:{(current - created).total_seconds()}']:", deposit['id'])
if (current - created).total_seconds() < DEPSOIT_DELAY:
print(
f"Recent Deposit of {float(deposit['amount']['amount']):.2f} found @ {deposit['created_at']}"
)
need_deposit = False
break
# if needed, make the deposit
#
if need_deposit:
# Find the PMTTYP (ACH) payment type
#
if not pmt_method_id:
resp = cbv3.v3_client.list_payment_methods()
for pmt_method in resp["payment_methods"]:
if pmt_method["type"] == PMTTYP:
pmt_method_id = pmt_method["id"]
break
assert account_id and pmt_method_id
cboa.keystore.set("coinbase.state.ach_payment", pmt_method_id)
cboa.keystore.save()
# print(f"DBG: payment_method['type:{PMTTYP}']:", pmt_method_id)
# Make the deposit
#
resp = cboa.oa2_client.deposit(
account_id,
amount=f"{dcausd:.2f}",
currency="USD",
payment_method=pmt_method_id,
)
print(
f"Created deposit of {float(resp['amount']['amount']):.2f} @ {resp['created_at']}"
)
# print(f"DBG: deposit['amt:{dcausd}']:", dumps(data_toDict(resp)))
if CANCEL_OPEN:
# Get outstanding orders to cancel
#
resp = cbv3.v3_client.list_orders(order_status=["OPEN"])
params = []
for order in resp["orders"]:
if (
order["status"] == "OPEN"
and order["product_id"] == PRODID
and order["side"] == "BUY"
):
params += [order["order_id"]]
# Cancel the outstanding orders
#
if params:
sublist = [params[i : i + MAXCAN] for i in range(0, len(params), MAXCAN)]
for params in sublist:
resp = cbv3.v3_client.cancel_orders(order_ids=params)
# Get today's min_size and price for PRODID (BTC-USD)
#
product = cbv3.v3_client.get_product(product_id=PRODID)
if product["product_id"] == PRODID:
min_size = float(product["base_min_size"])
spot = float(product["price"])
assert account_id and min_size and spot
# Get my available balance of WALTID (USD Wallet)
#
adjust = 0.0 if DEPOSIT and need_deposit else THRESHOLD * dcausd
target -= adjust
while abs(balance - target) > 1 and balance < target:
sleep(1)
print(f"Waiting: balance={balance:.2f}, target={target:.2f}")
balance = get_balance(cbv3, account_id)
# "Peanut Butter Spread" the buys as small as possible from spot down to SPREAD (5%) below.
#
# price_hi = 66_857.20
price_hi = spot * (1 + TAKER)
price_lo = price_hi * (1 - SPREAD)
price_av = (price_hi + price_lo) / 2
count = min(int(balance / (min_size * price_av * (1 + MKRFEE))), MAXCNT)
step = (price_hi - price_lo) / (count - 1) # remember bookend math
price = price_hi
# print(spot, count, balance)
for i in range(count, 0, -1):
xprice = price
uuid = cbv3.new_uuid()
(size, cost) = mk_size(price, i, step, balance, spot, min_size)
params = dict(
client_order_id=str(uuid),
product_id=PRODID,
base_size=f"{size:.8f}",
limit_price=f"{price:.2f}",
post_only=True,
)
resp = mk_order(cbv3, params, min_size)
if resp["order"]["status"] == "FILLED":
cost = float(resp["order"]["total_value_after_fees"])
xprice = float(resp["order"]["average_filled_price"])
# balance -= cost
cbal = balance - cost
balance = get_balance(cbv3, account_id)
print(
f"{count} Limit buy of {params['base_size']} btc at {xprice:.2f}, at a cost of {cost:.2f}, leaving balance of {balance:.2f} ({cbal:.2f})"
)
price -= step
assert price and step and cost and balance
return cboa
def get_balance(cbv3, account_id):
resp = cbv3._response = cbv3.v3_client.get_account(account_id)
if (
resp["account"]["available_balance"]["currency"] == "USD"
and resp["account"]["name"] == WALTID
):
balance = float(resp["account"]["available_balance"]["value"])
assert account_id and balance
return balance
def mk_order(cbv3, params, min_size):
retry = 3
for i in range(retry):
resp = cbv3.v3_client.limit_order_gtc_buy(**params)
if resp["success"]:
break
if resp["error_response"]["error"] == "INVALID_LIMIT_PRICE_POST_ONLY":
# params.update(dict(post_only=False, base_size=f"{min_size:.8f}"))
params.update(dict(post_only=False))
for j in range(retry):
resp = cbv3.v3_client.limit_order_gtc_buy(**params)
if resp["success"]:
break
# breakpoint()
if resp["success"]:
for i in range(retry):
try:
resp = cbv3.v3_client.get_order(resp["order_id"])
except Exception:
print("retrying")
sleep(1)
continue
if resp["order"]:
break
return resp
def mk_size(price, count, step, balance, spot, min_size):
price_hi = price
price_lo = price - step * (count - 1)
price_av = (price_hi + price_lo) / 2
if price > spot:
size = min_size
cost = size * spot * (1 + TKRFEE)
else:
size = (balance - count / 100) / count / (price_av * (1 + MKRFEE))
cost = size * price * (1 + MKRFEE)
return (size, cost)
if __name__ == "__main__":
# main()
try:
cboa = main()
except Exception as e:
ex = e
breakpoint()