]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/blobdiff - portfolio.py
Merge branch 'dev'
[perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git] / portfolio.py
index ed50b570ea2c31d310c6a3d271f37f7f413c43c0..535aaa843c22789dacc50d9eaf3a2371e6766297 100644 (file)
@@ -1,86 +1,7 @@
-import time
-from datetime import datetime, timedelta
-from decimal import Decimal as D, ROUND_DOWN
-from json import JSONDecodeError
-from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
-from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound
+from datetime import datetime
 from retry import retry
-import requests
-
-# FIXME: correctly handle web call timeouts
-
-class Portfolio:
-    URL = "https://cryptoportfolio.io/wp-content/uploads/portfolio/json/cryptoportfolio.json"
-    liquidities = {}
-    data = None
-    last_date = None
-
-    @classmethod
-    def wait_for_recent(cls, market, delta=4):
-        cls.repartition(market, refetch=True)
-        while cls.last_date is None or datetime.now() - cls.last_date > timedelta(delta):
-            time.sleep(30)
-            market.report.print_log("Attempt to fetch up-to-date cryptoportfolio")
-            cls.repartition(market, refetch=True)
-
-    @classmethod
-    def repartition(cls, market, liquidity="medium", refetch=False):
-        cls.parse_cryptoportfolio(market, refetch=refetch)
-        liquidities = cls.liquidities[liquidity]
-        return liquidities[cls.last_date]
-
-    @classmethod
-    def get_cryptoportfolio(cls, market):
-        try:
-            r = requests.get(cls.URL)
-            market.report.log_http_request(r.request.method,
-                    r.request.url, r.request.body, r.request.headers, r)
-        except Exception as e:
-            market.report.log_error("get_cryptoportfolio", exception=e)
-            return
-        try:
-            cls.data = r.json(parse_int=D, parse_float=D)
-        except (JSONDecodeError, SimpleJSONDecodeError):
-            cls.data = None
-
-    @classmethod
-    def parse_cryptoportfolio(cls, market, refetch=False):
-        if refetch or cls.data is None:
-            cls.get_cryptoportfolio(market)
-
-        def filter_weights(weight_hash):
-            if weight_hash[1][0] == 0:
-                return False
-            if weight_hash[0] == "_row":
-                return False
-            return True
-
-        def clean_weights(i):
-            def clean_weights_(h):
-                if h[0].endswith("s"):
-                    return [h[0][0:-1], (h[1][i], "short")]
-                else:
-                    return [h[0], (h[1][i], "long")]
-            return clean_weights_
-
-        def parse_weights(portfolio_hash):
-            weights_hash = portfolio_hash["weights"]
-            weights = {}
-            for i in range(len(weights_hash["_row"])):
-                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
-
-        high_liquidity = parse_weights(cls.data["portfolio_1"])
-        medium_liquidity = parse_weights(cls.data["portfolio_2"])
-
-        cls.liquidities = {
-                "medium": medium_liquidity,
-                "high":   high_liquidity,
-                }
-        cls.last_date = max(max(medium_liquidity.keys()), max(high_liquidity.keys()))
+from decimal import Decimal as D, ROUND_DOWN
+from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout, InvalidNonce
 
 class Computation:
     computations = {
@@ -348,20 +269,24 @@ class Trade:
             filled_amount += order.filled_amount(in_base_currency=in_base_currency)
         return filled_amount
 
-    def update_order(self, order, tick):
-        actions = {
-                0: ["waiting", None],
-                1: ["waiting", None],
-                2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
-                3: ["waiting", None],
-                4: ["waiting", None],
-                5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
-                6: ["waiting", None],
-                7: ["market_fallback", "default"],
-                }
+    tick_actions = {
+            0: ["waiting", None],
+            1: ["waiting", None],
+            2: ["adjusting", lambda x, y: (x[y] + x["average"]) / 2],
+            3: ["waiting", None],
+            4: ["waiting", None],
+            5: ["adjusting", lambda x, y: (x[y]*2 + x["average"]) / 3],
+            6: ["waiting", None],
+            7: ["market_fallback", "default"],
+            }
+
+    def tick_actions_recreate(self, tick, default="average"):
+        return ([default] + \
+                [ y[1] for x, y in self.tick_actions.items() if x <= tick and y[1] is not None ])[-1]
 
-        if tick in actions:
-            update, compute_value = actions[tick]
+    def update_order(self, order, tick):
+        if tick in self.tick_actions:
+            update, compute_value = self.tick_actions[tick]
         elif tick % 3 == 1:
             update = "market_adjust"
             compute_value = "default"
@@ -491,6 +416,9 @@ class Trade:
             for mouvement in order.mouvements:
                 self.market.report.print_log("{}\t\t{}".format(ind, mouvement))
 
+class RetryException(Exception):
+    pass
+
 class Order:
     def __init__(self, action, amount, rate, base_currency, trade_type, market,
             trade, close_if_possible=False):
@@ -507,6 +435,7 @@ class Order:
         self.close_if_possible = close_if_possible
         self.id = None
         self.tries = 0
+        self.start_date = None
 
     def as_json(self):
         return {
@@ -552,18 +481,18 @@ class Order:
     def finished(self):
         return self.status.startswith("closed") or self.status == "canceled" or self.status == "error"
 
-    @retry(InsufficientFunds)
+    @retry((InsufficientFunds, RetryException, InvalidNonce))
     def run(self):
         self.tries += 1
         symbol = "{}/{}".format(self.amount.currency, self.base_currency)
         amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
 
+        action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
         if self.market.debug:
-            self.market.report.log_debug_action("market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(
-                symbol, self.action, amount, self.rate, self.account))
+            self.market.report.log_debug_action(action)
             self.results.append({"debug": True, "id": -1})
         else:
-            action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
+            self.start_date = datetime.now()
             try:
                 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
             except InvalidOrder:
@@ -571,6 +500,27 @@ class Order:
                 self.status = "closed"
                 self.mark_finished_order()
                 return
+            except InvalidNonce as e:
+                if self.tries < 5:
+                    self.market.report.log_error(action, message="Retrying after invalid nonce", exception=e)
+                    raise e
+                else:
+                    self.market.report.log_error(action, message="Giving up {} after invalid nonce".format(self), exception=e)
+                    self.status = "error"
+                    return
+            except RequestTimeout as e:
+                if not self.retrieve_order():
+                    if self.tries < 5:
+                        self.market.report.log_error(action, message="Retrying after timeout", exception=e)
+                        # We make a specific call in case retrieve_order
+                        # would raise itself
+                        raise RetryException
+                    else:
+                        self.market.report.log_error(action, message="Giving up {} after timeouts".format(self), exception=e)
+                        self.status = "error"
+                        return
+                else:
+                    self.market.report.log_error(action, message="Timeout, found the order")
             except InsufficientFunds as e:
                 if self.tries < 5:
                     self.market.report.log_error(action, message="Retrying with reduced amount", exception=e)
@@ -596,6 +546,12 @@ class Order:
             self.fetch()
         return self.status
 
+    def mark_disappeared_order(self):
+        if self.status.startswith("closed") and \
+                len(self.mouvements) > 0 and \
+                self.mouvements[-1].total_in_base == 0:
+            self.status = "error_disappeared"
+
     def mark_finished_order(self):
         if self.status.startswith("closed") and self.market.debug:
             self.market.report.log_debug_action("Mark {} as finished".format(self))
@@ -619,6 +575,8 @@ class Order:
 
         self.fetch_mouvements()
 
+        self.mark_disappeared_order()
+
         self.mark_finished_order()
         # FIXME: consider open order with dust remaining as closed
 
@@ -649,6 +607,7 @@ class Order:
         for mouvement_hash in mouvements:
             self.mouvements.append(Mouvement(self.amount.currency,
                 self.base_currency, mouvement_hash))
+        self.mouvements.sort(key= lambda x: x.date)
 
     def cancel(self):
         if self.market.debug:
@@ -662,6 +621,54 @@ class Order:
                 self.market.report.log_error("cancel_order", message="Already cancelled order", exception=e)
             self.fetch()
 
+    def retrieve_order(self):
+        symbol = "{}/{}".format(self.amount.currency, self.base_currency)
+        amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
+        start_timestamp = self.start_date.timestamp() - 5
+
+        similar_open_orders = self.market.ccxt.fetch_orders(symbol=symbol, since=start_timestamp)
+        for order in similar_open_orders:
+            if (order["info"]["margin"] == 1 and self.account == "exchange") or\
+                    (order["info"]["margin"] != 1 and self.account == "margin"):
+                i_m_tested = True # coverage bug ?!
+                continue
+            if order["info"]["side"] != self.action:
+                continue
+            amount_diff = round(
+                    abs(D(order["info"]["startingAmount"]) - amount),
+                    self.market.ccxt.order_precision(symbol))
+            rate_diff = round(
+                    abs(D(order["info"]["rate"]) - self.rate),
+                    self.market.ccxt.order_precision(symbol))
+            if amount_diff != 0 or rate_diff != 0:
+                continue
+            self.results.append({"id": order["id"]})
+            return True
+
+        similar_trades = self.market.ccxt.fetch_my_trades(symbol=symbol, since=start_timestamp)
+        # FIXME: use set instead of sorted(list(...))
+        for order_id in sorted(list(map(lambda x: x["order"], similar_trades))):
+            trades = list(filter(lambda x: x["order"] == order_id, similar_trades))
+            if any(x["timestamp"] < start_timestamp for x in trades):
+                continue
+            if any(x["side"] != self.action for x in trades):
+                continue
+            if any(x["info"]["category"] == "exchange" and self.account == "margin" for x in trades) or\
+                    any(x["info"]["category"] == "marginTrade" and self.account == "exchange" for x in trades):
+                continue
+            trade_sum = sum(D(x["info"]["amount"]) for x in trades)
+            amount_diff = round(abs(trade_sum - amount),
+                    self.market.ccxt.order_precision(symbol))
+            if amount_diff != 0:
+                continue
+            if (self.action == "sell" and any(D(x["info"]["rate"]) < self.rate for x in trades)) or\
+                    (self.action == "buy" and any(D(x["info"]["rate"]) > self.rate for x in trades)):
+                continue
+            self.results.append({"id": order_id})
+            return True
+
+        return False
+
 class Mouvement:
     def __init__(self, currency, base_currency, hash_):
         self.currency = currency