aboutsummaryrefslogblamecommitdiff
path: root/portfolio.py
blob: 1d8bfd5f122775df881bcbbdd3a76e8422740896 (plain) (tree)

































                                                                                             







                                            




















































































































































































                                                                                                           



                                                                          













                                                                                 






























                                                                                    
































































































































































                                                                                                              
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()

        try:
            r = http.request("GET", cls.URL)
        except Exception:
            return
        try:
            cls.data = json.loads(r.data)
        except json.JSONDecodeError:
            cls.data = None

    @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 from_hash(cls, currency, hash_):
        return cls(currency, hash_["total"], hash_["free"], hash_["used"])

    @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 _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 __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)