]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blobdiff - portfolio.py
Add method to wait for portfolio update
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
index b629966b3e057cd53e50220be244e686d5418ad9..b77f975e9cdf2d01cae09c07a72f3b8b15d16aa4 100644 (file)
@@ -1,7 +1,9 @@
 import time
+from datetime import datetime, timedelta
 from decimal import Decimal as D, ROUND_DOWN
 # Put your poloniex api key in market.py
 from json import JSONDecodeError
+from ccxt import ExchangeError, ExchangeNotAvailable
 import requests
 import helper as h
 from store import *
@@ -12,12 +14,19 @@ class Portfolio:
     URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
     liquidities = {}
     data = None
+    last_date = None
 
     @classmethod
-    def repartition(cls, liquidity="medium"):
-        cls.parse_cryptoportfolio()
+    def wait_for_recent(cls, delta=4):
+        cls.repartition(refetch=True)
+        while cls.last_date is None or datetime.now() - cls.last_date > timedelta(delta):
+            time.sleep(30)
+            cls.repartition(refetch=True)
+
+    @classmethod
+    def repartition(cls, liquidity="medium", refetch=False):
+        cls.parse_cryptoportfolio(refetch=refetch)
         liquidities = cls.liquidities[liquidity]
-        cls.last_date = sorted(liquidities.keys())[-1]
         return liquidities[cls.last_date]
 
     @classmethod
@@ -32,8 +41,8 @@ class Portfolio:
             cls.data = None
 
     @classmethod
-    def parse_cryptoportfolio(cls):
-        if cls.data is None:
+    def parse_cryptoportfolio(cls, refetch=False):
+        if refetch or cls.data is None:
             cls.get_cryptoportfolio()
 
         def filter_weights(weight_hash):
@@ -55,7 +64,8 @@ class Portfolio:
             weights_hash = portfolio_hash["weights"]
             weights = {}
             for i in range(len(weights_hash["_row"])):
-                weights[weights_hash["_row"][i]] = dict(filter(
+                date = datetime.strptime(weights_hash["_row"][i], "%Y-%m-%d")
+                weights[date] = dict(filter(
                         filter_weights,
                         map(clean_weights(i), weights_hash.items())))
             return weights
@@ -67,6 +77,7 @@ class Portfolio:
                 "medium": medium_liquidity,
                 "high":   high_liquidity,
                 }
+        cls.last_date = max(max(medium_liquidity.keys()), max(high_liquidity.keys()))
 
 class Computation:
     computations = {
@@ -122,6 +133,8 @@ class Amount:
         return Amount(self.currency, abs(self.value))
 
     def __add__(self, other):
+        if other == 0:
+            return self
         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)
@@ -139,6 +152,12 @@ class Amount:
             raise Exception("Summing amounts must be done with same currencies")
         return Amount(self.currency, self.value - other.value)
 
+    def __rsub__(self, other):
+        if other == 0:
+            return -self
+        else:
+            return -self.__sub__(other)
+
     def __mul__(self, value):
         if not isinstance(value, (int, float, D)):
             raise TypeError("Amount may only be multiplied by numbers")
@@ -272,7 +291,7 @@ class Trade:
         if self.base_currency == self.currency:
             return None
 
-        if self.value_from < self.value_to:
+        if abs(self.value_from) < abs(self.value_to):
             return "acquire"
         else:
             return "dispose"
@@ -301,19 +320,16 @@ class Trade:
         if tick in [0, 1, 3, 4, 6]:
             print("{}, tick {}, waiting".format(order, tick))
         elif tick == 2:
-            self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2)
-            new_order = self.orders[-1]
+            new_order = self.prepare_order(compute_value=lambda x, y: (x[y] + x["average"]) / 2)
             print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order))
         elif tick ==5:
-            self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3)
-            new_order = self.orders[-1]
+            new_order = self.prepare_order(compute_value=lambda x, y: (x[y]*2 + x["average"]) / 3)
             print("{}, tick {}, cancelling and adjusting to {}".format(order, tick, new_order))
         elif tick >= 7:
             if tick == 7:
                 print("{}, tick {}, fallbacking to market value".format(order, tick))
             if (tick - 7) % 3 == 0:
-                self.prepare_order(compute_value="default")
-                new_order = self.orders[-1]
+                new_order = self.prepare_order(compute_value="default")
                 print("{}, tick {}, market value, cancelling and adjusting to {}".format(order, tick, new_order))
 
         if new_order is not None:
@@ -322,7 +338,7 @@ class Trade:
 
     def prepare_order(self, compute_value="default"):
         if self.action is None:
-            return
+            return None
         ticker = h.get_ticker(self.currency, self.base_currency, self.market)
         inverted = ticker["inverted"]
         if inverted:
@@ -387,11 +403,13 @@ class Trade:
 
         if delta <= 0:
             print("Less to do than already filled: {}".format(delta))
-            return
+            return None
 
-        self.orders.append(Order(self.order_action(inverted),
+        order = Order(self.order_action(inverted),
             delta, rate, base_currency, self.trade_type,
-            self.market, self, close_if_possible=close_if_possible))
+            self.market, self, close_if_possible=close_if_possible)
+        self.orders.append(order)
+        return order
 
     def __repr__(self):
         return "Trade({} -> {} in {}, {})".format(
@@ -419,6 +437,8 @@ class Order:
         self.status = "pending"
         self.trade = trade
         self.close_if_possible = close_if_possible
+        self.id = None
+        self.fetch_cache_timestamp = None
 
     def __repr__(self):
         return "Order({} {} {} at {} {} [{}]{})".format(
@@ -438,6 +458,10 @@ class Order:
         else:
             return "margin"
 
+    @property
+    def open(self):
+        return self.status == "open"
+
     @property
     def pending(self):
         return self.status == "pending"
@@ -446,10 +470,6 @@ class Order:
     def finished(self):
         return self.status == "closed" or self.status == "canceled" or self.status == "error"
 
-    @property
-    def id(self):
-        return self.results[0]["id"]
-
     def run(self):
         symbol = "{}/{}".format(self.amount.currency, self.base_currency)
         amount = round(self.amount, self.market.order_precision(symbol)).value
@@ -457,26 +477,32 @@ class Order:
         if TradeStore.debug:
             print("market.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
                 symbol, self.action, amount, self.rate, self.account))
-            self.status = "open"
             self.results.append({"debug": True, "id": -1})
         else:
             try:
                 self.results.append(self.market.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
-                self.status = "open"
+            except ExchangeNotAvailable:
+                # Impossible to honor the order (dust amount)
+                self.status = "closed"
+                self.mark_finished_order()
+                return
             except Exception as e:
                 self.status = "error"
                 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)
+                return
+        self.id = self.results[0]["id"]
+        self.status = "open"
 
     def get_status(self):
         if TradeStore.debug:
             return self.status
         # other states are "closed" and "canceled"
-        if self.status == "open":
+        if not self.finished:
             self.fetch()
-            if self.status != "open":
+            if self.finished:
                 self.mark_finished_order()
         return self.status
 
@@ -487,15 +513,15 @@ class Order:
             if self.trade_type == "short" and self.action == "buy" and self.close_if_possible:
                 self.market.close_margin_position(self.amount.currency, self.base_currency)
 
-    fetch_cache_timestamp = None
     def fetch(self, force=False):
         if TradeStore.debug or (not force and self.fetch_cache_timestamp is not None
                 and time.time() - self.fetch_cache_timestamp < 10):
             return
         self.fetch_cache_timestamp = time.time()
 
-        self.results.append(self.market.fetch_order(self.id))
-        result = self.results[-1]
+        result = self.market.fetch_order(self.id)
+        self.results.append(result)
+
         self.status = result["status"]
         # Time at which the order started
         self.timestamp = result["datetime"]
@@ -503,11 +529,9 @@ class Order:
 
         # FIXME: consider open order with dust remaining as closed
 
-    @property
     def dust_amount_remaining(self):
-        return self.remaining_amount < 0.001
+        return self.remaining_amount() < Amount(self.amount.currency, D("0.001"))
 
-    @property
     def remaining_amount(self):
         if self.status == "open":
             self.fetch()
@@ -525,7 +549,10 @@ class Order:
         return filled_amount
 
     def fetch_mouvements(self):
-        mouvements = self.market.privatePostReturnOrderTrades({"orderNumber": self.id})
+        try:
+            mouvements = self.market.privatePostReturnOrderTrades({"orderNumber": self.id})
+        except ExchangeError:
+            mouvements = []
         self.mouvements = []
 
         for mouvement_hash in mouvements:
@@ -536,22 +563,25 @@ class Order:
         if TradeStore.debug:
             self.status = "canceled"
             return
-        self.market.cancel_order(self.result['id'])
+        self.market.cancel_order(self.id)
         self.fetch()
 
 class Mouvement:
     def __init__(self, currency, base_currency, hash_):
         self.currency = currency
         self.base_currency = base_currency
-        self.id = hash_["id"]
-        self.action = hash_["type"]
-        self.fee_rate = D(hash_["fee"])
-        self.date = datetime.strptime(hash_["date"], '%Y-%m-%d %H:%M:%S')
-        self.rate = D(hash_["rate"])
-        self.total = Amount(currency, hash_["amount"])
+        self.id = hash_.get("tradeID")
+        self.action = hash_.get("type")
+        self.fee_rate = D(hash_.get("fee", -1))
+        try:
+            self.date = datetime.strptime(hash_.get("date", ""), '%Y-%m-%d %H:%M:%S')
+        except ValueError:
+            self.date = None
+        self.rate = D(hash_.get("rate", 0))
+        self.total = Amount(currency, hash_.get("amount", 0))
         # rate * total = total_in_base
-        self.total_in_base = Amount(base_currency, hash_["total"])
+        self.total_in_base = Amount(base_currency, hash_.get("total", 0))
 
-if __name__ == '__main__':
+if __name__ == '__main__': # pragma: no cover
     from market import market
     h.print_orders(market)