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