From 6b33d7597f4e9c96a9385702be946468f2c411f8 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Thu, 21 Nov 2024 11:25:57 +0100 Subject: [PATCH 1/2] [Coinbase] support portfolio id --- .../Exchange/coinbase/coinbase_exchange.py | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/Trading/Exchange/coinbase/coinbase_exchange.py b/Trading/Exchange/coinbase/coinbase_exchange.py index 0a0f379e9..4b24b6fb3 100644 --- a/Trading/Exchange/coinbase/coinbase_exchange.py +++ b/Trading/Exchange/coinbase/coinbase_exchange.py @@ -137,8 +137,37 @@ async def get_account_id(self, **kwargs: dict) -> str: try: # warning might become deprecated # https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-users - user_data = await self.connector.client.v2PrivateGetUser() - return user_data["data"]["id"] + portfolio_id = None + try: + # use portfolio id when possible to enable "coinbase subaccounts" which are called "portfolios" + # note: portfolio id == user id (from v2PrivateGetUser) when using master account + accounts = await self.connector.client.fetch_accounts() + portfolio_ids = set(account[ccxt_constants.CCXT_INFO]['retail_portfolio_id'] for account in accounts) + if len(portfolio_ids) != 1: + is_up_to_date_key = self._is_up_to_date_api_key() + if is_up_to_date_key: + self.logger.error( + f"Unexpected: failed to identify Coinbase portfolio id on up to date API keys: " + f"{portfolio_ids=}" + ) + self.logger.info( + f"{len(portfolio_ids)} portfolio found on Coinbase account. " + f"This can happen with non up-to-date API keys ({is_up_to_date_key=}). " + f"Falling back to v2PrivateGetUser() to get the current account id." + ) + else: + portfolio_id = next(iter(portfolio_ids)) + except (IndexError, KeyError): + pass + if portfolio_id is None: + # fallback to user id + user_data = await self.connector.client.v2PrivateGetUser() + portfolio_id = user_data["data"]["id"] + self.logger.warning( + f"No Coinbase portfolio id can be selected from fetch_accounts(), used v2PrivateGetUser() " + f"to identify user id and use it as portfolio id: {portfolio_id}" + ) + return portfolio_id except ccxt.BaseError as err: self.logger.exception( err, True, @@ -149,6 +178,13 @@ async def get_account_id(self, **kwargs: dict) -> str: ) return trading_constants.DEFAULT_ACCOUNT_ID + def _is_up_to_date_api_key(self) -> bool: + return ( + self.connector.client.apiKey.find('organizations/') >= 0 or + self.connector.client.apiKey.startswith('-----BEGIN') + ) + + @_coinbase_retrier async def get_symbol_prices(self, symbol: str, time_frame: commons_enums.TimeFrames, limit: int = None, **kwargs: dict) -> typing.Optional[list]: From 3d5b1620d3fe526f7dc0d579ef053c0ae7185d29 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Thu, 21 Nov 2024 20:40:28 +0100 Subject: [PATCH 2/2] [Coinbase] remove v2PrivateGetUser call --- .../Exchange/coinbase/coinbase_exchange.py | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/Trading/Exchange/coinbase/coinbase_exchange.py b/Trading/Exchange/coinbase/coinbase_exchange.py index 4b24b6fb3..ea17848b4 100644 --- a/Trading/Exchange/coinbase/coinbase_exchange.py +++ b/Trading/Exchange/coinbase/coinbase_exchange.py @@ -138,35 +138,32 @@ async def get_account_id(self, **kwargs: dict) -> str: # warning might become deprecated # https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-users portfolio_id = None - try: - # use portfolio id when possible to enable "coinbase subaccounts" which are called "portfolios" - # note: portfolio id == user id (from v2PrivateGetUser) when using master account - accounts = await self.connector.client.fetch_accounts() - portfolio_ids = set(account[ccxt_constants.CCXT_INFO]['retail_portfolio_id'] for account in accounts) - if len(portfolio_ids) != 1: - is_up_to_date_key = self._is_up_to_date_api_key() - if is_up_to_date_key: - self.logger.error( - f"Unexpected: failed to identify Coinbase portfolio id on up to date API keys: " - f"{portfolio_ids=}" - ) - self.logger.info( - f"{len(portfolio_ids)} portfolio found on Coinbase account. " - f"This can happen with non up-to-date API keys ({is_up_to_date_key=}). " - f"Falling back to v2PrivateGetUser() to get the current account id." + accounts = await self.connector.client.fetch_accounts() + # use portfolio id when possible to enable "coinbase subaccounts" which are called "portfolios" + # note: oldest portfolio portfolio id == user id (from previous v2PrivateGetUser) when using master account + portfolio_ids = set(account[ccxt_constants.CCXT_INFO]['retail_portfolio_id'] for account in accounts) + if len(portfolio_ids) != 1: + is_up_to_date_key = self._is_up_to_date_api_key() + if is_up_to_date_key: + self.logger.error( + f"Unexpected: failed to identify Coinbase portfolio id on up to date API keys: " + f"{portfolio_ids=}" ) - else: - portfolio_id = next(iter(portfolio_ids)) - except (IndexError, KeyError): - pass - if portfolio_id is None: - # fallback to user id - user_data = await self.connector.client.v2PrivateGetUser() - portfolio_id = user_data["data"]["id"] - self.logger.warning( - f"No Coinbase portfolio id can be selected from fetch_accounts(), used v2PrivateGetUser() " - f"to identify user id and use it as portfolio id: {portfolio_id}" + sorted_portfolios = sorted( + [ + account[ccxt_constants.CCXT_INFO] + for account in accounts + ], + key=lambda account: account["created_at"], ) + portfolio_id = sorted_portfolios[0]['retail_portfolio_id'] + self.logger.info( + f"{len(portfolio_ids)} portfolio found on Coinbase account. " + f"This can happen with non up-to-date API keys ({is_up_to_date_key=}). " + f"Using the oldest portfolio id to bind to main account: {portfolio_id=}." + ) + else: + portfolio_id = next(iter(portfolio_ids)) return portfolio_id except ccxt.BaseError as err: self.logger.exception(