Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Coinbase Advanced Pair Converter Plugin #271

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ All pair converter plugins are subclasses of [AbstractPairConverterPlugin](src/d
* implement the `cache_key()` method;
* implement the `get_historic_bar_from_native_source()` method.

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

### Country Plugin Development
Country plugins are reused from RP2 and their DaLI counterpart has trivial implementation.
Expand Down
7 changes: 5 additions & 2 deletions docs/configuration_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,17 +623,20 @@ Be aware that:
* 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.


### Historic Crypto
This plugin is based on the Historic_Crypto Python library.
### Coinbase Advanced
This plugin is based on the coinbase_advanced Python library.

Initialize this plugin section as follows:
<pre>
[dali.plugin.pair_converter.historic_crypto</em>]
historical_price_type = <em>&lt;historical_price_type&gt;</em>
api_key = <em>&lt;api_key&gt;</em>
api_secret = <em>&lt;api_secret&gt;</em>
</pre>

Where:
* `<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.
* `<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.

## Builtin Sections
Builtin sections are used as global configuration of DaLI's behavior.
Expand Down
5 changes: 3 additions & 2 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ disallow_any_expr = False
disallow_any_explicit = False
disallow_any_expr = False

[mypy-dali.plugin.pair_converter.historic_crypto]
[mypy-dali.plugin.pair_converter.coinbase_advanced]
disallow_any_expr = False
disallow_any_explicit = False

Expand Down Expand Up @@ -157,9 +157,10 @@ disallow_any_expr = False
disallow_any_explicit = False
disallow_any_expr = False

[mypy-test_plugin_historic_crypto]
[mypy-test_plugin_coinbase_advanced]
disallow_any_explicit = False
disallow_any_expr = False
disallow_any_decorated = False

[mypy-test_plugin_kraken_csv_download]
disallow_any_explicit = False
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ packages = find:
install_requires =
backports-datetime-fromisoformat==2.0.1
ccxt==3.0.79
Historic-Crypto>=0.1.6
coinbase-advanced-py==1.8.2
jsonschema>=3.2.0
pandas
prezzemolo>=0.0.4
Expand Down
1 change: 1 addition & 0 deletions src/dali/abstract_ccxt_pair_converter_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
DAYS_IN_WEEK: int = 7
MANY_YEARS_IN_THE_FUTURE: relativedelta = relativedelta(years=100)


class AssetPairAndHistoricalPrice(NamedTuple):
from_asset: str
to_asset: str
Expand Down
6 changes: 3 additions & 3 deletions src/dali/dali_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
from dali.plugin.pair_converter.ccxt import (
PairConverterPlugin as CcxtPairConverterPlugin,
)
from dali.plugin.pair_converter.historic_crypto import (
PairConverterPlugin as HistoricCryptoPairConverterPlugin,
from dali.plugin.pair_converter.coinbase_advanced import (
PairConverterPlugin as CoinbaseAdvancedPairConverterPlugin,
)
from dali.transaction_manifest import TransactionManifest
from dali.transaction_resolver import resolve_transactions
Expand Down Expand Up @@ -160,7 +160,7 @@ def _dali_main_internal(country: AbstractCountry) -> None:
sys.exit(1)

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

Expand Down
94 changes: 94 additions & 0 deletions src/dali/plugin/pair_converter/coinbase_advanced.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright 2024 orientalperil. Neal Chambers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime, timedelta, timezone
from typing import Dict, Optional

from coinbase.rest import RESTClient
from rp2.rp2_decimal import RP2Decimal

from dali.abstract_pair_converter_plugin import AbstractPairConverterPlugin
from dali.historical_bar import HistoricalBar
from dali.logger import LOGGER
from dali.transaction_manifest import TransactionManifest

TIME_GRANULARITY: Dict[str, int] = {
"ONE_MINUTE": 60,
"FIVE_MINUTE": 300,
"FIFTEEN_MINUTE": 900,
"THIRTY_MINUTE": 1800,
"ONE_HOUR": 3600,
"TWO_HOUR": 7200,
"SIX_HOUR": 21600,
"ONE_DAY": 86400,
}


class PairConverterPlugin(AbstractPairConverterPlugin):
def __init__(self, historical_price_type: str, api_key: Optional[str] = None, api_secret: Optional[str] = None) -> None:
super().__init__(historical_price_type)
self._authorized: bool = False
if api_key is not None and api_secret is not None:
self.client = RESTClient(api_key=api_key, api_secret=api_secret)
self._authorized = True
else:
self.client = RESTClient()
LOGGER.info(
"API key and API secret were not provided for the Coinbase Advanced Pair Converter Plugin. "
"Requests will be throttled. For faster price resolution, please provide a valid "
"API key and secret in the Dali-rp2 configuration file."
)

def name(self) -> str:
return "coinbase_advanced"

def cache_key(self) -> str:
return self.name()

def optimize(self, transaction_manifest: TransactionManifest) -> None:
pass

def get_historic_bar_from_native_source(self, timestamp: datetime, from_asset: str, to_asset: str, exchange: str) -> Optional[HistoricalBar]:
result: Optional[HistoricalBar] = None
utc_timestamp = timestamp.astimezone(timezone.utc)
start = utc_timestamp
retry_count: int = 0

while retry_count < len(TIME_GRANULARITY):
macanudo527 marked this conversation as resolved.
Show resolved Hide resolved
try:
granularity = list(TIME_GRANULARITY.keys())[retry_count]
start_epoch = int(start.timestamp())
start_epoch = start_epoch - (start_epoch % TIME_GRANULARITY[granularity])
end_epoch = start_epoch
if self._authorized:
candle = self.client.get_candles(f"{from_asset}-{to_asset}", str(start_epoch), str(end_epoch), granularity).to_dict()["candles"][0]
else:
candle = self.client.get_public_candles(f"{from_asset}-{to_asset}", str(start_epoch), str(end_epoch), granularity).to_dict()["candles"][0]
candle_start = datetime.fromtimestamp(int(candle["start"]), timezone.utc)
result = HistoricalBar(
duration=timedelta(seconds=TIME_GRANULARITY[granularity]),
timestamp=candle_start,
open=RP2Decimal(candle["open"]),
high=RP2Decimal(candle["high"]),
low=RP2Decimal(candle["low"]),
close=RP2Decimal(candle["close"]),
volume=RP2Decimal(candle["volume"]),
)
except ValueError:
retry_count += 1
if result:
break
retry_count += 1

return result
65 changes: 0 additions & 65 deletions src/dali/plugin/pair_converter/historic_crypto.py

This file was deleted.

Empty file added src/stubs/coinbase/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions src/stubs/coinbase/rest.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2024 Neal Chambers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import IO, Any, Optional, Union

class RESTClient:
def __init__(
self,
api_key: Optional[str] = ...,
api_secret: Optional[str] = ...,
key_file: Optional[Union[IO[bytes], str]] = ...,
base_url: Optional[str] = ...,
timeout: Optional[int] = ...,
verbose: Optional[bool] = ...,
rate_limit_headers: Optional[bool] = ...,
) -> None: ...
def get_candles(self, product_id: str, start: str, end: str, granularity: str, limit: Optional[int] = None, **kwargs) -> Any: ... # type: ignore
def get_public_candles(self, product_id: str, start: str, end: str, granularity: str, limit: Optional[int] = None, **kwargs) -> Any: ... # type: ignore
Loading
Loading