X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=portfolio.py;h=d9d2d4dc3d2bd2d41235c5b8d650be0a0add44c4;hb=350ed24de673dc125be9e2fdecb0f1abc7835b41;hp=cb14c5d6223d44751f6f99d1f886e9a8aa5de7c2;hpb=c11e42744cb0355ea4c5bd2c99c7fee5fc5d647c;p=perso%2FImmae%2FProjets%2FCryptomonnaies%2FCryptoportfolio%2FTrader.git diff --git a/portfolio.py b/portfolio.py index cb14c5d..d9d2d4d 100644 --- a/portfolio.py +++ b/portfolio.py @@ -1,6 +1,6 @@ -import ccxt +from ccxt import ExchangeError import time -from decimal import Decimal as D +from decimal import Decimal as D, ROUND_DOWN # Put your poloniex api key in market.py from market import market @@ -10,7 +10,7 @@ class Portfolio: data = None @classmethod - def repartition_pertenthousand(cls, liquidity="medium"): + def repartition(cls, liquidity="medium"): cls.parse_cryptoportfolio() liquidities = cls.liquidities[liquidity] cls.last_date = sorted(liquidities.keys())[-1] @@ -26,7 +26,7 @@ class Portfolio: try: r = http.request("GET", cls.URL) except Exception: - return + return None try: cls.data = json.loads(r.data, parse_int=D, @@ -40,7 +40,7 @@ class Portfolio: cls.get_cryptoportfolio() def filter_weights(weight_hash): - if weight_hash[1] == 0: + if weight_hash[1][0] == 0: return False if weight_hash[0] == "_row": return False @@ -48,15 +48,13 @@ class Portfolio: def clean_weights(i): def clean_weights_(h): - if type(h[1][i]) == str: - return [h[0], h[1][i]] + if h[0].endswith("s"): + return [h[0][0:-1], (h[1][i], "short")] else: - return [h[0], int(h[1][i] * 10000)] + return [h[0], (h[1][i], "long")] 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"])): @@ -105,6 +103,9 @@ class Amount: else: raise Exception("This asset is not available in the chosen market") + def __round__(self, n=8): + return Amount(self.currency, self.value.quantize(D(1)/D(10**n), rounding=ROUND_DOWN)) + def __abs__(self): return Amount(self.currency, abs(self.value)) @@ -125,7 +126,7 @@ class Amount: return Amount(self.currency, self.value - other.value) def __mul__(self, value): - if type(value) != int and type(value) != float and type(value) != D: + if not isinstance(value, (int, float, D)): raise TypeError("Amount may only be multiplied by numbers") return Amount(self.currency, self.value * value) @@ -133,7 +134,7 @@ class Amount: return self.__mul__(value) def __floordiv__(self, value): - if type(value) != int and type(value) != float and type(value) != D: + if not isinstance(value, (int, float, D)): raise TypeError("Amount may only be multiplied by integers") return Amount(self.currency, self.value / value) @@ -196,35 +197,50 @@ class Balance: for key in hash_: if key in ["info", "free", "used", "total"]: continue - if hash_[key]["total"] > 0 or key in cls.known_balances: + 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 + # FIXME:Separate balances per trade type and in position + # Need to check how balances in position are represented + @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()]) + repartition = Portfolio.repartition() + sum_ratio = sum([v[0] for k, v in repartition.items()]) amounts = {} - for currency, ptt in repartition.items(): - amounts[currency] = ptt * amount / sum_pertenthousand + for currency, (ptt, trade_type) in repartition.items(): + amounts[currency] = ptt * amount / sum_ratio if currency not in cls.known_balances: cls.known_balances[currency] = cls(currency, 0, 0, 0) return amounts + @classmethod + def dispatch_trade_types(cls, repartition=None): + if repartition is None: + repartition = Portfolio.repartition() + trade_types = {} + for currency, (ptt, trade_type) in repartition.items(): + trade_types[currency] = trade_type + return trade_types + # FIXME: once we know the repartition and sold everything, we can move + # the necessary part to the margin account + @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) + trade_types = cls.dispatch_trade_types() # 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) + Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market) @classmethod def update_trades(cls, market, base_currency="BTC", compute_value="average", only=None): @@ -232,15 +248,17 @@ class Balance: 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) + trade_types = cls.dispatch_trade_types() + Trade.compute_trades(values_in_base, new_repartition, trade_types, 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) + new_repartition = cls.dispatch_assets(total_base_value, repartition={ base_currency: (1, "long") }) + trade_types = cls.dispatch_trade_types() + Trade.compute_trades(values_in_base, new_repartition, trade_types, market=market) def __repr__(self): return "Balance({} [{}/{}/{}])".format(self.currency, str(self.free), str(self.used), str(self.total)) @@ -253,16 +271,16 @@ class Computation: "ask": lambda x, y: x["ask"], } - class Trade: trades = {} - def __init__(self, value_from, value_to, currency, market=None): + def __init__(self, value_from, value_to, currency, trade_type, 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.trade_type = trade_type self.orders = [] self.market = market assert self.value_from.currency == self.value_to.currency @@ -304,16 +322,16 @@ class Trade: 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: + except 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: + except 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): + def compute_trades(cls, values_in_base, new_repartition, trade_types, only=None, market=None): base_currency = sum(values_in_base.values()).currency for currency in Balance.currencies(): if currency == base_currency: @@ -322,6 +340,7 @@ class Trade: values_in_base.get(currency, Amount(base_currency, 0)), new_repartition.get(currency, Amount(base_currency, 0)), currency, + trade_types.get(currency, "long"), market=market ) if only is None or trade.action == only: @@ -347,25 +366,30 @@ class Trade: return "sell" def order_action(self, inverted): - if self.value_from < self.value_to: - return "buy" if not inverted else "sell" + # a xor b xor c + if (self.trade_type == "short") != ((self.value_from < self.value_to) != inverted): + return "buy" else: - return "sell" if not inverted else "buy" + return "sell" def prepare_order(self, compute_value="default"): if self.action is None: return - ticker = self.value_from.ticker + ticker = Trade.get_ticker(self.currency, self.base_currency, self.market) inverted = ticker["inverted"] if inverted: ticker = ticker["original"] rate = Trade.compute_value(ticker, self.order_action(inverted), compute_value=compute_value) # 0.1 + # FIXME: optimize if value_to == 0 or value_from == 0?) + 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: + currency = self.base_currency + # BTC 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 @@ -382,36 +406,27 @@ class Trade: 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 + delta = delta_in_base + # 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 + # Action: "buy" "9 BTC" at rate "1/0.1" "FOO" on market + # buy: + # I want to buy 9 / 0.1 FOO + # Action: "sell" "9 BTC" at rate "1/0.1" "FOO" on "market" - self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.market)) + self.orders.append(Order(self.order_action(inverted), delta, rate, currency, self.trade_type, self.market)) @classmethod def compute_value(cls, ticker, action, compute_value="default"): - if type(compute_value) == str: + if action == "buy": + action = "ask" + if action == "sell": + action = "bid" + if isinstance(compute_value, str): compute_value = Computation.computations[compute_value] return compute_value(ticker, action) @@ -450,11 +465,12 @@ class Trade: order.get_status() def __repr__(self): - return "Trade({} -> {} in {}, {})".format( + return "Trade({} -> {} in {}, {} {})".format( self.value_from, self.value_to, self.currency, - self.action) + self.action, + self.trade_type) @classmethod def print_all_with_order(cls): @@ -467,24 +483,33 @@ class Trade: print("\t", order, sep="") class Order: - def __init__(self, action, amount, rate, base_currency, market): + def __init__(self, action, amount, rate, base_currency, trade_type, market): self.action = action self.amount = amount self.rate = rate self.base_currency = base_currency self.market = market + self.trade_type = trade_type self.result = None self.status = "pending" def __repr__(self): - return "Order({} {} at {} {} [{}])".format( + return "Order({} {} {} at {} {} [{}])".format( self.action, + self.trade_type, self.amount, self.rate, self.base_currency, self.status ) + @property + def account(self): + if self.trade_type == "long": + return "exchange" + else: + return "margin" + @property def pending(self): return self.status == "pending" @@ -495,19 +520,21 @@ class Order: def run(self, debug=False): symbol = "{}/{}".format(self.amount.currency, self.base_currency) - amount = self.amount.value + amount = round(self.amount, self.market.order_precision(symbol)).value if debug: - print("market.create_order('{}', 'limit', '{}', {}, price={})".format( - symbol, self.action, amount, self.rate)) + print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( + symbol, self.action, amount, self.rate, self.account)) else: try: - self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate) + if self.action == "sell" and self.trade_type == "short": + assert self.market.transfer_balance(self.base_currency, amount * self.rate, "exchange", "margin") + self.result = self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account) 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)) + print("error when running market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format( + symbol, self.action, amount, self.rate, self.account)) self.error_message = str("{}: {}".format(e.__class__.__name__, e)) print(self.error_message) @@ -526,7 +553,7 @@ def print_orders(market, base_currency="BTC"): Trade.prepare_orders(compute_value="average") for currency, balance in Balance.known_balances.items(): print(balance) - portfolio.Trade.print_all_with_order() + Trade.print_all_with_order() def make_orders(market, base_currency="BTC"): Balance.prepare_trades(market, base_currency=base_currency)