diff options
-rw-r--r-- | ccxt_wrapper.py | 2 | ||||
-rw-r--r-- | market.py | 6 | ||||
-rw-r--r-- | portfolio.py | 12 | ||||
-rw-r--r-- | 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): | |||
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 | ||
@@ -1,4 +1,4 @@ | |||
1 | from ccxt import ExchangeError, NotSupported, RequestTimeout | 1 | from ccxt import ExchangeError, NotSupported, RequestTimeout, InvalidNonce |
2 | import ccxt_wrapper as ccxt | 2 | import ccxt_wrapper as ccxt |
3 | import time | 3 | import time |
4 | import psycopg2 | 4 | import 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 @@ | |||
1 | from datetime import datetime | 1 | from datetime import datetime |
2 | from retry import retry | 2 | from retry import retry |
3 | from decimal import Decimal as D, ROUND_DOWN | 3 | from decimal import Decimal as D, ROUND_DOWN |
4 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout | 4 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder, OrderNotCached, OrderNotFound, RequestTimeout, InvalidNonce |
5 | 5 | ||
6 | class Computation: | 6 | class 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: |
@@ -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") |