diff --git a/examples/coinbase_dca.py b/examples/coinbase_dca.py index 01e2714..0797e2a 100644 --- a/examples/coinbase_dca.py +++ b/examples/coinbase_dca.py @@ -11,13 +11,14 @@ HOUR = 60 * 60 # one-hr in seconds -TAKER = 0 - 0.000001 # do buys 1% above market (taker action) +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 -DEPOSIT = True -CANCEL_OPEN = True DEPSOIT_DELAY = 12 * HOUR # If we've deposited in the last 12hrs, skip MAXCNT = 500 # 500 The maximum number of orders allowed @@ -31,6 +32,7 @@ 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 @@ -39,26 +41,6 @@ def main(): account_id = cboa.keystore.get("coinbase.state.usd_wallet") pmt_method_id = cboa.keystore.get("coinbase.state.ach_payment") - 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 my account_id of WALTID (USD Wallet) # if not account_id: @@ -75,6 +57,12 @@ def main(): 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 # @@ -115,7 +103,6 @@ def main(): cboa.keystore.save() # print(f"DBG: payment_method['type:{PMTTYP}']:", pmt_method_id) - dcausd = DCAUSD + current.day / 100 # set pennies to day of month # Make the deposit # resp = cboa.oa2_client.deposit( @@ -129,18 +116,25 @@ def main(): ) # print(f"DBG: deposit['amt:{dcausd}']:", dumps(data_toDict(resp))) - sleep(3) # There really is a settle time from cancel to avail balance... insane. - - # Get my available balance of WALTID (USD Wallet) - # - resp = 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"]) + 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"]] - assert account_id and balance + # 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) # @@ -149,11 +143,21 @@ def main(): min_size = float(product["base_min_size"]) spot = float(product["price"]) - assert account_id and balance and min_size and spot + 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 @@ -176,9 +180,11 @@ def main(): if resp["order"]["status"] == "FILLED": cost = float(resp["order"]["total_value_after_fees"]) xprice = float(resp["order"]["average_filled_price"]) - balance -= cost + # 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}" + 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 @@ -186,6 +192,18 @@ def main(): 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): @@ -193,7 +211,8 @@ def mk_order(cbv3, params, min_size): 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, 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"]: @@ -228,8 +247,9 @@ def mk_size(price, count, step, balance, spot, min_size): if __name__ == "__main__": - # try: - cboa = main() - # except Exception as e: - # ex = e - # print(ex) + # main() + try: + cboa = main() + except Exception as e: + ex = e + breakpoint() diff --git a/examples/coinbase_resluts.py b/examples/coinbase_resluts.py index 9914091..5613174 100644 --- a/examples/coinbase_resluts.py +++ b/examples/coinbase_resluts.py @@ -40,6 +40,7 @@ def main(): start=str(earlier), end=str(now), ) + spot = float(candles["candles"][0]["close"]) * (1 + TAKER) date_candle = dict() for candle in candles["candles"]: @@ -53,9 +54,14 @@ def main(): for order in fills["fills"]: if order["side"] != "BUY" or order["trade_type"] != "FILL": break - dt = datetime.strptime(order["trade_time"], "%Y-%m-%dT%H:%M:%S.%fZ").replace( - tzinfo=timezone.utc - ) + try: + dt = datetime.strptime( + order["trade_time"], "%Y-%m-%dT%H:%M:%S.%fZ" + ).replace(tzinfo=timezone.utc) + except ValueError: + dt = datetime.strptime(order["trade_time"], "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) key = dt.strftime("%Y-%m-%d") # date_fill[DATE][FILLS] = list() # date_fill[DATE][CANDEL] = candle @@ -68,7 +74,7 @@ def main(): assert candle, "Every date should match" date_fill[key]["candle"] = candle - avg_usd = ord_usd = ord_btc = 0 + buc_btc = avg_usd = ord_usd = ord_btc = 0 for key, value in date_fill.items(): usd = btc = 0 for order in value["fills"]: @@ -92,13 +98,28 @@ def main(): ord_usd += usd ord_btc += btc avg_usd += avg_price * btc + buc_btc += 1 / avg_price + count = len(date_fill.keys()) saved_usd = avg_usd - ord_usd saved_pct = saved_usd / avg_usd * 100 + saved_dca = (ord_btc - (buc_btc * ord_usd / count)) / ord_btc * 100 + + print( + f"Total: Bought {ord_btc:.8f} BTC for {ord_usd/ord_btc:.2f} instead of {avg_usd/ord_btc:.2f}" + ) + print( + f" Saving {saved_pct:.4f}% (${saved_usd:.2f}) vs buy-on-avg, and {saved_dca:.4f}% vs dca-market-buy" + ) print( - f"Total: Bought {ord_btc:.8f} BTC for {ord_usd/ord_btc:.2f} instead of {avg_usd/ord_btc:.2f}, saving ${saved_usd:.2f} ({saved_pct:.2f}%)" + f" Saving {100-(ord_usd/ord_btc)/spot*100:.2f}% vs one-time market buy at current price of: {spot:.2f}" ) if __name__ == "__main__": - main() + # main() + try: + main() + except Exception as e: + ex = e + breakpoint() diff --git a/pyexch/exchange.py b/pyexch/exchange.py index 4e8197a..ff28fc6 100644 --- a/pyexch/exchange.py +++ b/pyexch/exchange.py @@ -135,6 +135,20 @@ def my_ipv6(self): def new_uuid(self): return uuid4() + def dbg(self): + token = self.keystore.get("coinbase.oauth2.token") + return f"DBG: {token[:4]}...{token[-4:]}" + + def tick(self): + import time + + expiration = self.keystore.get("coinbase.oauth2.expiration") + left = expiration - time.time() + hrs = left // (60 * 60) + min = (left - hrs * 60 * 60) // 60 + sec = left - hrs * 60 * 60 - min * 60 + return f"DBG: countdown {int(hrs):02}:{int(min):02}:{sec:0>02.2f}" + class Coinbase(Exchange): def __init__(self, keystore): @@ -303,6 +317,8 @@ def oa2_auth(self): ) # rule https://forums.coinbasecloud.dev/t/walletsend-is-limited-1-00-day-per-user/866/2 # broke https://forums.coinbasecloud.dev/t/oauth-application-maximum-of-1-00-per-month/7096/13 + # Confirmed this meta tags allow to set the send limit. Next is to try a send with CB-2FA-TOKEN + # header: https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/sign-in-with-coinbase-2fa if "wallet:transactions:send" in self.keystore.get("coinbase.oauth2.scope"): self._params.update( { @@ -387,6 +403,8 @@ def oa2_revoke(self): uri = self.keystore.get("coinbase.oauth2.revoke_url") self._params = dict( token=self.keystore.get("coinbase.oauth2.token"), + client_id=self.keystore.get("coinbase.oauth2.id"), + client_secret=self.keystore.get("coinbase.oauth2.secret"), ) self._response = requests.post(uri, data=self._params) diff --git a/pyexch/pyexch.py b/pyexch/pyexch.py index 2cfdcb8..25b9cad 100644 --- a/pyexch/pyexch.py +++ b/pyexch/pyexch.py @@ -84,7 +84,7 @@ def main(): with open(args.params, "r") as pj: params = load(pj) - internals = ["my_ipv4", "my_ipv6", "new_uuid"] + internals = ["my_ipv4", "my_ipv6", "new_uuid", "dbg", "tick"] ex = Exchange.create(args.keystore, args.auth) @@ -125,8 +125,14 @@ def main(): if resp: print(dumps(resp, indent=2)) elif ex._response is not None: - print("[ERROR] Last Response:", ex._response, file=stderr) + print("Last Response:", ex._response, file=stderr) if __name__ == "__main__": - main() + # main() + # Debug check + try: + main() + except Exception as e: + ex = e + breakpoint() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..dbfd9d6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,29 @@ +# migrate to pyproject.toml +# black config in .black, run with `black --config .black .` +# +[black] +line-length = 88 +target-version = ['py38'] + +# Run with `flake8` +# +[flake8] +max-line-length = 88 +filename = + ./examples/*.py, + ./pyexch/*.py, +ignore = + # E501: line too long + E501, + # W503 line break before binary operator + W503, + # E203 whitespace before ':' + E203, + +# Run with `isort .` +# +[isort] +py_version = 38 +profile = black +src_paths = pyexch, examples + diff --git a/todo.md b/todo.md index 94fbe2e..6c9c1a0 100644 --- a/todo.md +++ b/todo.md @@ -15,6 +15,7 @@ - [x] Paginate fills on get_fills in DCA results - [ ] BugRpt that max_size has dropped to 0.00000001 - [ ] BugRpt that retries are req to pull committed orders (see try in DCA) +- [ ] Test Send Transaction with [CB-2FA-TOKEN][m] console prompting - [ ] Convert from setup.py to pyproject.toml - [ ] Add [AES encryption][h], or port samples to [CryptoDomeX][i] - [ ] Cleaner update of UID across all my GPG keys @@ -67,7 +68,7 @@ [j]: #publish-to-github [k]: https://docs.readthedocs.io/en/stable/tutorial/index.html (RTD Tutorial) [l]: #publish-to-pypi -[m]: +[m]: https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/sign-in-with-coinbase-2fa [n]: [o]: [p]: