aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-25 23:57:57 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-03-25 23:57:57 +0200
commite967a372218df05424b1566a02cafe2432d57326 (patch)
treee69f8f99ad47cfc3270192428fc2abee87eea65b
parentd004a2a5e15a78991870dcb90cd6db63ab40a4e6 (diff)
parentb47d7b54cca8ff142eaadf38c8bb425bf11af2cd (diff)
downloadTrader-e967a372218df05424b1566a02cafe2432d57326.tar.gz
Trader-e967a372218df05424b1566a02cafe2432d57326.tar.zst
Trader-e967a372218df05424b1566a02cafe2432d57326.zip
Merge branch 'invalid_nonce' into dev
Fixes https://git.immae.eu/mantisbt/view.php?id=59
-rw-r--r--ccxt_wrapper.py2
-rw-r--r--market.py6
-rw-r--r--portfolio.py12
-rw-r--r--test.py56
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):
33 retriable = any(re.match(call, path) for call in self.RETRIABLE_CALLS) 33 retriable = any(re.match(call, path) for call in self.RETRIABLE_CALLS)
34 if api == "public" or method == "GET" or retriable: 34 if api == "public" or method == "GET" or retriable:
35 return retry_call(origin_request, fargs=[path], fkwargs=kwargs, 35 return retry_call(origin_request, fargs=[path], fkwargs=kwargs,
36 tries=10, delay=1, exceptions=(RequestTimeout,)) 36 tries=10, delay=1, exceptions=(RequestTimeout, InvalidNonce))
37 else: 37 else:
38 return origin_request(path, **kwargs) 38 return origin_request(path, **kwargs)
39 39
diff --git a/market.py b/market.py
index ca65bca..d0e6ab4 100644
--- a/market.py
+++ b/market.py
@@ -1,4 +1,4 @@
1from ccxt import ExchangeError, NotSupported, RequestTimeout 1from ccxt import ExchangeError, NotSupported, RequestTimeout, InvalidNonce
2import ccxt_wrapper as ccxt 2import ccxt_wrapper as ccxt
3import time 3import time
4import psycopg2 4import psycopg2
@@ -89,7 +89,7 @@ class Market:
89 finally: 89 finally:
90 self.store_report() 90 self.store_report()
91 91
92 @retry(RequestTimeout, tries=5) 92 @retry((RequestTimeout, InvalidNonce), tries=5)
93 def move_balances(self): 93 def move_balances(self):
94 needed_in_margin = {} 94 needed_in_margin = {}
95 moving_to_margin = {} 95 moving_to_margin = {}
@@ -114,7 +114,7 @@ class Market:
114 self.ccxt.transfer_balance(currency, delta, "exchange", "margin") 114 self.ccxt.transfer_balance(currency, delta, "exchange", "margin")
115 elif delta < 0: 115 elif delta < 0:
116 self.ccxt.transfer_balance(currency, -delta, "margin", "exchange") 116 self.ccxt.transfer_balance(currency, -delta, "margin", "exchange")
117 except RequestTimeout as e: 117 except (RequestTimeout, InvalidNonce) as e:
118 self.report.log_error(action, message="Retrying", exception=e) 118 self.report.log_error(action, message="Retrying", exception=e)
119 self.report.log_move_balances(needed_in_margin, moving_to_margin) 119 self.report.log_move_balances(needed_in_margin, moving_to_margin)
120 self.balances.fetch_balances() 120 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 @@
1from datetime import datetime 1from datetime import datetime
2from retry import retry 2from retry import retry
3from decimal import Decimal as D, ROUND_DOWN 3from decimal import Decimal as D, ROUND_DOWN
4from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout 4from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout, InvalidNonce
5 5
6class Computation: 6class Computation:
7 computations = { 7 computations = {
@@ -477,7 +477,7 @@ class Order:
477 def finished(self): 477 def finished(self):
478 return self.status.startswith("closed") or self.status == "canceled" or self.status == "error" 478 return self.status.startswith("closed") or self.status == "canceled" or self.status == "error"
479 479
480 @retry((InsufficientFunds, RetryException)) 480 @retry((InsufficientFunds, RetryException, InvalidNonce))
481 def run(self): 481 def run(self):
482 self.tries += 1 482 self.tries += 1
483 symbol = "{}/{}".format(self.amount.currency, self.base_currency) 483 symbol = "{}/{}".format(self.amount.currency, self.base_currency)
@@ -496,6 +496,14 @@ class Order:
496 self.status = "closed" 496 self.status = "closed"
497 self.mark_finished_order() 497 self.mark_finished_order()
498 return 498 return
499 except InvalidNonce as e:
500 if self.tries < 5:
501 self.market.report.log_error(action, message="Retrying after invalid nonce", exception=e)
502 raise e
503 else:
504 self.market.report.log_error(action, message="Giving up {} after invalid nonce".format(self), exception=e)
505 self.status = "error"
506 return
499 except RequestTimeout as e: 507 except RequestTimeout as e:
500 if not self.retrieve_order(): 508 if not self.retrieve_order():
501 if self.tries < 5: 509 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):
101 retry_call.assert_called_with(request, 101 retry_call.assert_called_with(request,
102 delay=1, tries=10, fargs=["foo"], 102 delay=1, tries=10, fargs=["foo"],
103 fkwargs={'api': 'public', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, 103 fkwargs={'api': 'public', 'method': 'GET', 'params': {}, 'headers': None, 'body': None},
104 exceptions=(market.ccxt.RequestTimeout,)) 104 exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
105 request.assert_not_called() 105 request.assert_not_called()
106 106
107 with self.subTest(desc="private GET"): 107 with self.subTest(desc="private GET"):
@@ -109,7 +109,7 @@ class poloniexETest(unittest.TestCase):
109 retry_call.assert_called_with(request, 109 retry_call.assert_called_with(request,
110 delay=1, tries=10, fargs=["foo"], 110 delay=1, tries=10, fargs=["foo"],
111 fkwargs={'api': 'private', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, 111 fkwargs={'api': 'private', 'method': 'GET', 'params': {}, 'headers': None, 'body': None},
112 exceptions=(market.ccxt.RequestTimeout,)) 112 exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
113 request.assert_not_called() 113 request.assert_not_called()
114 114
115 with self.subTest(desc="private POST regexp"): 115 with self.subTest(desc="private POST regexp"):
@@ -117,7 +117,7 @@ class poloniexETest(unittest.TestCase):
117 retry_call.assert_called_with(request, 117 retry_call.assert_called_with(request,
118 delay=1, tries=10, fargs=["returnFoo"], 118 delay=1, tries=10, fargs=["returnFoo"],
119 fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, 119 fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None},
120 exceptions=(market.ccxt.RequestTimeout,)) 120 exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
121 request.assert_not_called() 121 request.assert_not_called()
122 122
123 with self.subTest(desc="private POST non-regexp"): 123 with self.subTest(desc="private POST non-regexp"):
@@ -125,7 +125,7 @@ class poloniexETest(unittest.TestCase):
125 retry_call.assert_called_with(request, 125 retry_call.assert_called_with(request,
126 delay=1, tries=10, fargs=["getMarginPosition"], 126 delay=1, tries=10, fargs=["getMarginPosition"],
127 fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, 127 fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None},
128 exceptions=(market.ccxt.RequestTimeout,)) 128 exceptions=(market.ccxt.RequestTimeout, market.ccxt.InvalidNonce))
129 request.assert_not_called() 129 request.assert_not_called()
130 retry_call.reset_mock() 130 retry_call.reset_mock()
131 request.reset_mock() 131 request.reset_mock()
@@ -1461,16 +1461,18 @@ class MarketTest(WebMockTestCase):
1461 1461
1462 m.ccxt.transfer_balance.side_effect = [ 1462 m.ccxt.transfer_balance.side_effect = [
1463 market.ccxt.RequestTimeout, 1463 market.ccxt.RequestTimeout,
1464 market.ccxt.InvalidNonce,
1464 True 1465 True
1465 ] 1466 ]
1466 m.move_balances() 1467 m.move_balances()
1467 self.ccxt.transfer_balance.assert_has_calls([ 1468 self.ccxt.transfer_balance.assert_has_calls([
1468 mock.call("BTC", 3, "exchange", "margin"), 1469 mock.call("BTC", 3, "exchange", "margin"),
1470 mock.call("BTC", 3, "exchange", "margin"),
1469 mock.call("BTC", 3, "exchange", "margin") 1471 mock.call("BTC", 3, "exchange", "margin")
1470 ]) 1472 ])
1471 self.assertEqual(2, fetch_balances.call_count) 1473 self.assertEqual(3, fetch_balances.call_count)
1472 m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY) 1474 m.report.log_error.assert_called_with(mock.ANY, message="Retrying", exception=mock.ANY)
1473 self.assertEqual(2, m.report.log_move_balances.call_count) 1475 self.assertEqual(3, m.report.log_move_balances.call_count)
1474 1476
1475 self.ccxt.transfer_balance.reset_mock() 1477 self.ccxt.transfer_balance.reset_mock()
1476 m.report.reset_mock() 1478 m.report.reset_mock()
@@ -3214,6 +3216,48 @@ class OrderTest(WebMockTestCase):
3214 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) 3216 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)
3215 3217
3216 self.m.reset_mock() 3218 self.m.reset_mock()
3219 with self.subTest(invalid_nonce=True):
3220 with self.subTest(retry_success=True):
3221 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
3222 D("0.1"), "BTC", "long", self.m, "trade")
3223 self.m.ccxt.create_order.side_effect = [
3224 portfolio.InvalidNonce,
3225 portfolio.InvalidNonce,
3226 { "id": 123 },
3227 ]
3228 order.run()
3229 self.m.ccxt.create_order.assert_has_calls([
3230 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
3231 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
3232 mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
3233 ])
3234 self.assertEqual(3, self.m.ccxt.create_order.call_count)
3235 self.assertEqual(3, order.tries)
3236 self.m.report.log_error.assert_called()
3237 self.assertEqual(2, self.m.report.log_error.call_count)
3238 self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after invalid nonce", exception=mock.ANY)
3239 self.assertEqual(123, order.id)
3240
3241 self.m.reset_mock()
3242 with self.subTest(retry_success=False):
3243 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
3244 D("0.1"), "BTC", "long", self.m, "trade")
3245 self.m.ccxt.create_order.side_effect = [
3246 portfolio.InvalidNonce,
3247 portfolio.InvalidNonce,
3248 portfolio.InvalidNonce,
3249 portfolio.InvalidNonce,
3250 portfolio.InvalidNonce,
3251 ]
3252 order.run()
3253 self.assertEqual(5, self.m.ccxt.create_order.call_count)
3254 self.assertEqual(5, order.tries)
3255 self.m.report.log_error.assert_called()
3256 self.assertEqual(5, self.m.report.log_error.call_count)
3257 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)
3258 self.assertEqual("error", order.status)
3259
3260 self.m.reset_mock()
3217 with self.subTest(request_timeout=True): 3261 with self.subTest(request_timeout=True):
3218 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), 3262 order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
3219 D("0.1"), "BTC", "long", self.m, "trade") 3263 D("0.1"), "BTC", "long", self.m, "trade")