]> 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 9c586769575cf1ef33794856a161be40274fce01..1d291069bfa08c7cb497e90046de010e6d7f2e02 100644 (file)
@@ -1,9 +1,13 @@
-from datetime import datetime
+import datetime
 from retry import retry
 from decimal import Decimal as D, ROUND_DOWN
-from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout
+from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout, InvalidNonce
 
 class Computation:
+    @staticmethod
+    def eat_several(market):
+        return lambda x, y: market.ccxt.fetch_nth_order_book(x["symbol"], y, 15)
+
     computations = {
             "default": lambda x, y: x[y],
             "average": lambda x, y: x["average"],
@@ -48,7 +52,7 @@ class Amount:
                     ticker=asset_ticker,
                     rate=rate)
         else:
-            raise Exception("This asset is not available in the chosen market")
+            return Amount(other_currency, 0, linked_to=self, ticker=None, rate=0)
 
     def as_json(self):
         return {
@@ -261,31 +265,39 @@ class Trade:
 
     @property
     def is_fullfiled(self):
-        return abs(self.filled_amount(in_base_currency=(not self.inverted))) >= abs(self.delta)
+        return abs(self.filled_amount(in_base_currency=(not self.inverted), refetch=True)) >= abs(self.delta)
 
-    def filled_amount(self, in_base_currency=False):
+    def filled_amount(self, in_base_currency=False, refetch=False):
         filled_amount = 0
         for order in self.orders:
-            filled_amount += order.filled_amount(in_base_currency=in_base_currency)
+            filled_amount += order.filled_amount(in_base_currency=in_base_currency, refetch=refetch)
         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"],
+            }
 
-        if tick in actions:
-            update, compute_value = actions[tick]
+    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]
+
+    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"
+            if tick < 20:
+                update = "market_adjust"
+                compute_value = "default"
+            else:
+                update = "market_adjust_eat"
+                compute_value = Computation.eat_several(self.market)
         else:
             update = "waiting"
             compute_value = None
@@ -307,13 +319,15 @@ class Trade:
         if self.action is None:
             return None
         ticker = self.market.get_ticker(self.currency, self.base_currency)
+        if ticker is None:
+            self.market.report.log_error("prepare_order",
+                    message="Unknown ticker {}/{}".format(self.currency, self.base_currency))
+            return None
         self.inverted = ticker["inverted"]
         if self.inverted:
             ticker = ticker["original"]
         rate = Computation.compute_value(ticker, self.order_action(), compute_value=compute_value)
 
-        # FIXME: Dust amount should be removed from there if they werent
-        # honored in other sales
         delta_in_base = abs(self.delta)
         # 9 BTC's worth of move (10 - 1 or 1 - 10 depending on case)
 
@@ -477,7 +491,7 @@ class Order:
     def finished(self):
         return self.status.startswith("closed") or self.status == "canceled" or self.status == "error"
 
-    @retry((InsufficientFunds, RetryException))
+    @retry((InsufficientFunds, RetryException, InvalidNonce))
     def run(self):
         self.tries += 1
         symbol = "{}/{}".format(self.amount.currency, self.base_currency)
@@ -488,7 +502,7 @@ class Order:
             self.market.report.log_debug_action(action)
             self.results.append({"debug": True, "id": -1})
         else:
-            self.start_date = datetime.now()
+            self.start_date = datetime.datetime.now()
             try:
                 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
             except InvalidOrder:
@@ -496,6 +510,14 @@ 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:
@@ -534,6 +556,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))
@@ -557,17 +585,19 @@ class Order:
 
         self.fetch_mouvements()
 
+        self.mark_disappeared_order()
+        self.mark_dust_amount_remaining_order()
         self.mark_finished_order()
-        # FIXME: consider open order with dust remaining as closed
 
-    def dust_amount_remaining(self):
-        return self.remaining_amount() < Amount(self.amount.currency, D("0.001"))
+    def mark_dust_amount_remaining_order(self):
+        if self.status == "open" and self.market.ccxt.is_dust_trade(self.remaining_amount().value, self.rate):
+            self.status = "closed_dust_remaining"
 
-    def remaining_amount(self):
-        return self.amount - self.filled_amount()
+    def remaining_amount(self, refetch=False):
+        return self.amount - self.filled_amount(refetch=refetch)
 
-    def filled_amount(self, in_base_currency=False):
-        if self.status == "open":
+    def filled_amount(self, in_base_currency=False, refetch=False):
+        if refetch and self.status == "open":
             self.fetch()
         filled_amount = 0
         for mouvement in self.mouvements:
@@ -587,13 +617,14 @@ 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:
             self.market.report.log_debug_action("Mark {} as cancelled".format(self))
             self.status = "canceled"
             return
-        if self.open and self.id is not None:
+        if (self.status == "closed_dust_remaining" or self.open) and self.id is not None:
             try:
                 self.market.ccxt.cancel_order(self.id)
             except OrderNotFound as e: # Closed inbetween
@@ -625,7 +656,6 @@ class Order:
             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):
@@ -656,7 +686,7 @@ class Mouvement:
         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')
+            self.date = datetime.datetime.strptime(hash_.get("date", ""), '%Y-%m-%d %H:%M:%S')
         except ValueError:
             self.date = None
         self.rate = D(hash_.get("rate", 0))