aboutsummaryrefslogblamecommitdiff
path: root/portfolio.py
blob: cb14c5d6223d44751f6f99d1f886e9a8aa5de7c2 (plain) (tree)
1
2
3
4
5
6

           
                                


                                        








                                                                                             

                                                      







                                    




                                            


                                         

                                    








































                                                                                          
                                                                                
                                
                             

                                  
                        



                                                 
                                                                                                   

                                           





                                      
                                                                              
                                    
                                                                                         

                                   
                                      
                                   

                                        


                                                                               
                      
                                                     

                             
                                                                             
                                                                                
                                                              







                                      
                                                                             
                                                                                
                                                              

                             
                                                                            
                                                                       
                                                        




                                  
                                                                            
                                                                        
                                                        






                                                                                  
                                       


                            
                                  

                                                                                  
                                        














                                                                                                    







                                                                      



                                                                          
                                                                                        



                                                           
                                                                                     







                                                     



                                                        
                                                                    







                                                                        



                                                                     
                    
                                                 





                                                                     
                                                                                  
                                  
                                                                                            

                                                               
                                                     









                                                                                            
 







                                                                                                 


                                                                                                              
                  

                                         





                                                 









                                                                             
                            



                                                                                                            













                                                                  
                                                                       



























                                                                                                          
                
                                                                                     



                                                             
                        




                                                                        

                                                    

                         





                                                                











                                               
                                     
                                           
                                                    
             
                                                    
 
                                                     



                                       


                                                                                                    
             
 
                                                            
                                                                    

                        
                                     

                                                                                  
                                                      
                                      
                                                                                                             
                                                     
                                                  



                                                                                                      

                                                                                          




                                                                              
                                         
                 
             
                                     






                                                                                  

                                     





                                                                              
                                    
                 
 
                                                                                                  

                





                                                                    










                                                                          

                
                                                   


                                                  
                             


                                            
                                                
                                                 



                                                          
 




                                                  






                                                  








                                         
 
            
                                                                    



                                          
                            
                          
                               









                                                   





                                       
                                                                                             
 
                               


                                                                         
                 



                                                                                  
                                                                                                             
                                    





                                                                                                         
 
                         

                                                  
                                                               


                                          


                                                   
                                              
                                                                                        
                                                 

                                                            
                                          






                                                               
                       


                          
import ccxt
import time
from decimal import Decimal as D
# Put your poloniex api key in market.py
from market import market

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]
        cls.last_date = sorted(liquidities.keys())[-1]
        return liquidities[cls.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,
                    parse_int=D,
                    parse_float=D)
        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:
    def __init__(self, currency, value, linked_to=None, ticker=None, rate=None):
        self.currency = currency
        self.value = D(value)
        self.linked_to = linked_to
        self.ticker = ticker
        self.rate = rate

        self.ticker_cache = {}
        self.ticker_cache_timestamp = time.time()

    def in_currency(self, other_currency, market, rate=None, action=None, compute_value="average"):
        if other_currency == self.currency:
            return self
        if rate is not None:
            return Amount(
                    other_currency,
                    self.value * rate,
                    linked_to=self,
                    rate=rate)
        asset_ticker = Trade.get_ticker(self.currency, other_currency, market)
        if asset_ticker is not None:
            rate = Trade.compute_value(asset_ticker, action, compute_value=compute_value)
            return Amount(
                    other_currency,
                    self.value * rate,
                    linked_to=self,
                    ticker=asset_ticker,
                    rate=rate)
        else:
            raise Exception("This asset is not available in the chosen market")

    def __abs__(self):
        return Amount(self.currency, 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, 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, self.value - other.value)

    def __mul__(self, value):
        if type(value) != int and type(value) != float and type(value) != D:
            raise TypeError("Amount may only be multiplied by numbers")
        return Amount(self.currency, self.value * value)

    def __rmul__(self, value):
        return self.__mul__(value)

    def __floordiv__(self, value):
        if type(value) != int and type(value) != float and type(value) != D:
            raise TypeError("Amount may only be multiplied by integers")
        return Amount(self.currency, 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 = {}

    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, compute_value="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, compute_value=compute_value)
            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 or key in cls.known_balances:
                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=None):
        if repartition is None:
            repartition = Portfolio.repartition_pertenthousand()
        sum_pertenthousand = sum([v for k, v in repartition.items()])
        amounts = {}
        for currency, ptt in repartition.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", compute_value="average"):
        cls.fetch_balances(market)
        values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
        total_base_value = sum(values_in_base.values())
        new_repartition = cls.dispatch_assets(total_base_value)
        # Recompute it in case we have new currencies
        values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
        Trade.compute_trades(values_in_base, new_repartition, market=market)

    @classmethod
    def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None):
        cls.fetch_balances(market)
        values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
        total_base_value = sum(values_in_base.values())
        new_repartition = cls.dispatch_assets(total_base_value)
        Trade.compute_trades(values_in_base, new_repartition, only=only, market=market)

    @classmethod
    def prepare_trades_to_sell_all(cls, market, base_currency="BTC", compute_value="average"):
        cls.fetch_balances(market)
        values_in_base = cls.in_currency(base_currency, market, compute_value=compute_value)
        total_base_value = sum(values_in_base.values())
        new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: 1 })
        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 Computation:
    computations = {
            "default": lambda x, y: x[y],
            "average": lambda x, y: x["average"],
            "bid": lambda x, y: x["bid"],
            "ask": lambda x, y: x["ask"],
            }


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 = []
        self.market = market
        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

    fees_cache = {}
    @classmethod
    def fetch_fees(cls, market):
        if market.__class__ not in cls.fees_cache:
            cls.fees_cache[market.__class__] = market.fetch_fees()
        return cls.fees_cache[market.__class__]

    ticker_cache = {}
    ticker_cache_timestamp = time.time()
    @classmethod
    def get_ticker(cls, c1, c2, market, refresh=False):
        def invert(ticker):
            return {
                    "inverted": True,
                    "average": (1/ticker["bid"] + 1/ticker["ask"]) / 2,
                    "original": ticker,
                    }
        def augment_ticker(ticker):
            ticker.update({
                "inverted": False,
                "average": (ticker["bid"] + ticker["ask"] ) / 2,
                })

        if time.time() - cls.ticker_cache_timestamp > 5:
            cls.ticker_cache = {}
            cls.ticker_cache_timestamp = time.time()
        elif not refresh:
            if (c1, c2, market.__class__) in cls.ticker_cache:
                return cls.ticker_cache[(c1, c2, market.__class__)]
            if (c2, c1, market.__class__) in cls.ticker_cache:
                return invert(cls.ticker_cache[(c2, c1, market.__class__)])

        try:
            cls.ticker_cache[(c1, c2, market.__class__)] = market.fetch_ticker("{}/{}".format(c1, c2))
            augment_ticker(cls.ticker_cache[(c1, c2, market.__class__)])
        except ccxt.ExchangeError:
            try:
                cls.ticker_cache[(c2, c1, market.__class__)] = market.fetch_ticker("{}/{}".format(c2, c1))
                augment_ticker(cls.ticker_cache[(c2, c1, market.__class__)])
            except ccxt.ExchangeError:
                cls.ticker_cache[(c1, c2, market.__class__)] = None
        return cls.get_ticker(c1, c2, market)

    @classmethod
    def compute_trades(cls, values_in_base, new_repartition, only=None, market=None):
        base_currency = sum(values_in_base.values()).currency
        for currency in Balance.currencies():
            if currency == base_currency:
                continue
            trade = cls(
                values_in_base.get(currency, Amount(base_currency, 0)), 
                new_repartition.get(currency, Amount(base_currency, 0)),
                currency,
                market=market
                )
            if only is None or trade.action == only:
                cls.trades[currency] = trade
        return cls.trades

    @classmethod
    def prepare_orders(cls, only=None, compute_value="default"):
        for currency, trade in cls.trades.items():
            if only is None or trade.action == only:
                trade.prepare_order(compute_value=compute_value)

    @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 order_action(self, inverted):
        if self.value_from < self.value_to:
            return "buy" if not inverted else "sell"
        else:
            return "sell" if not inverted else "buy"

    def prepare_order(self, compute_value="default"):
        if self.action is None:
            return
        ticker = self.value_from.ticker
        inverted = ticker["inverted"]
        if inverted:
            ticker = ticker["original"]
        rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value)
        # 0.1

        delta_in_base = abs(self.value_from - self.value_to)
        # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)

        if not inverted:
            if self.action == "sell":
                # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
                # At rate 1 Foo = 0.1 BTC
                value_from = self.value_from.linked_to
                # value_from = 100 FOO
                value_to = self.value_to.in_currency(self.currency, self.market, rate=1/self.value_from.rate)
                # value_to   = 10 FOO (1 BTC * 1/0.1)
                delta = abs(value_to - value_from)
                # delta      = 90 FOO
                # Action: "sell" "90 FOO" at rate "0.1" "BTC" on "market"

                # Note: no rounding error possible: if we have value_to == 0, then delta == value_from
            else:
                delta = delta_in_base.in_currency(self.currency, self.market, rate=1/rate)
                # I want to buy 9 / 0.1 FOO
                # Action: "buy" "90 FOO" at rate "0.1" "BTC" on "market"

                # FIXME: Need to round up to the correct amount of FOO in case
                # we want to use all BTC
            currency = self.base_currency
            # BTC
        else:
            if self.action == "sell":
                # I have 10 BTC worth of FOO, and I want to sell 9 BTC worth of it
                # At rate 1 Foo = 0.1 BTC
                delta = delta_in_base
                # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market

                # FIXME: Need to round up to the correct amount of FOO in case
                # we want to sell all
            else:
                delta = delta_in_base
                # I want to buy 9 / 0.1 FOO
                # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market"

                # FIXME: Need to round up to the correct amount of FOO in case
                # we want to use all BTC

            currency = self.currency
            # FOO

        self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.market))

    @classmethod
    def compute_value(cls, ticker, action, compute_value="default"):
        if type(compute_value) == str:
            compute_value = Computation.computations[compute_value]
        return compute_value(ticker, action)

    @classmethod
    def all_orders(cls, state=None):
        all_orders = sum(map(lambda v: v.orders, cls.trades.values()), [])
        if state is None:
            return all_orders
        else:
            return list(filter(lambda o: o.status == state, all_orders))

    @classmethod
    def run_orders(cls):
        for order in cls.all_orders(state="pending"):
            order.run()

    @classmethod
    def follow_orders(cls, verbose=True, sleep=30):
        orders = cls.all_orders()
        finished_orders = []
        while len(orders) != len(finished_orders):
            time.sleep(sleep)
            for order in orders:
                if order in finished_orders:
                    continue
                if order.get_status() != "open":
                    finished_orders.append(order)
                    if verbose:
                        print("finished {}".format(order))
        if verbose:
            print("All orders finished")

    @classmethod
    def update_all_orders_status(cls):
        for order in cls.all_orders(state="open"):
            order.get_status()

    def __repr__(self):
        return "Trade({} -> {} in {}, {})".format(
                self.value_from,
                self.value_to,
                self.currency,
                self.action)

    @classmethod
    def print_all_with_order(cls):
        for trade in cls.trades.values():
            trade.print_with_order()

    def print_with_order(self):
        print(self)
        for order in self.orders:
            print("\t", order, sep="")

class Order:
    def __init__(self, action, amount, rate, base_currency, market):
        self.action = action
        self.amount = amount
        self.rate = rate
        self.base_currency = base_currency
        self.market = market
        self.result = None
        self.status = "pending"

    def __repr__(self):
        return "Order({} {} at {} {} [{}])".format(
                self.action,
                self.amount,
                self.rate,
                self.base_currency,
                self.status
                )

    @property
    def pending(self):
        return self.status == "pending"

    @property
    def finished(self):
        return self.status == "closed" or self.status == "canceled" or self.status == "error"

    def run(self, debug=False):
        symbol = "{}/{}".format(self.amount.currency, self.base_currency)
        amount = self.amount.value

        if debug:
            print("market.create_order('{}', 'limit', '{}', {}, price={})".format(
                symbol, self.action, amount, self.rate))
        else:
            try:
                self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate)
                self.status = "open"
            except Exception as e:
                self.status = "error"
                print("error when running market.create_order('{}', 'limit', '{}', {}, price={})".format(
                    symbol, self.action, amount, self.rate))
                self.error_message = str("{}: {}".format(e.__class__.__name__, e))
                print(self.error_message)

    def get_status(self):
        # other states are "closed" and "canceled"
        if self.status == "open":
            result = self.market.fetch_order(self.result['id'])
            self.status = result["status"]
        return self.status

    def cancel(self):
        self.market.cancel_order(self.result['id'])

def print_orders(market, base_currency="BTC"):
    Balance.prepare_trades(market, base_currency=base_currency, compute_value="average")
    Trade.prepare_orders(compute_value="average")
    for currency, balance in Balance.known_balances.items():
        print(balance)
    portfolio.Trade.print_all_with_order()

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

if __name__ == '__main__':
    print_orders(market)