]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/Trader.git/commitdiff
Retry running order when available balance is insufficient
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Mon, 26 Feb 2018 13:18:34 +0000 (14:18 +0100)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Mon, 26 Feb 2018 13:18:34 +0000 (14:18 +0100)
helper.py
portfolio.py
requirements.txt
test.py

index 6d28c3fc301e2196bb63f2a677619458442a5dcc..4d73078dbb99aed90f819c0ac9947f04a8f9d080 100644 (file)
--- a/helper.py
+++ b/helper.py
@@ -98,6 +98,7 @@ def print_orders(market, base_currency="BTC"):
     market.trades.prepare_orders(compute_value="average")
 
 def print_balances(market, base_currency="BTC"):
+    market.report.log_stage("print_balances")
     market.balances.fetch_balances()
     if base_currency is not None:
         market.report.print_log("total:")
index 0797de0a0489542ce02c20863625d33a2bb3eab8..6763fc67a6b56d759deb972cbd75795a424057ce 100644 (file)
@@ -3,7 +3,8 @@ 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, ExchangeNotAvailable, InvalidOrder
+from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder
+from retry import retry
 import requests
 
 # FIXME: correctly handle web call timeouts
@@ -476,6 +477,7 @@ class Order:
         self.close_if_possible = close_if_possible
         self.id = None
         self.fetch_cache_timestamp = None
+        self.tries = 0
 
     def as_json(self):
         return {
@@ -521,7 +523,9 @@ class Order:
     def finished(self):
         return self.status == "closed" or self.status == "canceled" or self.status == "error"
 
+    @retry(InsufficientFunds)
     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
 
@@ -530,16 +534,25 @@ class Order:
                 symbol, self.action, amount, self.rate, self.account))
             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)
             try:
                 self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
-            except (ExchangeNotAvailable, InvalidOrder):
+            except InvalidOrder:
                 # Impossible to honor the order (dust amount)
                 self.status = "closed"
                 self.mark_finished_order()
                 return
+            except InsufficientFunds as e:
+                if self.tries < 5:
+                    self.market.report.log_error(action, message="Retrying with reduced amount", exception=e)
+                    self.amount = self.amount * D("0.99")
+                    raise e
+                else:
+                    self.market.report.log_error(action, message="Giving up {}".format(self), exception=e)
+                    self.status = "error"
+                    return
             except Exception as e:
                 self.status = "error"
-                action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
                 self.market.report.log_error(action, exception=e)
                 return
         self.id = self.results[0]["id"]
index 8e78c05f4d7cdf669345c025ea4c17db894cb6d7..5a25dbf86977b325e24ea83edebfe8e34337517a 100644 (file)
@@ -1,5 +1,6 @@
-ccxt>=1.10.593
-simplejson>=3.13.2
-requests>=2.11.1
-requests_mock>=1.4.0
-psycopg2>=2.7
+ccxt==1.10.1216
+simplejson==3.13.2
+requests>=2.18.4
+requests_mock==1.4.0
+psycopg2==2.7.4
+retry==0.9.2
diff --git a/test.py b/test.py
index 52d737d7528f773261242e0830146415ddb1f487..955e2a180caf92b2da8211a37c6502dfff58f291 100644 (file)
--- a/test.py
+++ b/test.py
@@ -2073,24 +2073,68 @@ class OrderTest(WebMockTestCase):
                 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
             order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001),
                     D("0.1"), "BTC", "long", self.m, "trade")
-            self.m.ccxt.create_order.side_effect = portfolio.ExchangeNotAvailable
+            self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder
             order.run()
             self.m.ccxt.create_order.assert_called_once()
             self.assertEqual(0, len(order.results))
             self.assertEqual("closed", order.status)
             mark_finished_order.assert_called_once()
 
+        self.m.ccxt.order_precision.return_value = 8
         self.m.ccxt.create_order.reset_mock()
-        with self.subTest(dust_amount_exception=True),\
+        with self.subTest(insufficient_funds=True),\
                 mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
-            order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001),
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                     D("0.1"), "BTC", "long", self.m, "trade")
-            self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder
+            self.m.ccxt.create_order.side_effect = [
+                    portfolio.InsufficientFunds,
+                    portfolio.InsufficientFunds,
+                    portfolio.InsufficientFunds,
+                    { "id": 123 },
+                    ]
             order.run()
-            self.m.ccxt.create_order.assert_called_once()
+            self.m.ccxt.create_order.assert_has_calls([
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
+                ])
+            self.assertEqual(4, self.m.ccxt.create_order.call_count)
+            self.assertEqual(1, len(order.results))
+            self.assertEqual("open", order.status)
+            self.assertEqual(4, order.tries)
+            self.m.report.log_error.assert_called()
+            self.assertEqual(4, self.m.report.log_error.call_count)
+
+        self.m.ccxt.order_precision.return_value = 8
+        self.m.ccxt.create_order.reset_mock()
+        self.m.report.log_error.reset_mock()
+        with self.subTest(insufficient_funds=True),\
+                mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
+            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+                    D("0.1"), "BTC", "long", self.m, "trade")
+            self.m.ccxt.create_order.side_effect = [
+                    portfolio.InsufficientFunds,
+                    portfolio.InsufficientFunds,
+                    portfolio.InsufficientFunds,
+                    portfolio.InsufficientFunds,
+                    portfolio.InsufficientFunds,
+                    ]
+            order.run()
+            self.m.ccxt.create_order.assert_has_calls([
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
+                mock.call('ETH/BTC', 'limit', 'buy', D('0.00096059'), account='exchange', price=D('0.1')),
+                ])
+            self.assertEqual(5, self.m.ccxt.create_order.call_count)
             self.assertEqual(0, len(order.results))
-            self.assertEqual("closed", order.status)
-            mark_finished_order.assert_called_once()
+            self.assertEqual("error", order.status)
+            self.assertEqual(5, order.tries)
+            self.m.report.log_error.assert_called()
+            self.assertEqual(5, self.m.report.log_error.call_count)
+            self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY)
 
 
 @unittest.skipUnless("unit" in limits, "Unit skipped")
@@ -2750,6 +2794,7 @@ class HelperTest(WebMockTestCase):
 
         helper.print_balances(self.m)
 
+        self.m.report.log_stage.assert_called_once_with("print_balances")
         self.m.balances.fetch_balances.assert_called_with()
         self.m.report.print_log.assert_has_calls([
             mock.call("total:"),