Skip to content

Commit

Permalink
Merge pull request #42 from rhettre/feature/coinbase-accounts
Browse files Browse the repository at this point in the history
Account services
  • Loading branch information
rhettre authored Jul 24, 2024
2 parents 41a64aa + 4b10c6c commit f2fe01a
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 13 deletions.
38 changes: 26 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ api_secret = "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\
client = EnhancedRESTClient(api_key=api_key, api_secret=api_secret)
```

## Usage of Strategies
## Using the Official SDK

The `EnhancedRESTClient` inherits from the Coinbase SDK's `RESTClient`, which means you can use all the functions provided by the official SDK. Here's an example of how to use the `get_product` function:

Expand All @@ -48,13 +48,9 @@ print(product_info)

## Using Wrapper Strategies

Here's an example of how to use the strategies package to buy $10 worth of Bitcoin. By making assumptions about limit price in [trading_config.py](coinbase_advanced_trader/trading_config.py) we are able to simplify the syntax for making limit orders:
Here's an example of how to use the strategies package to buy $10 worth of Bitcoin. By making assumptions about limit price in [trading_config.py](coinbase_advanced_trader/trading_config.py) we are able to simplify the syntax for making orders:

```python
from coinbase_advanced_trader.enhanced_rest_client import EnhancedRESTClient

client = EnhancedRESTClient(api_key=api_key, api_secret=api_secret)

# Perform a market buy
client.fiat_market_buy("BTC-USDC", "10")

Expand All @@ -77,19 +73,37 @@ client.fiat_limit_sell("BTC-USDC", "5")
client.fiat_limit_sell("BTC-USDC", "5", price_multiplier="1.1")
```

## Usage of Fear and Greed Index
### Account Balance Operations

The `EnhancedRESTClient` provides methods to retrieve account balances for cryptocurrencies. These methods are particularly useful for managing and monitoring your cryptocurrency holdings on Coinbase.

#### Listing All Non-Zero Crypto Balances

To get a dictionary of all cryptocurrencies with non-zero balances in your account:

```python
from coinbase_advanced_trader.enhanced_rest_client import EnhancedRESTClient
balances = client.list_held_crypto_balances()
print(balances)
```

client = EnhancedRESTClient(api_key=api_key, api_secret=api_secret)
#### Getting a Specific Crypto Balance

To get the available balance of a specific cryptocurrency in your account (returns 0 if the specified cryptocurrency is not found in the account):

```python
balance = client.get_crypto_balance("BTC")
print(balance)
```

Note: Both methods use a caching mechanism to reduce API calls. The account data is cached for one hour before a fresh fetch is made from Coinbase.

### Usage of Fear and Greed Index

```python
# Trade based on Fear and Greed Index
client.trade_based_on_fgi("BTC-USDC", "10")
```

## Advanced Usage

You can also update and retrieve the Fear and Greed Index schedule:

```python
Expand Down Expand Up @@ -141,4 +155,4 @@ GitHub: https://github.com/rhettre/coinbase-advancedtrade-python

## Disclaimer

This project is not affiliated with, maintained, or endorsed by Coinbase. Use this software at your own risk. Trading cryptocurrencies carries a risk of financial loss. The developers of this software are not responsible for any financial losses or damages incurred while using this software. Nothing in this software should be seen as an inducement to trade with a particular strategy or as financial advice.
This project is not affiliated with, maintained, or endorsed by Coinbase. Use this software at your own risk. Trading cryptocurrencies carries a risk of financial loss. The developers of this software are not responsible for any financial losses or damages incurred while using this software. Nothing in this software should be seen as an inducement to trade with a particular strategy or as financial advice.
22 changes: 22 additions & 0 deletions coinbase_advanced_trader/enhanced_rest_client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Enhanced REST client for Coinbase Advanced Trading API."""

from typing import Optional, List, Dict, Any
from decimal import Decimal
from coinbase.rest import RESTClient
from .services.order_service import OrderService
from .services.fear_and_greed_strategy import FearAndGreedStrategy
from .services.price_service import PriceService
from .trading_config import FearAndGreedConfig
from coinbase_advanced_trader.constants import DEFAULT_CONFIG
from coinbase_advanced_trader.logger import logger
from coinbase_advanced_trader.services.account_service import AccountService


class EnhancedRESTClient(RESTClient):
Expand All @@ -23,13 +25,33 @@ def __init__(self, api_key: str, api_secret: str, **kwargs: Any) -> None:
**kwargs: Additional keyword arguments for RESTClient.
"""
super().__init__(api_key=api_key, api_secret=api_secret, **kwargs)
self._account_service = AccountService(self)
self._price_service = PriceService(self)
self._order_service = OrderService(self, self._price_service)
self._config = FearAndGreedConfig()
self._fear_and_greed_strategy = FearAndGreedStrategy(
self._order_service, self._price_service, self._config
)

def get_crypto_balance(self, currency: str) -> Decimal:
"""
Get the available balance of a specific cryptocurrency.
Args:
currency: The currency code (e.g., 'BTC', 'ETH', 'USDC').
Returns:
The available balance of the specified cryptocurrency.
"""
return self._account_service.get_crypto_balance(currency)

def list_held_crypto_balances(self) -> Dict[str, Decimal]:
"""
Get a dictionary of all held cryptocurrencies and their balances.
Returns:
Dict[str, Decimal]: A dictionary where the key is the currency code
and the value is the balance as a Decimal.
"""
return self._account_service.list_held_crypto_balances()

def update_fgi_schedule(self, new_schedule: List[Dict[str, Any]]) -> bool:
"""
Update the Fear and Greed Index trading schedule.
Expand Down
65 changes: 65 additions & 0 deletions coinbase_advanced_trader/services/account_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from decimal import Decimal
from typing import Dict, List, Any, Optional
from datetime import datetime, timedelta

from coinbase.rest import RESTClient

from coinbase_advanced_trader.logger import logger

class AccountService:
"""Service for handling account-related operations."""

def __init__(self, rest_client: RESTClient):
self.rest_client = rest_client
self._accounts_cache = None
self._cache_timestamp = None
self._cache_duration = timedelta(hours=1)

def _get_accounts(self, limit: int = 250) -> Dict[str, Dict[str, Any]]:
if self._accounts_cache is None or \
(datetime.now() - self._cache_timestamp) > self._cache_duration:
logger.info("Fetching fresh account data from Coinbase")
response = self.rest_client.get_accounts(limit=limit)
self._accounts_cache = {
account['currency']: {
'uuid': account['uuid'],
'available_balance': Decimal(account['available_balance']['value'])
}
for account in response['accounts']
}
logger.debug(f"Processed accounts cache: {self._accounts_cache}")
self._cache_timestamp = datetime.now()
return self._accounts_cache

def get_crypto_balance(self, currency: str) -> Decimal:
try:
accounts = self._get_accounts()
if currency not in accounts:
logger.warning(f"No account found for {currency}")
return Decimal('0')
balance = accounts[currency]['available_balance']
logger.info(f"Retrieved balance for {currency}: {balance}")
return balance
except Exception as e:
logger.error(f"Error retrieving balance for {currency}: {str(e)}")
raise

def list_held_crypto_balances(self) -> Dict[str, Decimal]:
"""
Get a dictionary of all held cryptocurrencies and their balances.
Returns:
Dict[str, Decimal]: A dictionary where the key is the currency code
and the value is the balance as a Decimal.
"""
accounts = self._get_accounts()
non_zero_balances = {
currency: account['available_balance']
for currency, account in accounts.items()
if account['available_balance'] > 0
}

logger.info(f"Found {len(non_zero_balances)} non-zero balances:")
for currency, balance in non_zero_balances.items():
logger.info(f"{currency}: {balance}")

return non_zero_balances
116 changes: 116 additions & 0 deletions coinbase_advanced_trader/tests/test_account_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Unit tests for the AccountService class."""

import unittest
from unittest.mock import Mock, patch
from decimal import Decimal
from datetime import datetime

from coinbase.rest import RESTClient
from coinbase_advanced_trader.services.account_service import AccountService


class TestAccountService(unittest.TestCase):
"""Test cases for the AccountService class."""

def setUp(self):
"""Set up the test environment."""
self.rest_client_mock = Mock(spec=RESTClient)
self.account_service = AccountService(self.rest_client_mock)

def test_get_accounts_cache(self):
"""Test the caching behavior of _get_accounts method."""
mock_response = {
'accounts': [
{
'uuid': 'abc123',
'currency': 'BTC',
'available_balance': {'value': '1.5', 'currency': 'BTC'}
},
{
'uuid': 'def456',
'currency': 'ETH',
'available_balance': {'value': '10.0', 'currency': 'ETH'}
}
]
}
self.rest_client_mock.get_accounts.return_value = mock_response

# First call should fetch from API
accounts = self.account_service._get_accounts()
self.rest_client_mock.get_accounts.assert_called_once()
self.assertEqual(len(accounts), 2)
self.assertEqual(accounts['BTC']['available_balance'], Decimal('1.5'))

# Second call should use cache
self.rest_client_mock.get_accounts.reset_mock()
accounts = self.account_service._get_accounts()
self.rest_client_mock.get_accounts.assert_not_called()

def test_get_crypto_balance(self):
"""Test the get_crypto_balance method."""
mock_accounts = {
'BTC': {'uuid': 'abc123', 'available_balance': Decimal('1.5')},
'ETH': {'uuid': 'def456', 'available_balance': Decimal('10.0')}
}
self.account_service._get_accounts = Mock(return_value=mock_accounts)

btc_balance = self.account_service.get_crypto_balance('BTC')
self.assertEqual(btc_balance, Decimal('1.5'))

eth_balance = self.account_service.get_crypto_balance('ETH')
self.assertEqual(eth_balance, Decimal('10.0'))

xrp_balance = self.account_service.get_crypto_balance('XRP')
self.assertEqual(xrp_balance, Decimal('0'))

def test_list_held_cryptocurrencies(self):
"""Test the list_held_cryptocurrencies method."""
mock_accounts = {
'BTC': {'uuid': 'abc123', 'available_balance': Decimal('1.5')},
'ETH': {'uuid': 'def456', 'available_balance': Decimal('0')},
'XRP': {'uuid': 'ghi789', 'available_balance': Decimal('100.0')}
}
self.account_service._get_accounts = Mock(return_value=mock_accounts)

held_currencies = self.account_service.list_held_cryptocurrencies()
self.assertEqual(set(held_currencies), {'BTC', 'XRP'})

@patch('coinbase_advanced_trader.services.account_service.datetime')
def test_cache_expiration(self, mock_datetime):
"""Test the cache expiration behavior."""
mock_response = {
'accounts': [
{
'uuid': 'abc123',
'currency': 'BTC',
'available_balance': {'value': '1.5', 'currency': 'BTC'}
}
]
}
self.rest_client_mock.get_accounts.return_value = mock_response

# Set initial time
mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0)

# First call should fetch from API
self.account_service._get_accounts()
self.rest_client_mock.get_accounts.assert_called_once()

# Set time to 30 minutes later (within cache duration)
mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 30, 0)

# Second call should use cache
self.rest_client_mock.get_accounts.reset_mock()
self.account_service._get_accounts()
self.rest_client_mock.get_accounts.assert_not_called()

# Set time to 61 minutes later (outside cache duration)
mock_datetime.now.return_value = datetime(2023, 1, 1, 13, 1, 0)

# Third call should fetch from API again
self.account_service._get_accounts()
self.rest_client_mock.get_accounts.assert_called_once()


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

setup(
name='coinbase-advancedtrade-python',
version='0.2.1',
version='0.2.2',
description='The unofficial Python client for the Coinbase Advanced Trade API',
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down

0 comments on commit f2fe01a

Please sign in to comment.