Skip to content

Commit

Permalink
Merge pull request #25 from EtWnn/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
EtWnn authored Apr 27, 2021
2 parents e864d46 + 743ae47 commit 2ac234c
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 40 deletions.
12 changes: 10 additions & 2 deletions CryptoPrice/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
__version__ = "0.1.0"
__version__ = "0.1.1"
__author__ = "EtWnn"

from CryptoPrice.retrievers.BinanceRetriever import BinanceRetriever
from CryptoPrice.retrievers.KucoinRetriever import KucoinRetriever
from CryptoPrice.retrievers.MetaRetriever import MetaRetriever

retriever = MetaRetriever([BinanceRetriever(), KucoinRetriever()])

def get_default_retriever() -> MetaRetriever:
"""
Provides a hands on price retriever made from the default BinanceRetriever and the default KucoinRetriever
:return: the meta retriever constructed
:rtype: MetaRetriever
"""
return MetaRetriever([BinanceRetriever(), KucoinRetriever()])
16 changes: 10 additions & 6 deletions CryptoPrice/common/prices.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Union
from typing import List, Union, Set

from CryptoPrice.utils.time import TIMEFRAME


@dataclass
@dataclass(frozen=True)
class Price:
value: float
asset: str
Expand All @@ -14,13 +14,13 @@ class Price:
source: str


@dataclass
@dataclass(frozen=True)
class MetaPrice:
value: float
asset: str
ref_asset: str
prices: List[Union[Price, MetaPrice]]
source: str = ''
source: Set[str]

@staticmethod
def mean_from_meta_price(meta_prices: List[MetaPrice]) -> MetaPrice:
Expand All @@ -32,6 +32,7 @@ def mean_from_meta_price(meta_prices: List[MetaPrice]) -> MetaPrice:
:return: the mean Meta price
:rtype: MetaPrice
"""
source = set()
if len(meta_prices) == 0:
raise ValueError("at least one MetaPrice is needed")
asset = meta_prices[0].asset
Expand All @@ -41,7 +42,8 @@ def mean_from_meta_price(meta_prices: List[MetaPrice]) -> MetaPrice:
if (asset, ref_asset) != (meta_price.asset, meta_price.ref_asset):
raise ValueError("asset and ref asset are inconsistent")
cum_value += meta_price.value
return MetaPrice(cum_value / len(meta_prices), asset, ref_asset, meta_prices, source="mean_meta")
source.update(meta_price.source)
return MetaPrice(cum_value / len(meta_prices), asset, ref_asset, meta_prices, source=source)

@staticmethod
def from_price_path(assets: List[str], price_path: List[Price]) -> MetaPrice:
Expand All @@ -55,6 +57,7 @@ def from_price_path(assets: List[str], price_path: List[Price]) -> MetaPrice:
:return: the MetaPrice representing the price between the first and the last asset
:rtype: MetaPrice
"""
source = set()
if len(assets) < 2:
raise ValueError(f"at least two assets are required, {len(assets)} were received")
if len(assets) != len(price_path) + 1:
Expand All @@ -66,7 +69,8 @@ def from_price_path(assets: List[str], price_path: List[Price]) -> MetaPrice:
cumulated_price /= price.value
else:
cumulated_price *= price.value
return MetaPrice(cumulated_price, assets[0], assets[-1], price_path, source='price_path')
source.add(price.source)
return MetaPrice(cumulated_price, assets[0], assets[-1], price_path, source=source)


@dataclass
Expand Down
2 changes: 2 additions & 0 deletions CryptoPrice/retrievers/AbstractRetriever.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def get_closest_price(self, asset: str, ref_asset: str, timestamp: int) -> Optio
:return: the price closest in time found or None if no price found
:rtype: Optional[Price]
"""
if asset == ref_asset:
return Price(1, asset, ref_asset, timestamp, source='')
trading_pair = TradingPair('', asset, ref_asset, '')
if trading_pair not in self.supported_pairs:
return
Expand Down
10 changes: 7 additions & 3 deletions CryptoPrice/retrievers/KucoinRetriever.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,15 @@ def _get_klines_online(self, asset: str, ref_asset: str, timeframe: TIMEFRAME,
if not isinstance(result, List): # valid trading pair but no data
return []
except Exception as e:
if len(e.args):
if "403" in e.args[0]: # Kucoin exception for rate limit
if e.__class__ == Exception: # kucoin.client's exceptions are from the base class only
try:
error_content = e.args[0] # error code and error message from kucoin.client are in a single string
except (IndexError, TypeError):
raise e
if "403" in error_content: # kucoin.client's for rate limit
retry_after = 1 + 60 - datetime.datetime.now().timestamp() % 60 # time until next minute
raise RateAPIException(retry_after)
elif "400100" in e.args[0]: # invalid parameters -> trading pair not supported
elif "400100" in error_content: # invalid parameters -> trading pair not supported
return []
raise e
klines = []
Expand Down
54 changes: 35 additions & 19 deletions CryptoPrice/retrievers/MetaRetriever.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ def _get_closest_price(self, asset: str, ref_asset: str, timestamp: int) -> Opti
return price

def get_mean_price(self, asset: str, ref_asset: str, timestamp: int, preferred_assets: Optional[List[str]] = None,
max_depth: int = 2, max_plateau: int = 1) -> Optional[MetaPrice]:
max_depth: int = 3, max_depth_range: int = 0) -> Optional[MetaPrice]:
"""
Will use the method get_path_price and return the mean price of an asset compared to a reference asset
Will use the method get_path_prices and return the mean price of an asset compared to a reference asset
on a given timestamp.
:param asset: name of the asset to get the price of
Expand All @@ -60,44 +60,46 @@ def get_mean_price(self, asset: str, ref_asset: str, timestamp: int, preferred_a
:type preferred_assets: Optional[List[str]]
:param max_depth: maximum number of trading pair to use, default 3
:type max_depth: int
:param max_plateau: maximum number of plateau to consider. A plateau is a group of trading path with the
same number of trading pair. For example, a max_plateau of 2 will allow trading path with a length of 1 and
2 (or length of 2 and 3, depending of the minimum length)
:type max_plateau: int
:param max_depth_range: maximum length difference between different trading path. If the first trading path has
a length of 1 and this parameter is equal to 2, trading_path with a length superior to 3 will be ignored
:type max_depth_range: int
:return: Metaprice reflecting the value calculated with a mean of trading path
:rtype: Optional[MetaPrice]
"""
meta_prices = []
min_plateau = None
for meta_price in self.get_path_price(asset, ref_asset, timestamp, preferred_assets, max_depth):
for meta_price in self.get_path_prices(asset, ref_asset, timestamp, preferred_assets,
max_depth, max_depth_range):
if meta_price is not None:
if min_plateau is None:
min_plateau = len(meta_price.prices)
elif len(meta_price.prices) >= min_plateau + max_plateau:
break
meta_prices.append(meta_price)
if len(meta_prices):
return MetaPrice.mean_from_meta_price(meta_prices)

def get_path_price(self, asset: str, ref_asset: str, timestamp: int,
preferred_assets: Optional[List[str]] = None, max_depth: int = 3) -> Iterator[MetaPrice]:
def get_path_prices(self, asset: str, ref_asset: str, timestamp: int,
preferred_assets: Optional[List[str]] = None, max_depth: int = 2,
max_depth_range: int = -1) -> Iterator[MetaPrice]:
"""
Iterator that return MetaPrices that estimates the price of an asset compared to a reference asset.
It will use the trading pair at its disposal to create trading path from the asset to the ref asset.
It use a BFS algorithm, so the shortest path will be returned first.
If no price is found, return None
:param max_depth_range:
:type max_depth_range:
:param asset: name of the asset to get the price of
:type asset: str
:param ref_asset: name of the reference asset
:type ref_asset: str
:param timestamp: time to fetch the price needed (in seconds)
:type timestamp: int
:param preferred_assets: list of assets to construct the price path from. If None, default value is
['BTC', 'USDT', 'BUSD', 'ETH']
['BTC', 'ETH']
:type preferred_assets: Optional[List[str]]
:param max_depth: maximum number of trading pair to use, default 3
:param max_depth: maximum number of trading pair to use, default 2
:type max_depth: int
:param max_depth_range: maximum length difference between different trading path. If the first trading path has
a length of 1 and this parameter is equal to 2, trading_path with a length superior to 3 will be ignored.
Default -1 means that this parameter is ignored.
:type max_depth_range: int
:return: Metaprice reflecting the value calculated through a trading path
:rtype: Optional[MetaPrice]
"""
Expand All @@ -113,7 +115,8 @@ def get_path_price(self, asset: str, ref_asset: str, timestamp: int,
seen_assets = [asset]
current_path = []
for assets_p, trade_p in self._explore_assets_path(ref_asset, assets_neighbours, seen_assets,
current_path, max_depth=max_depth):
current_path, max_depth=max_depth,
max_depth_range=max_depth_range):
price_path = []
for pair in trade_p:
price = self.retrievers[pair.source].get_closest_price(pair.asset, pair.ref_asset, timestamp)
Expand Down Expand Up @@ -155,8 +158,8 @@ def construct_assets_neighbours(self, asset_subsets: List[str]) -> Dict:

@staticmethod
def _explore_assets_path(target_asset: str, assets_neighbours: Dict,
seen_assets: List[str], current_path: List[TradingPair],
max_depth: int = 3) -> Iterator[Tuple[List[str], List[TradingPair]]]:
seen_assets: List[str], current_path: List[TradingPair], max_depth: int = 3,
max_depth_range: int = -1) -> Iterator[Tuple[List[str], List[TradingPair]]]:
"""
Iterator that will explore the trading possibilities to link one asset to another one through a trading path
Expand All @@ -170,18 +173,31 @@ def _explore_assets_path(target_asset: str, assets_neighbours: Dict,
:type current_path: List[TradingPair]
:param max_depth: maximum number of trading pair to use, default 3
:type max_depth: int
:param max_depth_range: maximum length difference between different trading path. If the first trading path has
a length of 1 and this parameter is equal to 2, trading_path with a length superior to 3 will be ignored.
Default -1 means that this parameter is ignored.
:type max_depth_range: int
:return: list of assets to explore and the trading path to take
:rtype: Tuple[List[str], List[TradingPair]]
"""
to_explore = Queue()
to_explore.put((seen_assets, current_path))
min_depth = None
while to_explore.qsize():
seen_assets, current_path = to_explore.get()
current_asset = seen_assets[-1]
if min_depth is not None and len(current_path) + 1 - min_depth > max_depth_range:
break

for next_asset in assets_neighbours[current_asset]:

if next_asset not in seen_assets:

for pair in assets_neighbours[current_asset][next_asset]:
if next_asset == target_asset:
if min_depth is None and max_depth_range >= 0:
min_depth = len(current_path) + 1
yield seen_assets + [next_asset], current_path + [pair]

elif len(current_path) + 1 < max_depth:
to_explore.put((seen_assets + [next_asset], current_path + [pair]))
12 changes: 8 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
===============================
Welcome to CryptoPrice 0.1.0
Welcome to CryptoPrice 0.1.1
===============================


Expand Down Expand Up @@ -54,7 +54,9 @@ A price retriever is already provided by the library, but feel free to check the
.. code-block:: python
import datetime
from CryptoPrice import retriever
from CryptoPrice import get_default_retriever
retriever = get_default_retriever()
asset = 'BTC'
ref_asset = 'USDT'
Expand All @@ -74,7 +76,9 @@ than the one above as several API calls (or database requests) have to be made.
.. code-block:: python
import datetime
from CryptoPrice import retriever
from CryptoPrice import get_default_retriever
retriever = get_default_retriever()
asset = 'LTC'
ref_asset = 'XRP'
Expand All @@ -87,7 +91,7 @@ than the one above as several API calls (or database requests) have to be made.
.. code-block:: bash
>>LTC = 420.80573 XRP, source: mean_meta
>>LTC = 420.80573 XRP, source: {'kucoin', 'binance'}
Donation
Expand Down
18 changes: 12 additions & 6 deletions docs/source/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ This library only use public API endpoints, so there is no need to register to a
Get a retriever
---------------

To fetch some prices, you will need to use a retriever, there are several retrievers in this library
but one is already instantiated for you:
To fetch some prices, you will need to use a retriever, there are several kinds and instances of retrievers in this
library so we made a default one for you:

.. code:: python
from CryptoPrice import retriever
from CryptoPrice import get_default_retriever
retriever = get_default_retriever()
You can also refer to the :doc:`retrievers` to learn how to instantiate your custom retriever.

Expand All @@ -43,7 +45,9 @@ Here are some examples, with the retriever instantiated in the library
.. code-block:: python
import datetime
from CryptoPrice import retriever
from CryptoPrice import get_default_retriever
retriever = get_default_retriever()
asset = 'BTC'
ref_asset = 'USDT'
Expand All @@ -63,7 +67,9 @@ than the one above as several API calls (or database requests) have to be made.
.. code-block:: python
import datetime
from CryptoPrice import retriever
from CryptoPrice import get_default_retriever
retriever = get_default_retriever()
asset = 'LTC'
ref_asset = 'XRP'
Expand All @@ -76,4 +82,4 @@ than the one above as several API calls (or database requests) have to be made.
.. code-block:: bash
>>LTC = 420.80573 XRP, source: mean_meta
>>LTC = 420.80573 XRP, source: {'kucoin', 'binance'}

0 comments on commit 2ac234c

Please sign in to comment.