Skip to content

Commit e1c27c0

Browse files
authored
Merge pull request #271 from macanudo527/pair_converters/implement_coinbase_advanced
Implement Coinbase Advanced Pair Converter Plugin
2 parents 6b21827 + 321890c commit e1c27c0

10 files changed

+139
-79
lines changed

README.dev.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ All pair converter plugins are subclasses of [AbstractPairConverterPlugin](src/d
279279
* implement the `cache_key()` method;
280280
* implement the `get_historic_bar_from_native_source()` method.
281281

282-
For an example of pair converter look at the [Historic-Crypto](src/dali/plugin/pair_converter/historic_crypto.py) plugin.
282+
For an example of pair converter look at the [Coinbase-advanced](src/dali/plugin/pair_converter/coinbase_advanced.py) plugin.
283283

284284
### Country Plugin Development
285285
Country plugins are reused from RP2 and their DaLI counterpart has trivial implementation.

docs/configuration_file.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -623,17 +623,20 @@ Be aware that:
623623
* All locked plugins make use of the default forex exchange API, Frankfurter, which provides daily rates from the European Central Bank. Rates for bank holidays and weekends are taken from the previous trading day, so if a rate is requested for Saturday, the Friday rate will be used.
624624

625625

626-
### Historic Crypto
627-
This plugin is based on the Historic_Crypto Python library.
626+
### Coinbase Advanced
627+
This plugin is based on the coinbase_advanced Python library.
628628

629629
Initialize this plugin section as follows:
630630
<pre>
631631
[dali.plugin.pair_converter.historic_crypto</em>]
632632
historical_price_type = <em>&lt;historical_price_type&gt;</em>
633+
api_key = <em>&lt;api_key&gt;</em>
634+
api_secret = <em>&lt;api_secret&gt;</em>
633635
</pre>
634636

635637
Where:
636638
* `<historical_price_type>` is one of `open`, `high`, `low`, `close`, `nearest`. When DaLI downloads historical market data, it captures a `bar` of data surrounding the timestamp of the transaction. Each bar has a starting timestamp, an ending timestamp, and OHLC prices. You can choose which price to select for price lookups. The open, high, low, and close prices are self-explanatory. The `nearest` price is either the open price or the close price of the bar depending on whether the transaction time is nearer the bar starting time or the bar ending time.
639+
* `<api_key>` and `<api_secret>` can be obtained from your Coinbase account. They are not required, but will allow you many more calls to the API and so will speed up the process of retrieving prices. Without an api key and secret, your calls will be throttled.
637640

638641
## Builtin Sections
639642
Builtin sections are used as global configuration of DaLI's behavior.

mypy.ini

+2-2
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ disallow_any_expr = False
9191
disallow_any_explicit = False
9292
disallow_any_expr = False
9393

94-
[mypy-dali.plugin.pair_converter.historic_crypto]
94+
[mypy-dali.plugin.pair_converter.coinbase_advanced]
9595
disallow_any_expr = False
9696
disallow_any_explicit = False
9797

@@ -157,7 +157,7 @@ disallow_any_expr = False
157157
disallow_any_explicit = False
158158
disallow_any_expr = False
159159

160-
[mypy-test_plugin_historic_crypto]
160+
[mypy-test_plugin_coinbase_advanced]
161161
disallow_any_explicit = False
162162
disallow_any_expr = False
163163

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ packages = find:
3333
install_requires =
3434
backports-datetime-fromisoformat>=2.0.3
3535
ccxt==3.0.79
36-
Historic-Crypto>=0.1.6
36+
coinbase-advanced-py==1.8.2
3737
jsonschema>=3.2.0
3838
pandas
3939
pandas-stubs

src/dali/dali_main.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
from dali.plugin.pair_converter.ccxt import (
4949
PairConverterPlugin as CcxtPairConverterPlugin,
5050
)
51-
from dali.plugin.pair_converter.historic_crypto import (
52-
PairConverterPlugin as HistoricCryptoPairConverterPlugin,
51+
from dali.plugin.pair_converter.coinbase_advanced import (
52+
PairConverterPlugin as CoinbaseAdvancedPairConverterPlugin,
5353
)
5454
from dali.transaction_manifest import TransactionManifest
5555
from dali.transaction_resolver import resolve_transactions
@@ -160,7 +160,7 @@ def _dali_main_internal(country: AbstractCountry) -> None:
160160
sys.exit(1)
161161

162162
if not pair_converter_list:
163-
pair_converter_list.append(HistoricCryptoPairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value))
163+
pair_converter_list.append(CoinbaseAdvancedPairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value))
164164
pair_converter_list.append(CcxtPairConverterPlugin(Keyword.HISTORICAL_PRICE_HIGH.value))
165165
LOGGER.info("No pair converter plugins found in configuration file: using default pair converters.")
166166

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2024 orientalperil. Neal Chambers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from datetime import datetime, timedelta, timezone
16+
from typing import Dict, Optional
17+
18+
from coinbase.rest import RESTClient
19+
from rp2.rp2_decimal import RP2Decimal
20+
21+
from dali.abstract_pair_converter_plugin import AbstractPairConverterPlugin
22+
from dali.historical_bar import HistoricalBar
23+
from dali.logger import LOGGER
24+
from dali.transaction_manifest import TransactionManifest
25+
26+
TIME_GRANULARITY: Dict[str, int] = {
27+
"ONE_MINUTE": 60,
28+
"FIVE_MINUTE": 300,
29+
"FIFTEEN_MINUTE": 900,
30+
"THIRTY_MINUTE": 1800,
31+
"ONE_HOUR": 3600,
32+
"TWO_HOUR": 7200,
33+
"SIX_HOUR": 21600,
34+
"ONE_DAY": 86400,
35+
}
36+
37+
38+
class PairConverterPlugin(AbstractPairConverterPlugin):
39+
def __init__(self, historical_price_type: str, api_key: Optional[str] = None, api_secret: Optional[str] = None) -> None:
40+
super().__init__(historical_price_type)
41+
self._authorized: bool = False
42+
if api_key is not None and api_secret is not None:
43+
self.client = RESTClient(api_key=api_key, api_secret=api_secret)
44+
self._authorized = True
45+
else:
46+
self.client = RESTClient()
47+
LOGGER.info(
48+
"API key and API secret were not provided for the Coinbase Advanced Pair Converter Plugin. "
49+
"Requests will be throttled. For faster price resolution, please provide a valid "
50+
"API key and secret in the Dali-rp2 configuration file."
51+
)
52+
53+
def name(self) -> str:
54+
return "dali_dali_coinbase"
55+
56+
def cache_key(self) -> str:
57+
return self.name()
58+
59+
def optimize(self, transaction_manifest: TransactionManifest) -> None:
60+
pass
61+
62+
def get_historic_bar_from_native_source(self, timestamp: datetime, from_asset: str, to_asset: str, exchange: str) -> Optional[HistoricalBar]:
63+
result: Optional[HistoricalBar] = None
64+
utc_timestamp = timestamp.astimezone(timezone.utc)
65+
start = utc_timestamp.replace(second=0)
66+
end = start
67+
retry_count: int = 0
68+
69+
while retry_count < len(TIME_GRANULARITY):
70+
try:
71+
granularity = list(TIME_GRANULARITY.keys())[retry_count]
72+
if self._authorized:
73+
candle = self.client.get_candles(f"{from_asset}-{to_asset}", str(start.timestamp()), str(end.timestamp()), granularity).to_dict()[
74+
"candles"
75+
][0]
76+
else:
77+
candle = self.client.get_public_candles(f"{from_asset}-{to_asset}", str(start.timestamp()), str(end.timestamp()), granularity).to_dict()[
78+
"candles"
79+
][0]
80+
candle_start = datetime.fromtimestamp(int(candle["start"]), timezone.utc)
81+
result = HistoricalBar(
82+
duration=timedelta(seconds=TIME_GRANULARITY[granularity]),
83+
timestamp=candle_start,
84+
open=RP2Decimal(candle["open"]),
85+
high=RP2Decimal(candle["high"]),
86+
low=RP2Decimal(candle["low"]),
87+
close=RP2Decimal(candle["close"]),
88+
volume=RP2Decimal(candle["volume"]),
89+
)
90+
except ValueError:
91+
retry_count += 1
92+
93+
return result

src/dali/plugin/pair_converter/historic_crypto.py

-65
This file was deleted.

src/stubs/coinbase/__init__.py

Whitespace-only changes.

src/stubs/coinbase/rest.pyi

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2024 Neal Chambers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import IO, Any, Optional, Union
16+
17+
class RESTClient:
18+
def __init__(
19+
self,
20+
api_key: Optional[str] = ...,
21+
api_secret: Optional[str] = ...,
22+
key_file: Optional[Union[IO[bytes], str]] = ...,
23+
base_url: Optional[str] = ...,
24+
timeout: Optional[int] = ...,
25+
verbose: Optional[bool] = ...,
26+
rate_limit_headers: Optional[bool] = ...,
27+
) -> None: ...
28+
def get_candles(self, product_id: str, start: str, end: str, granularity: str, limit: Optional[int] = None, **kwargs) -> Any: ... # type: ignore
29+
def get_public_candles(self, product_id: str, start: str, end: str, granularity: str, limit: Optional[int] = None, **kwargs) -> Any: ... # type: ignore

tests/test_plugin_historic_crypto.py tests/test_plugin_coinbase_advanced.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from dali.cache import CACHE_DIR, load_from_cache
2323
from dali.configuration import Keyword
2424
from dali.historical_bar import HistoricalBar
25-
from dali.plugin.pair_converter.historic_crypto import PairConverterPlugin
25+
from dali.plugin.pair_converter.coinbase_advanced import PairConverterPlugin
2626

2727
BAR_DURATION: timedelta = timedelta(seconds=60)
2828
BAR_TIMESTAMP: datetime = datetime(2020, 6, 1, 0, 0).replace(tzinfo=timezone.utc)
@@ -53,7 +53,7 @@ def test_historical_prices(self, mocker: Any) -> None:
5353
)
5454

5555
# Read price without cache
56-
data = plugin.get_historic_bar_from_native_source(BAR_TIMESTAMP, "BTC", "USD", "Coinbase")
56+
data = plugin.get_historic_bar_from_native_source(BAR_TIMESTAMP, "BTC", "USD", "Coinbase Advanced")
5757

5858
assert data
5959
assert data.timestamp == BAR_TIMESTAMP
@@ -65,7 +65,7 @@ def test_historical_prices(self, mocker: Any) -> None:
6565
assert data.volume == BAR_VOLUME
6666

6767
# Read price again, but populate plugin cache this time
68-
value = plugin.get_conversion_rate(BAR_TIMESTAMP, "BTC", "USD", "Coinbase")
68+
value = plugin.get_conversion_rate(BAR_TIMESTAMP, "BTC", "USD", "Coinbase Advanced")
6969
assert value
7070
assert value == BAR_HIGH
7171

@@ -74,7 +74,7 @@ def test_historical_prices(self, mocker: Any) -> None:
7474

7575
# Load plugin cache and verify
7676
cache = load_from_cache(plugin.cache_key())
77-
key = AssetPairAndTimestamp(BAR_TIMESTAMP, "BTC", "USD", "Coinbase")
77+
key = AssetPairAndTimestamp(BAR_TIMESTAMP, "BTC", "USD", "Coinbase Advanced")
7878
assert len(cache) == 1, str(cache)
7979
assert key in cache
8080
data = cache[key]
@@ -94,5 +94,5 @@ def test_missing_historical_prices(self, mocker: Any) -> None:
9494

9595
mocker.patch.object(plugin, "get_historic_bar_from_native_source").return_value = None
9696

97-
data = plugin.get_historic_bar_from_native_source(timestamp, "EUR", "JPY", "Coinbase")
97+
data = plugin.get_historic_bar_from_native_source(timestamp, "EUR", "JPY", "Coinbase Advanced")
9898
assert data is None

0 commit comments

Comments
 (0)