From f70bb858007cd3be6766ee0aa4a3d9133952eb98 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Mon, 26 Feb 2018 14:18:34 +0100 Subject: [PATCH] Retry running order when available balance is insufficient --- helper.py | 1 + portfolio.py | 19 +++++++++++++--- requirements.txt | 11 +++++---- test.py | 59 ++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/helper.py b/helper.py index 6d28c3f..4d73078 100644 --- 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:") diff --git a/portfolio.py b/portfolio.py index 0797de0..6763fc6 100644 --- a/portfolio.py +++ b/portfolio.py @@ -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"] diff --git a/requirements.txt b/requirements.txt index 8e78c05..5a25dbf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 52d737d..955e2a1 100644 --- 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:"), -- 2.41.0