# -*- coding: utf-8 -*-
from fractions import Fraction
from .exceptions import InvalidAssetException
from .utils import assets_from_string, formatTimeString, parse_time
from .instance import AbstractBlockchainInstanceProvider
class Price(dict, AbstractBlockchainInstanceProvider):
""" This class deals with all sorts of prices of any pair of assets to
simplify dealing with the tuple::
(quote, base)
each being an instance of :class:`.amount.Amount`. The
amount themselves define the price.
.. note::
The price (floating) is derived as ``base/quote``
:param list args: Allows to deal with different representations of a price
:param asset.Asset base: Base asset
:param asset.Asset quote: Quote asset
:param instance blockchain_instance: instance to use when accesing a RPC
:returns: All data required to represent a price
:rtype: dict
Way to obtain a proper instance:
* ``args`` is a str with a price and two assets
* ``args`` can be a floating number and ``base`` and ``quote`` being instances of :class:`.asset.Asset`
* ``args`` can be a floating number and ``base`` and ``quote`` being instances of ``str``
* ``args`` can be dict with keys ``price``, ``base``, and ``quote`` (*graphene balances*)
* ``args`` can be dict with keys ``base`` and ``quote``
* ``args`` can be dict with key ``receives`` (filled orders)
* ``args`` being a list of ``[quote, base]`` both being instances of :class:`.amount.Amount`
* ``args`` being a list of ``[quote, base]`` both being instances of ``str`` (``amount symbol``)
* ``base`` and ``quote`` being instances of :class:`.asset.Amount`
This allows instanciations like:
* ``Price("0.315 USD/BTS")``
* ``Price(0.315, base="USD", quote="BTS")``
* ``Price(0.315, base=self.asset_class("USD"), quote=self.asset_class("BTS"))``
* ``Price({"base": {"amount": 1, "asset_id": "1.3.0"}, "quote": {"amount": 10, "asset_id": "1.3.106"}})``
* ``Price({"receives": {"amount": 1, "asset_id": "1.3.0"}, "pays": {"amount": 10, "asset_id": "1.3.106"}}, base_asset=self.asset_class("1.3.0"))``
* ``Price(quote="10 GOLD", base="1 USD")``
* ``Price("10 GOLD", "1 USD")``
* ``Price(self.amount_class("10 GOLD"), self.amount_class("1 USD"))``
* ``Price(1.0, "USD/GOLD")``
Instances of this class can be used in regular mathematical expressions
(``+-*/%``) such as:
.. code-block:: python
>>> from price import Price
>>> Price("0.3314 USD/BTS") * 2
0.662600000 USD/BTS
"""
def __init__(
self,
*args,
base=None,
quote=None,
base_asset=None, # to identify sell/buy
**kwargs
):
self.define_classes()
assert self.amount_class
assert self.asset_class
if len(args) == 1 and isinstance(args[0], str) and not base and not quote:
import re
price, assets = args[0].split(" ")
base_symbol, quote_symbol = assets_from_string(assets)
base = self.asset_class(base_symbol, blockchain_instance=self.blockchain)
quote = self.asset_class(quote_symbol, blockchain_instance=self.blockchain)
frac = Fraction(float(price)).limit_denominator(10 ** base["precision"])
self["quote"] = self.amount_class(
amount=frac.denominator,
asset=quote,
blockchain_instance=self.blockchain,
)
self["base"] = self.amount_class(
amount=frac.numerator, asset=base, blockchain_instance=self.blockchain
)
elif (
len(args) == 1
and isinstance(args[0], dict)
and "base" in args[0]
and "quote" in args[0]
):
assert "price" not in args[0], "You cannot provide a 'price' this way"
# Regular 'price' objects according to the backend
base_id = args[0]["base"]["asset_id"]
if args[0]["base"]["asset_id"] == base_id:
self["base"] = self.amount_class(
args[0]["base"], blockchain_instance=self.blockchain
)
self["quote"] = self.amount_class(
args[0]["quote"], blockchain_instance=self.blockchain
)
else:
self["quote"] = self.amount_class(
args[0]["base"], blockchain_instance=self.blockchain
)
self["base"] = self.amount_class(
args[0]["quote"], blockchain_instance=self.blockchain
)
elif len(args) == 1 and isinstance(args[0], dict) and "receives" in args[0]:
# Filled order
assert base_asset, "Need a 'base_asset' asset"
base_asset = self.asset_class(
base_asset, blockchain_instance=self.blockchain
)
if args[0]["receives"]["asset_id"] == base_asset["id"]:
# If the seller received "base" in a quote_base market, than
# it has been a sell order of quote
self["base"] = self.amount_class(
args[0]["receives"], blockchain_instance=self.blockchain
)
self["quote"] = self.amount_class(
args[0]["pays"], blockchain_instance=self.blockchain
)
self["type"] = "sell"
else:
# buy order
self["base"] = self.amount_class(
args[0]["pays"], blockchain_instance=self.blockchain
)
self["quote"] = self.amount_class(
args[0]["receives"], blockchain_instance=self.blockchain
)
self["type"] = "buy"
elif len(args) == 1 and (
isinstance(base, self.asset_class) and isinstance(quote, self.asset_class)
):
price = args[0]
frac = Fraction(float(price)).limit_denominator(10 ** base["precision"])
self["quote"] = self.amount_class(
amount=frac.denominator,
asset=quote,
blockchain_instance=self.blockchain,
)
self["base"] = self.amount_class(
amount=frac.numerator, asset=base, blockchain_instance=self.blockchain
)
elif len(args) == 1 and (
isinstance(base, self.amount_class) and isinstance(quote, self.amount_class)
):
price = args[0]
self["quote"] = quote
self["base"] = base
elif len(args) == 1 and isinstance(base, str) and isinstance(quote, str):
price = args[0]
base = self.asset_class(base, blockchain_instance=self.blockchain)
quote = self.asset_class(quote, blockchain_instance=self.blockchain)
frac = Fraction(float(price)).limit_denominator(10 ** base["precision"])
self["quote"] = self.amount_class(
amount=frac.denominator,
asset=quote,
blockchain_instance=self.blockchain,
)
self["base"] = self.amount_class(
amount=frac.numerator, asset=base, blockchain_instance=self.blockchain
)
elif len(args) == 0 and isinstance(base, str) and isinstance(quote, str):
self["quote"] = self.amount_class(
quote, blockchain_instance=self.blockchain
)
self["base"] = self.amount_class(base, blockchain_instance=self.blockchain)
# len(args) > 1
elif len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], str):
self["base"] = self.amount_class(
args[1], blockchain_instance=self.blockchain
)
self["quote"] = self.amount_class(
args[0], blockchain_instance=self.blockchain
)
elif (
len(args) == 2
and isinstance(args[0], self.amount_class)
and isinstance(args[1], self.amount_class)
):
self["quote"], self["base"] = args[0], args[1]
# len(args) == 0
elif isinstance(base, self.amount_class) and isinstance(
quote, self.amount_class
):
self["quote"] = quote
self["base"] = base
elif (
len(args) == 2
and (isinstance(args[0], float) or isinstance(args[0], int))
and isinstance(args[1], str)
):
import re
price = args[0]
base_symbol, quote_symbol = assets_from_string(args[1])
base = self.asset_class(base_symbol, blockchain_instance=self.blockchain)
quote = self.asset_class(quote_symbol, blockchain_instance=self.blockchain)
frac = Fraction(float(price)).limit_denominator(10 ** base["precision"])
self["quote"] = self.amount_class(
amount=frac.denominator,
asset=quote,
blockchain_instance=self.blockchain,
)
self["base"] = self.amount_class(
amount=frac.numerator, asset=base, blockchain_instance=self.blockchain
)
else:
raise ValueError("Couldn't parse 'Price'.")
def __setitem__(self, key, value):
""" Here we set "price" if we change quote or base
"""
dict.__setitem__(self, key, value)
if (
"quote" in self and "base" in self and self["base"] and self["quote"]
): # don't derive price for deleted Orders
dict.__setitem__(
self,
"price",
self._safedivide(self["base"]["amount"], self["quote"]["amount"]),
)
[docs] def copy(self):
return self.__class__(base=self["base"].copy(), quote=self["quote"].copy())
def _safedivide(self, a, b):
if b != 0.0:
return a / b
else:
return float("Inf")
[docs] def symbols(self):
return self["base"]["symbol"], self["quote"]["symbol"]
[docs] def as_base(self, base):
""" Returns the price instance so that the base asset is ``base``.
Note: This makes a copy of the object!
"""
if base == self["base"]["symbol"]:
return self.copy()
elif base == self["quote"]["symbol"]:
return self.copy().invert()
else:
raise InvalidAssetException
[docs] def as_quote(self, quote):
""" Returns the price instance so that the quote asset is ``quote``.
Note: This makes a copy of the object!
"""
if quote == self["quote"]["symbol"]:
return self.copy()
elif quote == self["base"]["symbol"]:
return self.copy().invert()
else:
raise InvalidAssetException
[docs] def invert(self):
""" Invert the price (e.g. go from ``USD/BTS`` into ``BTS/USD``)
"""
tmp = self["quote"]
self["quote"] = self["base"]
self["base"] = tmp
if "for_sale" in self and self["for_sale"]:
self["for_sale"] = self.amount_class(
self["for_sale"]["amount"] * self["price"],
self["base"]["symbol"],
blockchain_instance=self.blockchain,
)
return self
[docs] def json(self):
"""
return {
"base": self["base"].json(),
"quote": self["quote"].json()
}
"""
quote = self["quote"]
base = self["base"]
frac = Fraction(int(quote) / int(base)).limit_denominator(
10 ** base["asset"]["precision"]
)
return {
"base": {"amount": int(frac.denominator), "asset_id": base["asset"]["id"]},
"quote": {"amount": int(frac.numerator), "asset_id": quote["asset"]["id"]},
}
def __repr__(self):
return "{price:.{precision}f} {base}/{quote}".format(
price=self["price"],
base=self["base"]["symbol"],
quote=self["quote"]["symbol"],
precision=(
self["base"]["asset"]["precision"] + self["quote"]["asset"]["precision"]
),
)
def __float__(self):
return self["price"]
def __mul__(self, other):
a = self.copy()
if isinstance(other, Price):
# Rotate/invert other
if (
self["quote"]["symbol"] not in other.symbols()
and self["base"]["symbol"] not in other.symbols()
):
raise InvalidAssetException
# base/quote = a/b
# a/b * b/c = a/c
a = self.copy()
if self["quote"]["symbol"] == other["base"]["symbol"]:
a["base"] = self.amount_class(
float(self["base"]) * float(other["base"]),
self["base"]["symbol"],
blockchain_instance=self.blockchain,
)
a["quote"] = self.amount_class(
float(self["quote"]) * float(other["quote"]),
other["quote"]["symbol"],
blockchain_instance=self.blockchain,
)
# a/b * c/a = c/b
elif self["base"]["symbol"] == other["quote"]["symbol"]:
a["base"] = self.amount_class(
float(self["base"]) * float(other["base"]),
other["base"]["symbol"],
blockchain_instance=self.blockchain,
)
a["quote"] = self.amount_class(
float(self["quote"]) * float(other["quote"]),
self["quote"]["symbol"],
blockchain_instance=self.blockchain,
)
else:
raise ValueError("Wrong rotation of prices")
elif isinstance(other, self.amount_class):
assert other["asset"]["id"] == self["quote"]["asset"]["id"]
a = other.copy() * self["price"]
a["asset"] = self["base"]["asset"].copy()
a["symbol"] = self["base"]["asset"]["symbol"]
else:
a["base"] *= other
return a
def __imul__(self, other):
if isinstance(other, Price):
tmp = self * other
self["base"] = tmp["base"]
self["quote"] = tmp["quote"]
else:
self["base"] *= other
return self
def __div__(self, other):
a = self.copy()
if isinstance(other, Price):
# Rotate/invert other
if sorted(self.symbols()) == sorted(other.symbols()):
return float(self.as_base(self["base"]["symbol"])) / float(
other.as_base(self["base"]["symbol"])
)
elif self["quote"]["symbol"] in other.symbols():
other = other.as_base(self["quote"]["symbol"])
elif self["base"]["symbol"] in other.symbols():
other = other.as_base(self["base"]["symbol"])
else:
raise InvalidAssetException
a["base"] = self.amount_class(
float(self["quote"] / other["quote"]),
other["quote"]["symbol"],
blockchain_instance=self.blockchain,
)
a["quote"] = self.amount_class(
float(self["base"] / other["base"]),
self["quote"]["symbol"],
blockchain_instance=self.blockchain,
)
elif isinstance(other, self.amount_class):
assert other["asset"]["id"] == self["quote"]["asset"]["id"]
a = other.copy() / self["price"]
a["asset"] = self["base"]["asset"].copy()
a["symbol"] = self["base"]["asset"]["symbol"]
else:
a["base"] /= other
return a
def __idiv__(self, other):
if isinstance(other, Price):
tmp = self / other
self["base"] = tmp["base"]
self["quote"] = tmp["quote"]
else:
self["base"] /= other
return self
def __floordiv__(self, other):
raise NotImplementedError("This is not possible as the price is a ratio")
def __ifloordiv__(self, other):
raise NotImplementedError("This is not possible as the price is a ratio")
def __lt__(self, other):
if isinstance(other, Price):
assert other["base"]["symbol"] == self["base"]["symbol"]
assert other["quote"]["symbol"] == self["quote"]["symbol"]
return self["price"] < other["price"]
else:
return self["price"] < float(other or 0)
def __le__(self, other):
if isinstance(other, Price):
assert other["base"]["symbol"] == self["base"]["symbol"]
assert other["quote"]["symbol"] == self["quote"]["symbol"]
return self["price"] <= other["price"]
else:
return self["price"] <= float(other or 0)
def __eq__(self, other):
if isinstance(other, Price):
assert other["base"]["symbol"] == self["base"]["symbol"]
assert other["quote"]["symbol"] == self["quote"]["symbol"]
return self["price"] == other["price"]
else:
return self["price"] == float(other or 0)
def __ne__(self, other):
if isinstance(other, Price):
assert other["base"]["symbol"] == self["base"]["symbol"]
assert other["quote"]["symbol"] == self["quote"]["symbol"]
return self["price"] != other["price"]
else:
return self["price"] != float(other or 0)
def __ge__(self, other):
if isinstance(other, Price):
assert other["base"]["symbol"] == self["base"]["symbol"]
assert other["quote"]["symbol"] == self["quote"]["symbol"]
return self["price"] >= other["price"]
else:
return self["price"] >= float(other or 0)
def __gt__(self, other):
if isinstance(other, Price):
assert other["base"]["symbol"] == self["base"]["symbol"]
assert other["quote"]["symbol"] == self["quote"]["symbol"]
return self["price"] > other["price"]
else:
return self["price"] > float(other or 0)
__truediv__ = __div__
__truemul__ = __mul__
__str__ = __repr__