From b47d7b54cca8ff142eaadf38c8bb425bf11af2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Sun, 25 Mar 2018 23:57:39 +0200 Subject: Handle invalid nonces --- ccxt_wrapper.py | 2 +- market.py | 6 +++--- portfolio.py | 12 ++++++++++-- test.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 64 insertions(+), 12 deletions(-) diff --git a/ccxt_wrapper.py b/ccxt_wrapper.py index 4ed37d9..97f359f 100644 --- a/ccxt_wrapper.py +++ b/ccxt_wrapper.py @@ -33,7 +33,7 @@ class poloniexE(poloniex): retriable = any(re.match(call, path) for call in self.RETRIABLE_CALLS) if api == "public" or method == "GET" or retriable: return retry_call(origin_request, fargs=[path], fkwargs=kwargs, - tries=10, delay=1, exceptions=(RequestTimeout,)) + tries=10, delay=1, exceptions=(RequestTimeout, InvalidNonce)) else: return origin_request(path, **kwargs) diff --git a/market.py b/market.py index ca65bca..d0e6ab4 100644 --- a/market.py +++ b/market.py @@ -1,4 +1,4 @@ -from ccxt import ExchangeError, NotSupported, RequestTimeout +from ccxt import ExchangeError, NotSupported, RequestTimeout, InvalidNonce import ccxt_wrapper as ccxt import time import psycopg2 @@ -89,7 +89,7 @@ class Market: finally: self.store_report() - @retry(RequestTimeout, tries=5) + @retry((RequestTimeout, InvalidNonce), tries=5) def move_balances(self): needed_in_margin = {} moving_to_margin = {} @@ -114,7 +114,7 @@ class Market: self.ccxt.transfer_balance(currency, delta, "exchange", "margin") elif delta < 0: self.ccxt.transfer_balance(currency, -delta, "margin", "exchange") - except RequestTimeout as e: + except (RequestTimeout, InvalidNonce) as e: self.report.log_error(action, message="Retrying", exception=e) self.report.log_move_balances(needed_in_margin, moving_to_margin) self.balances.fetch_balances() diff --git a/portfolio.py b/portfolio.py index 9c58676..9dae23e 100644 --- a/portfolio.py +++ b/portfolio.py @@ -1,7 +1,7 @@ from 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: computations = { @@ -477,7 +477,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) @@ -496,6 +496,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: diff --git a/test.py b/test.py index 5b9e2b4..c229801 100644 --- a/test.py +++ b/test.py @@ -101,7 +101,7 @@ class poloniexETest(unittest.TestCase): retry_call.assert_called_with(request, delay=1, tries=10, fargs=["foo"], fkwargs={'api': 'public', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, - exceptions=(market.ccxt.RequestTimeout,)) + exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce)) request.assert_not_called() with self.subTest(desc="private GET"): @@ -109,7 +109,7 @@ class poloniexETest(unittest.TestCase): retry_call.assert_called_with(request, delay=1, tries=10, fargs=["foo"], fkwargs={'api': 'private', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, - exceptions=(market.ccxt.RequestTimeout,)) + exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce)) request.assert_not_called() with self.subTest(desc="private POST regexp"): @@ -117,7 +117,7 @@ class poloniexETest(unittest.TestCase): retry_call.assert_called_with(request, delay=1, tries=10, fargs=["returnFoo"], fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, - exceptions=(market.ccxt.RequestTimeout,)) + exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce)) request.assert_not_called() with self.subTest(desc="private POST non-regexp"): @@ -125,7 +125,7 @@ class poloniexETest(unittest.TestCase): retry_call.assert_called_with(request, delay=1, tries=10, fargs=["getMarginPosition"], fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, - exceptions=(market.ccxt.RequestTimeout,)) + exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce)) request.assert_not_called() retry_call.reset_mock() request.reset_mock() @@ -1461,16 +1461,18 @@ class MarketTest(WebMockTestCase): m.ccxt.transfer_balance.side_effect = [ market.ccxt.RequestTimeout, + market.ccxt.InvalidNonce, True ] m.move_balances() self.ccxt.transfer_balance.assert_has_calls([ + mock.call("BTC", 3, "exchange", "margin"), mock.call("BTC", 3, "exchange", "margin"), mock.call("BTC", 3, "exchange", "margin") ]) - self.assertEqual(2, fetch_balances.call_count) + self.assertEqual(3, fetch_balances.call_count) m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY) - self.assertEqual(2, m.report.log_move_balances.call_count) + self.assertEqual(3, m.report.log_move_balances.call_count) self.ccxt.transfer_balance.reset_mock() m.report.reset_mock() @@ -3213,6 +3215,48 @@ class OrderTest(WebMockTestCase): 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) + self.m.reset_mock() + with self.subTest(invalid_nonce=True): + with self.subTest(retry_success=True): + 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.InvalidNonce, + portfolio.InvalidNonce, + { "id": 123 }, + ] + 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.0010'), account='exchange', price=D('0.1')), + mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), + ]) + self.assertEqual(3, self.m.ccxt.create_order.call_count) + self.assertEqual(3, order.tries) + self.m.report.log_error.assert_called() + self.assertEqual(2, self.m.report.log_error.call_count) + self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after invalid nonce", exception=mock.ANY) + self.assertEqual(123, order.id) + + self.m.reset_mock() + with self.subTest(retry_success=False): + 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.InvalidNonce, + portfolio.InvalidNonce, + portfolio.InvalidNonce, + portfolio.InvalidNonce, + portfolio.InvalidNonce, + ] + order.run() + self.assertEqual(5, self.m.ccxt.create_order.call_count) + 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.00100000 ETH at 0.1 BTC [pending]) after invalid nonce", exception=mock.ANY) + self.assertEqual("error", order.status) + self.m.reset_mock() with self.subTest(request_timeout=True): order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), -- cgit v1.2.3