diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-24 15:18:31 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-03-24 16:05:26 +0100 |
commit | c7c1e0b26821fdd5622f81fb456f1028d4c9ab09 (patch) | |
tree | f3115a444aa3472a98678193e09b21f3c3455332 | |
parent | 472787b6360221588423d03fe3e73d92c09a7c9d (diff) | |
download | Trader-c7c1e0b26821fdd5622f81fb456f1028d4c9ab09.tar.gz Trader-c7c1e0b26821fdd5622f81fb456f1028d4c9ab09.tar.zst Trader-c7c1e0b26821fdd5622f81fb456f1028d4c9ab09.zip |
Add retry facility for api call timeouts
Fixes https://git.immae.eu/mantisbt/view.php?id=40
-rw-r--r-- | ccxt_wrapper.py | 30 | ||||
-rw-r--r-- | test.py | 52 |
2 files changed, 82 insertions, 0 deletions
diff --git a/ccxt_wrapper.py b/ccxt_wrapper.py index d37c306..c500659 100644 --- a/ccxt_wrapper.py +++ b/ccxt_wrapper.py | |||
@@ -1,12 +1,42 @@ | |||
1 | from ccxt import * | 1 | from ccxt import * |
2 | import decimal | 2 | import decimal |
3 | import time | 3 | import time |
4 | from retry.api import retry_call | ||
5 | import re | ||
4 | 6 | ||
5 | def _cw_exchange_sum(self, *args): | 7 | def _cw_exchange_sum(self, *args): |
6 | return sum([arg for arg in args if isinstance(arg, (float, int, decimal.Decimal))]) | 8 | return sum([arg for arg in args if isinstance(arg, (float, int, decimal.Decimal))]) |
7 | Exchange.sum = _cw_exchange_sum | 9 | Exchange.sum = _cw_exchange_sum |
8 | 10 | ||
9 | class poloniexE(poloniex): | 11 | class poloniexE(poloniex): |
12 | RETRIABLE_CALLS = [ | ||
13 | re.compile(r"^return"), | ||
14 | re.compile(r"^cancel"), | ||
15 | re.compile(r"^closeMarginPosition$"), | ||
16 | re.compile(r"^getMarginPosition$"), | ||
17 | ] | ||
18 | |||
19 | def request(self, path, api='public', method='GET', params={}, headers=None, body=None): | ||
20 | """ | ||
21 | Wrapped to allow retry of non-posting requests" | ||
22 | """ | ||
23 | |||
24 | origin_request = super(poloniexE, self).request | ||
25 | kwargs = { | ||
26 | "api": api, | ||
27 | "method": method, | ||
28 | "params": params, | ||
29 | "headers": headers, | ||
30 | "body": body | ||
31 | } | ||
32 | |||
33 | retriable = any(re.match(call, path) for call in self.RETRIABLE_CALLS) | ||
34 | if api == "public" or method == "GET" or retriable: | ||
35 | return retry_call(origin_request, fargs=[path], fkwargs=kwargs, | ||
36 | tries=10, delay=1, exceptions=(RequestTimeout,)) | ||
37 | else: | ||
38 | return origin_request(path, **kwargs) | ||
39 | |||
10 | @staticmethod | 40 | @staticmethod |
11 | def nanoseconds(): | 41 | def nanoseconds(): |
12 | return int(time.time() * 1000000000) | 42 | return int(time.time() * 1000000000) |
@@ -80,6 +80,58 @@ class poloniexETest(unittest.TestCase): | |||
80 | time.return_value = 123456.7890123456 | 80 | time.return_value = 123456.7890123456 |
81 | self.assertEqual(123456789012345, self.s.nonce()) | 81 | self.assertEqual(123456789012345, self.s.nonce()) |
82 | 82 | ||
83 | def test_request(self): | ||
84 | with mock.patch.object(market.ccxt.poloniex, "request") as request,\ | ||
85 | mock.patch("market.ccxt.retry_call") as retry_call: | ||
86 | with self.subTest(wrapped=True): | ||
87 | with self.subTest(desc="public"): | ||
88 | self.s.request("foo") | ||
89 | retry_call.assert_called_with(request, | ||
90 | delay=1, tries=10, fargs=["foo"], | ||
91 | fkwargs={'api': 'public', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, | ||
92 | exceptions=(market.ccxt.RequestTimeout,)) | ||
93 | request.assert_not_called() | ||
94 | |||
95 | with self.subTest(desc="private GET"): | ||
96 | self.s.request("foo", api="private") | ||
97 | retry_call.assert_called_with(request, | ||
98 | delay=1, tries=10, fargs=["foo"], | ||
99 | fkwargs={'api': 'private', 'method': 'GET', 'params': {}, 'headers': None, 'body': None}, | ||
100 | exceptions=(market.ccxt.RequestTimeout,)) | ||
101 | request.assert_not_called() | ||
102 | |||
103 | with self.subTest(desc="private POST regexp"): | ||
104 | self.s.request("returnFoo", api="private", method="POST") | ||
105 | retry_call.assert_called_with(request, | ||
106 | delay=1, tries=10, fargs=["returnFoo"], | ||
107 | fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, | ||
108 | exceptions=(market.ccxt.RequestTimeout,)) | ||
109 | request.assert_not_called() | ||
110 | |||
111 | with self.subTest(desc="private POST non-regexp"): | ||
112 | self.s.request("getMarginPosition", api="private", method="POST") | ||
113 | retry_call.assert_called_with(request, | ||
114 | delay=1, tries=10, fargs=["getMarginPosition"], | ||
115 | fkwargs={'api': 'private', 'method': 'POST', 'params': {}, 'headers': None, 'body': None}, | ||
116 | exceptions=(market.ccxt.RequestTimeout,)) | ||
117 | request.assert_not_called() | ||
118 | retry_call.reset_mock() | ||
119 | request.reset_mock() | ||
120 | with self.subTest(wrapped=False): | ||
121 | with self.subTest(desc="private POST non-matching regexp"): | ||
122 | self.s.request("marginBuy", api="private", method="POST") | ||
123 | request.assert_called_with("marginBuy", | ||
124 | api="private", method="POST", params={}, | ||
125 | headers=None, body=None) | ||
126 | retry_call.assert_not_called() | ||
127 | |||
128 | with self.subTest(desc="private POST non-matching non-regexp"): | ||
129 | self.s.request("closeMarginPositionOther", api="private", method="POST") | ||
130 | request.assert_called_with("closeMarginPositionOther", | ||
131 | api="private", method="POST", params={}, | ||
132 | headers=None, body=None) | ||
133 | retry_call.assert_not_called() | ||
134 | |||
83 | def test_order_precision(self): | 135 | def test_order_precision(self): |
84 | self.assertEqual(8, self.s.order_precision("FOO")) | 136 | self.assertEqual(8, self.s.order_precision("FOO")) |
85 | 137 | ||