]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blobdiff - portfolio.py
Complete refactor of the script to use classes
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
diff --git a/portfolio.py b/portfolio.py
new file mode 100644 (file)
index 0000000..507f796
--- /dev/null
@@ -0,0 +1,430 @@
+import ccxt
+import time
+# Put your poloniex api key in market.py
+from market import market
+
+# FIXME: Améliorer le bid/ask
+# FIXME: J'essayais d'utiliser plus de bitcoins que j'en avais à disposition
+# FIXME: better compute moves to avoid rounding errors
+
+def static_var(varname, value):
+    def decorate(func):
+        setattr(func, varname, value)
+        return func
+    return decorate
+
+class Portfolio:
+    URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
+    liquidities = {}
+    data = None
+
+    @classmethod
+    def repartition_pertenthousand(cls, liquidity="medium"):
+        cls.parse_cryptoportfolio()
+        liquidities = cls.liquidities[liquidity]
+        last_date = sorted(liquidities.keys())[-1]
+        return liquidities[last_date]
+
+    @classmethod
+    def get_cryptoportfolio(cls):
+        import json
+        import urllib3
+        urllib3.disable_warnings()
+        http = urllib3.PoolManager()
+
+        r = http.request("GET", cls.URL)
+        cls.data = json.loads(r.data)
+
+    @classmethod
+    def parse_cryptoportfolio(cls):
+        if cls.data is None:
+            cls.get_cryptoportfolio()
+
+        def filter_weights(weight_hash):
+            if weight_hash[1] == 0:
+                return False
+            if weight_hash[0] == "_row":
+                return False
+            return True
+
+        def clean_weights(i):
+            def clean_weights_(h):
+                if type(h[1][i]) == str:
+                    return [h[0], h[1][i]]
+                else:
+                    return [h[0], int(h[1][i] * 10000)]
+            return clean_weights_
+
+        def parse_weights(portfolio_hash):
+            # FIXME: we'll need shorts at some point
+            assert all(map(lambda x: x == "long", portfolio_hash["holding"]["direction"]))
+            weights_hash = portfolio_hash["weights"]
+            weights = {}
+            for i in range(len(weights_hash["_row"])):
+                weights[weights_hash["_row"][i]] = dict(filter(
+                        filter_weights,
+                        map(clean_weights(i), weights_hash.items())))
+            return weights
+
+        high_liquidity = parse_weights(cls.data["portfolio_1"])
+        medium_liquidity = parse_weights(cls.data["portfolio_2"])
+
+        cls.liquidities = {
+                "medium": medium_liquidity,
+                "high":   high_liquidity,
+                }
+
+class Amount:
+    MAX_DIGITS = 18
+
+    def __init__(self, currency, value, int_val=None, linked_to=None, ticker=None):
+        self.currency = currency
+        if int_val is None:
+            self._value = int(value * 10**self.MAX_DIGITS)
+        else:
+            self._value = int_val
+        self.linked_to = linked_to
+        self.ticker = ticker
+
+        self.ticker_cache = {}
+        self.ticker_cache_timestamp = time.time()
+
+    @property
+    def value(self):
+        return self._value / 10 ** self.MAX_DIGITS
+
+    def in_currency(self, other_currency, market, action="average"):
+        if other_currency == self.currency:
+            return self
+        asset_ticker = self.get_ticker(other_currency, market)
+        if asset_ticker is not None:
+            return Amount(
+                    other_currency,
+                    0,
+                    int_val=int(self._value * asset_ticker[action]),
+                    linked_to=self,
+                    ticker=asset_ticker)
+        else:
+            raise Exception("This asset is not available in the chosen market")
+
+    def get_ticker(self, c2, market, refresh=False):
+        c1 = self.currency
+
+        def invert(ticker):
+            return {
+                    "inverted": True,
+                    "average": (float(1/ticker["bid"]) + float(1/ticker["ask"]) ) / 2,
+                    "notInverted": ticker,
+                    }
+        def augment_ticker(ticker):
+            ticker.update({
+                "inverted": False,
+                "average": (ticker["bid"] + ticker["ask"] ) / 2,
+                })
+
+        if time.time() - self.ticker_cache_timestamp > 5:
+            self.ticker_cache = {}
+            self.ticker_cache_timestamp = time.time()
+        elif not refresh:
+            if (c1, c2, market.__class__) in self.ticker_cache:
+                return self.ticker_cache[(c1, c2, market.__class__)]
+            if (c2, c1, market.__class__) in self.ticker_cache:
+                return invert(self.ticker_cache[(c2, c1, market.__class__)])
+
+        try:
+            self.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
+            augment_ticker(self.ticker_cache[(c1, c2, market.__class__)])
+        except ccxt.ExchangeError:
+            try:
+                self.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
+                augment_ticker(self.ticker_cache[(c2, c1, market.__class__)])
+            except ccxt.ExchangeError:
+                self.ticker_cache[(c1, c2, market.__class__)] = None
+        return self.get_ticker(c2, market)
+
+    def __abs__(self):
+        return Amount(self.currency, 0, int_val=abs(self._value))
+
+    def __add__(self, other):
+        if other.currency != self.currency and other._value * self._value != 0:
+            raise Exception("Summing amounts must be done with same currencies")
+        return Amount(self.currency, 0, int_val=self._value + other._value)
+
+    def __radd__(self, other):
+        if other == 0:
+            return self
+        else:
+            return self.__add__(other)
+
+    def __sub__(self, other):
+        if other.currency != self.currency and other._value * self._value != 0:
+            raise Exception("Summing amounts must be done with same currencies")
+        return Amount(self.currency, 0, int_val=self._value - other._value)
+
+    def __int__(self):
+        return self._value
+
+    def __mul__(self, value):
+        if type(value) != int and type(value) != float:
+            raise TypeError("Amount may only be multiplied by numbers")
+        return Amount(self.currency, 0, int_val=(self._value * value))
+
+    def __rmul__(self, value):
+        return self.__mul__(value)
+
+    def __floordiv__(self, value):
+        if type(value) != int:
+            raise TypeError("Amount may only be multiplied by integers")
+        return Amount(self.currency, 0, int_val=(self._value // value))
+
+    def __truediv__(self, value):
+        return self.__floordiv__(value)
+
+    def __lt__(self, other):
+        if self.currency != other.currency:
+            raise Exception("Comparing amounts must be done with same currencies")
+        return self._value < other._value
+
+    def __eq__(self, other):
+        if other == 0:
+            return self._value == 0
+        if self.currency != other.currency:
+            raise Exception("Comparing amounts must be done with same currencies")
+        return self._value == other._value
+
+    def __str__(self):
+        if self.linked_to is None:
+            return "{:.8f} {}".format(self.value, self.currency)
+        else:
+            return "{:.8f} {} [{}]".format(self.value, self.currency, self.linked_to)
+
+    def __repr__(self):
+        if self.linked_to is None:
+            return "Amount({:.8f} {})".format(self.value, self.currency)
+        else:
+            return "Amount({:.8f} {} -> {})".format(self.value, self.currency, repr(self.linked_to))
+
+class Balance:
+    known_balances = {}
+    trades = {}
+
+    def __init__(self, currency, total_value, free_value, used_value):
+        self.currency = currency
+        self.total = Amount(currency, total_value)
+        self.free = Amount(currency, free_value)
+        self.used = Amount(currency, used_value)
+
+    @classmethod
+    def in_currency(cls, other_currency, market, action="average", type="total"):
+        amounts = {}
+        for currency in cls.known_balances:
+            balance = cls.known_balances[currency]
+            other_currency_amount = getattr(balance, type)\
+                    .in_currency(other_currency, market, action=action)
+            amounts[currency] = other_currency_amount
+        return amounts
+
+    @classmethod
+    def currencies(cls):
+        return cls.known_balances.keys()
+
+    @classmethod
+    def from_hash(cls, currency, hash_):
+        return cls(currency, hash_["total"], hash_["free"], hash_["used"])
+
+    @classmethod
+    def _fill_balances(cls, hash_):
+        for key in hash_:
+            if key in ["info", "free", "used", "total"]:
+                continue
+            if hash_[key]["total"] > 0:
+                cls.known_balances[key] = cls.from_hash(key, hash_[key])
+
+    @classmethod
+    def fetch_balances(cls, market):
+        cls._fill_balances(market.fetch_balance())
+        return cls.known_balances
+
+    @classmethod
+    def dispatch_assets(cls, amount):
+        repartition_pertenthousand = Portfolio.repartition_pertenthousand()
+        sum_pertenthousand = sum([v for k, v in repartition_pertenthousand.items()])
+        amounts = {}
+        for currency, ptt in repartition_pertenthousand.items():
+            amounts[currency] = ptt * amount / sum_pertenthousand
+            if currency not in cls.known_balances:
+                cls.known_balances[currency] = cls(currency, 0, 0, 0)
+        return amounts
+
+    @classmethod
+    def prepare_trades(cls, market, base_currency="BTC"):
+        cls.fetch_balances(market)
+        values_in_base = cls.in_currency(base_currency, market)
+        total_base_value = sum(values_in_base.values())
+        new_repartition = cls.dispatch_assets(total_base_value)
+        Trade.compute_trades(values_in_base, new_repartition, market=market)
+
+    def __int__(self):
+        return int(self.total)
+
+    def __repr__(self):
+        return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total))
+
+class Trade:
+    trades = {}
+
+    def __init__(self, value_from, value_to, currency, market=None):
+        # We have value_from of currency, and want to finish with value_to of
+        # that currency. value_* may not be in currency's terms
+        self.currency = currency
+        self.value_from = value_from
+        self.value_to = value_to
+        self.orders = []
+        assert self.value_from.currency == self.value_to.currency
+        assert self.value_from.linked_to is not None and self.value_from.linked_to.currency == self.currency
+        self.base_currency = self.value_from.currency
+
+        if market is not None:
+            self.prepare_order(market)
+
+    @classmethod
+    def compute_trades(cls, values_in_base, new_repartition, market=None):
+        base_currency = sum(values_in_base.values()).currency
+        for currency in Balance.currencies():
+            if currency == base_currency:
+                continue
+            cls.trades[currency] = cls(
+                values_in_base.get(currency, Amount(base_currency, 0)), 
+                new_repartition.get(currency, Amount(base_currency, 0)),
+                currency,
+                market=market
+                )
+        return cls.trades
+
+    @property
+    def action(self):
+        if self.value_from == self.value_to:
+            return None
+        if self.base_currency == self.currency:
+            return None
+
+        if self.value_from < self.value_to:
+            return "buy"
+        else:
+            return "sell"
+
+    def ticker_action(self, inverted):
+        if self.value_from < self.value_to:
+            return "ask" if not inverted else "bid"
+        else:
+            return "bid" if not inverted else "ask"
+
+    def prepare_order(self, market):
+        if self.action is None:
+            return
+        ticker = self.value_from.ticker
+        inverted = ticker["inverted"]
+
+        if not inverted:
+            value_from = self.value_from.linked_to
+            value_to = self.value_to.in_currency(self.currency, market)
+            delta = abs(value_to - value_from)
+            currency = self.base_currency
+        else:
+            ticker = ticker["notInverted"]
+            delta = abs(self.value_to - self.value_from)
+            currency = self.currency
+
+        rate = ticker[self.ticker_action(inverted)]
+
+        self.orders.append(Order(self.ticker_action(inverted), delta, rate, currency))
+
+    @classmethod
+    def all_orders(cls):
+        return sum(map(lambda v: v.orders, cls.trades.values()), [])
+
+    @classmethod
+    def follow_orders(cls, market):
+        orders = cls.all_orders()
+        finished_orders = []
+        while len(orders) != len(finished_orders):
+            time.sleep(30)
+            for order in orders:
+                if order in finished_orders:
+                    continue
+                if order.get_status(market) != "open":
+                    finished_orders.append(order)
+                    print("finished {}".format(order))
+        print("All orders finished")
+
+    def __repr__(self):
+        return "Trade({} -> {} in {}, {})".format(
+                self.value_from,
+                self.value_to,
+                self.currency,
+                self.action)
+
+class Order:
+    DEBUG = True
+
+    def __init__(self, action, amount, rate, base_currency):
+        self.action = action
+        self.amount = amount
+        self.rate = rate
+        self.base_currency = base_currency
+        self.result = None
+        self.status = "not run"
+
+    def __repr__(self):
+        return "Order({} {} at {} {} [{}])".format(
+                self.action,
+                self.amount,
+                self.rate,
+                self.base_currency,
+                self.status
+                )
+
+    def run(self, market):
+        symbol = "{}/{}".format(self.amount.currency, self.base_currency)
+        amount = self.amount.value
+
+        if self.DEBUG:
+            print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
+                symbol, self.action, amount, self.rate))
+        else:
+            try:
+                self.result = market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
+                self.status = "open"
+            except Exception:
+                pass
+
+    def get_status(self, market):
+        # other states are "closed" and "canceled"
+        if self.status == "open":
+            result = market.fetch_order(self.result['id'])
+            self.status = result["status"]
+        return self.status
+
+@static_var("cache", {})
+def fetch_fees(market):
+    if market.__class__ not in fetch_fees.cache:
+        fetch_fees.cache[market.__class__] = market.fetch_fees()
+    return fetch_fees.cache[market.__class__]
+
+def print_orders(market, base_currency="BTC"):
+    Balance.prepare_trades(market, base_currency=base_currency)
+    for currency, trade in Trade.trades.items():
+        print(trade)
+        for order in trade.orders:
+            print("\t", order, sep="")
+
+def make_orders(market, base_currency="BTC"):
+    Balance.prepare_trades(market, base_currency=base_currency)
+    for currency, trade in Trade.trades.items():
+        print(trade)
+        for order in trade.orders:
+            print("\t", order, sep="")
+            order.run(market)
+
+if __name__ == '__main__':
+    print_orders(market)