diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-04-30 14:21:41 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-04-30 14:21:41 +0200 |
commit | 52ea19aa73348a523b3b884e2a7fb749b2bf4f19 (patch) | |
tree | 1325863772f475fa00953a34489ab94fbe7f828a | |
parent | fcb18fead0e92ddc075416e11934b62afa3e2ba3 (diff) | |
download | Trader-52ea19aa73348a523b3b884e2a7fb749b2bf4f19.tar.gz Trader-52ea19aa73348a523b3b884e2a7fb749b2bf4f19.tar.zst Trader-52ea19aa73348a523b3b884e2a7fb749b2bf4f19.zip |
Fix price imprecision due to floats
-rw-r--r-- | ccxt_wrapper.py | 75 | ||||
-rw-r--r-- | main.py | 2 | ||||
-rw-r--r-- | tests/test_ccxt_wrapper.py | 138 | ||||
-rw-r--r-- | tests/test_main.py | 4 |
4 files changed, 143 insertions, 76 deletions
diff --git a/ccxt_wrapper.py b/ccxt_wrapper.py index d2c9b4c..f30c7d2 100644 --- a/ccxt_wrapper.py +++ b/ccxt_wrapper.py | |||
@@ -232,39 +232,9 @@ class poloniexE(poloniex): | |||
232 | 232 | ||
233 | return all_balances | 233 | return all_balances |
234 | 234 | ||
235 | def create_exchange_order(self, symbol, type, side, amount, price=None, params={}): | ||
236 | return super().create_order(symbol, type, side, amount, price=price, params=params) | ||
237 | |||
238 | def create_margin_order(self, symbol, type, side, amount, price=None, lending_rate=None, params={}): | ||
239 | if type == 'market': | ||
240 | raise ExchangeError(self.id + ' allows limit orders only') | ||
241 | self.load_markets() | ||
242 | method = 'privatePostMargin' + self.capitalize(side) | ||
243 | market = self.market(symbol) | ||
244 | price = float(price) | ||
245 | amount = float(amount) | ||
246 | if lending_rate is not None: | ||
247 | params = self.extend({"lendingRate": lending_rate}, params) | ||
248 | response = getattr(self, method)(self.extend({ | ||
249 | 'currencyPair': market['id'], | ||
250 | 'rate': self.price_to_precision(symbol, price), | ||
251 | 'amount': self.amount_to_precision(symbol, amount), | ||
252 | }, params)) | ||
253 | timestamp = self.milliseconds() | ||
254 | order = self.parse_order(self.extend({ | ||
255 | 'timestamp': timestamp, | ||
256 | 'status': 'open', | ||
257 | 'type': type, | ||
258 | 'side': side, | ||
259 | 'price': price, | ||
260 | 'amount': amount, | ||
261 | }, response), market) | ||
262 | id = order['id'] | ||
263 | self.orders[id] = order | ||
264 | return self.extend({'info': response}, order) | ||
265 | |||
266 | def order_precision(self, symbol): | 235 | def order_precision(self, symbol): |
267 | return 8 | 236 | self.load_markets() |
237 | return self.markets[symbol]['precision']['price'] | ||
268 | 238 | ||
269 | def transfer_balance(self, currency, amount, from_account, to_account): | 239 | def transfer_balance(self, currency, amount, from_account, to_account): |
270 | result = self.privatePostTransferBalance({ | 240 | result = self.privatePostTransferBalance({ |
@@ -382,14 +352,49 @@ class poloniexE(poloniex): | |||
382 | 352 | ||
383 | def create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}): | 353 | def create_order(self, symbol, type, side, amount, price=None, account="exchange", lending_rate=None, params={}): |
384 | """ | 354 | """ |
385 | Wrapped to handle margin and exchange accounts | 355 | Wrapped to handle margin and exchange accounts, and get decimals |
386 | """ | 356 | """ |
357 | if type == 'market': | ||
358 | raise ExchangeError(self.id + ' allows limit orders only') | ||
359 | self.load_markets() | ||
387 | if account == "exchange": | 360 | if account == "exchange": |
388 | return self.create_exchange_order(symbol, type, side, amount, price=price, params=params) | 361 | method = 'privatePost' + self.capitalize(side) |
389 | elif account == "margin": | 362 | elif account == "margin": |
390 | return self.create_margin_order(symbol, type, side, amount, price=price, lending_rate=lending_rate, params=params) | 363 | method = 'privatePostMargin' + self.capitalize(side) |
364 | if lending_rate is not None: | ||
365 | params = self.extend({"lendingRate": lending_rate}, params) | ||
391 | else: | 366 | else: |
392 | raise NotImplementedError | 367 | raise NotImplementedError |
368 | market = self.market(symbol) | ||
369 | response = getattr(self, method)(self.extend({ | ||
370 | 'currencyPair': market['id'], | ||
371 | 'rate': self.price_to_precision(symbol, price), | ||
372 | 'amount': self.amount_to_precision(symbol, amount), | ||
373 | }, params)) | ||
374 | timestamp = self.milliseconds() | ||
375 | order = self.parse_order(self.extend({ | ||
376 | 'timestamp': timestamp, | ||
377 | 'status': 'open', | ||
378 | 'type': type, | ||
379 | 'side': side, | ||
380 | 'price': price, | ||
381 | 'amount': amount, | ||
382 | }, response), market) | ||
383 | id = order['id'] | ||
384 | self.orders[id] = order | ||
385 | return self.extend({'info': response}, order) | ||
386 | |||
387 | def price_to_precision(self, symbol, price): | ||
388 | """ | ||
389 | Wrapped to avoid float | ||
390 | """ | ||
391 | return ('{:.' + str(self.markets[symbol]['precision']['price']) + 'f}').format(price).rstrip("0").rstrip(".") | ||
392 | |||
393 | def amount_to_precision(self, symbol, amount): | ||
394 | """ | ||
395 | Wrapped to avoid float | ||
396 | """ | ||
397 | return ('{:.' + str(self.markets[symbol]['precision']['amount']) + 'f}').format(amount).rstrip("0").rstrip(".") | ||
393 | 398 | ||
394 | def common_currency_code(self, currency): | 399 | def common_currency_code(self, currency): |
395 | """ | 400 | """ |
@@ -63,7 +63,7 @@ def get_user_market(config_path, user_id, debug=False): | |||
63 | if debug: | 63 | if debug: |
64 | args.append("--debug") | 64 | args.append("--debug") |
65 | args = parse_args(args) | 65 | args = parse_args(args) |
66 | pg_config = parse_config(args) | 66 | pg_config, redis_config = parse_config(args) |
67 | market_id, market_config, user_id = list(fetch_markets(pg_config, str(user_id)))[0] | 67 | market_id, market_config, user_id = list(fetch_markets(pg_config, str(user_id)))[0] |
68 | return market.Market.from_config(market_config, args, | 68 | return market.Market.from_config(market_config, args, |
69 | pg_config=pg_config, market_id=market_id, | 69 | pg_config=pg_config, market_id=market_id, |
diff --git a/tests/test_ccxt_wrapper.py b/tests/test_ccxt_wrapper.py index 10e334d..9ddfbc1 100644 --- a/tests/test_ccxt_wrapper.py +++ b/tests/test_ccxt_wrapper.py | |||
@@ -110,7 +110,23 @@ class poloniexETest(unittest.TestCase): | |||
110 | retry_call.assert_not_called() | 110 | retry_call.assert_not_called() |
111 | 111 | ||
112 | def test_order_precision(self): | 112 | def test_order_precision(self): |
113 | self.assertEqual(8, self.s.order_precision("FOO")) | 113 | self.s.markets = { |
114 | "FOO": { | ||
115 | "precision": { | ||
116 | "price": 5, | ||
117 | "amount": 6, | ||
118 | } | ||
119 | }, | ||
120 | "BAR": { | ||
121 | "precision": { | ||
122 | "price": 7, | ||
123 | "amount": 8, | ||
124 | } | ||
125 | } | ||
126 | } | ||
127 | with mock.patch.object(self.s, "load_markets") as load_markets: | ||
128 | self.assertEqual(5, self.s.order_precision("FOO")) | ||
129 | load_markets.assert_called_once() | ||
114 | 130 | ||
115 | def test_transfer_balance(self): | 131 | def test_transfer_balance(self): |
116 | with self.subTest(success=True),\ | 132 | with self.subTest(success=True),\ |
@@ -167,33 +183,6 @@ class poloniexETest(unittest.TestCase): | |||
167 | } | 183 | } |
168 | self.assertEqual(expected, self.s.margin_summary()) | 184 | self.assertEqual(expected, self.s.margin_summary()) |
169 | 185 | ||
170 | def test_create_order(self): | ||
171 | with mock.patch.object(self.s, "create_exchange_order") as exchange,\ | ||
172 | mock.patch.object(self.s, "create_margin_order") as margin: | ||
173 | with self.subTest(account="unspecified"): | ||
174 | self.s.create_order("symbol", "type", "side", "amount", price="price", lending_rate="lending_rate", params="params") | ||
175 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
176 | margin.assert_not_called() | ||
177 | exchange.reset_mock() | ||
178 | margin.reset_mock() | ||
179 | |||
180 | with self.subTest(account="exchange"): | ||
181 | self.s.create_order("symbol", "type", "side", "amount", account="exchange", price="price", lending_rate="lending_rate", params="params") | ||
182 | exchange.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | ||
183 | margin.assert_not_called() | ||
184 | exchange.reset_mock() | ||
185 | margin.reset_mock() | ||
186 | |||
187 | with self.subTest(account="margin"): | ||
188 | self.s.create_order("symbol", "type", "side", "amount", account="margin", price="price", lending_rate="lending_rate", params="params") | ||
189 | margin.assert_called_once_with("symbol", "type", "side", "amount", lending_rate="lending_rate", price="price", params="params") | ||
190 | exchange.assert_not_called() | ||
191 | exchange.reset_mock() | ||
192 | margin.reset_mock() | ||
193 | |||
194 | with self.subTest(account="unknown"), self.assertRaises(NotImplementedError): | ||
195 | self.s.create_order("symbol", "type", "side", "amount", account="unknown") | ||
196 | |||
197 | def test_parse_ticker(self): | 186 | def test_parse_ticker(self): |
198 | ticker = { | 187 | ticker = { |
199 | "high24hr": "12", | 188 | "high24hr": "12", |
@@ -439,11 +428,13 @@ class poloniexETest(unittest.TestCase): | |||
439 | self.assertEqual(expected_doge, result["DOGE"]) | 428 | self.assertEqual(expected_doge, result["DOGE"]) |
440 | self.assertEqual(expected_btc, result["BTC"]) | 429 | self.assertEqual(expected_btc, result["BTC"]) |
441 | 430 | ||
442 | def test_create_margin_order(self): | 431 | def test_create_order(self): |
443 | with self.assertRaises(market.ExchangeError): | 432 | with self.subTest(type="market"),\ |
444 | self.s.create_margin_order("FOO", "market", "buy", "10") | 433 | self.assertRaises(market.ExchangeError): |
434 | self.s.create_order("FOO", "market", "buy", "10") | ||
445 | 435 | ||
446 | with mock.patch.object(self.s, "load_markets") as load_markets,\ | 436 | with self.subTest(type="limit", account="margin"),\ |
437 | mock.patch.object(self.s, "load_markets") as load_markets,\ | ||
447 | mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\ | 438 | mock.patch.object(self.s, "privatePostMarginBuy") as margin_buy,\ |
448 | mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\ | 439 | mock.patch.object(self.s, "privatePostMarginSell") as margin_sell,\ |
449 | mock.patch.object(self.s, "market") as market_mock,\ | 440 | mock.patch.object(self.s, "market") as market_mock,\ |
@@ -460,26 +451,97 @@ class poloniexETest(unittest.TestCase): | |||
460 | ptp.return_value = D("0.1") | 451 | ptp.return_value = D("0.1") |
461 | atp.return_value = D("12") | 452 | atp.return_value = D("12") |
462 | 453 | ||
463 | order = self.s.create_margin_order("BTC_ETC", "margin", "buy", "12", price="0.1") | 454 | order = self.s.create_order("BTC_ETC", "limit", "buy", "12", |
455 | account="margin", price="0.1") | ||
464 | self.assertEqual(123, order["id"]) | 456 | self.assertEqual(123, order["id"]) |
465 | margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) | 457 | margin_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) |
466 | margin_sell.assert_not_called() | 458 | margin_sell.assert_not_called() |
467 | margin_buy.reset_mock() | 459 | margin_buy.reset_mock() |
468 | margin_sell.reset_mock() | 460 | margin_sell.reset_mock() |
469 | 461 | ||
470 | order = self.s.create_margin_order("BTC_ETC", "margin", "sell", "12", lending_rate="0.01", price="0.1") | 462 | order = self.s.create_order("BTC_ETC", "limit", "sell", |
463 | "12", account="margin", lending_rate="0.01", price="0.1") | ||
471 | self.assertEqual(456, order["id"]) | 464 | self.assertEqual(456, order["id"]) |
472 | margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"}) | 465 | margin_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12"), "lendingRate": "0.01"}) |
473 | margin_buy.assert_not_called() | 466 | margin_buy.assert_not_called() |
474 | 467 | ||
475 | def test_create_exchange_order(self): | 468 | with self.subTest(type="limit", account="exchange"),\ |
476 | with mock.patch.object(market.ccxt.poloniex, "create_order") as create_order: | 469 | mock.patch.object(self.s, "load_markets") as load_markets,\ |
477 | self.s.create_order("symbol", "type", "side", "amount", price="price", params="params") | 470 | mock.patch.object(self.s, "privatePostBuy") as exchange_buy,\ |
471 | mock.patch.object(self.s, "privatePostSell") as exchange_sell,\ | ||
472 | mock.patch.object(self.s, "market") as market_mock,\ | ||
473 | mock.patch.object(self.s, "price_to_precision") as ptp,\ | ||
474 | mock.patch.object(self.s, "amount_to_precision") as atp: | ||
478 | 475 | ||
479 | create_order.assert_called_once_with("symbol", "type", "side", "amount", price="price", params="params") | 476 | exchange_buy.return_value = { |
477 | "orderNumber": 123 | ||
478 | } | ||
479 | exchange_sell.return_value = { | ||
480 | "orderNumber": 456 | ||
481 | } | ||
482 | market_mock.return_value = { "id": "BTC_ETC", "symbol": "BTC_ETC" } | ||
483 | ptp.return_value = D("0.1") | ||
484 | atp.return_value = D("12") | ||
485 | |||
486 | order = self.s.create_order("BTC_ETC", "limit", "buy", "12", | ||
487 | account="exchange", price="0.1") | ||
488 | self.assertEqual(123, order["id"]) | ||
489 | exchange_buy.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) | ||
490 | exchange_sell.assert_not_called() | ||
491 | exchange_buy.reset_mock() | ||
492 | exchange_sell.reset_mock() | ||
493 | |||
494 | order = self.s.create_order("BTC_ETC", "limit", "sell", | ||
495 | "12", account="exchange", lending_rate="0.01", price="0.1") | ||
496 | self.assertEqual(456, order["id"]) | ||
497 | exchange_sell.assert_called_once_with({"currencyPair": "BTC_ETC", "rate": D("0.1"), "amount": D("12")}) | ||
498 | exchange_buy.assert_not_called() | ||
499 | |||
500 | with self.subTest(account="unknown"), self.assertRaises(NotImplementedError),\ | ||
501 | mock.patch.object(self.s, "load_markets") as load_markets: | ||
502 | self.s.create_order("symbol", "type", "side", "amount", account="unknown") | ||
480 | 503 | ||
481 | def test_common_currency_code(self): | 504 | def test_common_currency_code(self): |
482 | self.assertEqual("FOO", self.s.common_currency_code("FOO")) | 505 | self.assertEqual("FOO", self.s.common_currency_code("FOO")) |
483 | 506 | ||
484 | def test_currency_id(self): | 507 | def test_currency_id(self): |
485 | self.assertEqual("FOO", self.s.currency_id("FOO")) | 508 | self.assertEqual("FOO", self.s.currency_id("FOO")) |
509 | |||
510 | def test_amount_to_precision(self): | ||
511 | self.s.markets = { | ||
512 | "FOO": { | ||
513 | "precision": { | ||
514 | "price": 5, | ||
515 | "amount": 6, | ||
516 | } | ||
517 | }, | ||
518 | "BAR": { | ||
519 | "precision": { | ||
520 | "price": 7, | ||
521 | "amount": 8, | ||
522 | } | ||
523 | } | ||
524 | } | ||
525 | self.assertEqual("0.0001", self.s.amount_to_precision("FOO", D("0.0001"))) | ||
526 | self.assertEqual("0.0000001", self.s.amount_to_precision("BAR", D("0.0000001"))) | ||
527 | self.assertEqual("0.000001", self.s.amount_to_precision("FOO", D("0.000001"))) | ||
528 | |||
529 | def test_price_to_precision(self): | ||
530 | self.s.markets = { | ||
531 | "FOO": { | ||
532 | "precision": { | ||
533 | "price": 5, | ||
534 | "amount": 6, | ||
535 | } | ||
536 | }, | ||
537 | "BAR": { | ||
538 | "precision": { | ||
539 | "price": 7, | ||
540 | "amount": 8, | ||
541 | } | ||
542 | } | ||
543 | } | ||
544 | self.assertEqual("0.0001", self.s.price_to_precision("FOO", D("0.0001"))) | ||
545 | self.assertEqual("0.0000001", self.s.price_to_precision("BAR", D("0.0000001"))) | ||
546 | self.assertEqual("0", self.s.price_to_precision("FOO", D("0.000001"))) | ||
547 | |||
diff --git a/tests/test_main.py b/tests/test_main.py index b650870..55b1382 100644 --- a/tests/test_main.py +++ b/tests/test_main.py | |||
@@ -103,7 +103,7 @@ class MainTest(WebMockTestCase): | |||
103 | mock.patch("main.parse_config") as main_parse_config: | 103 | mock.patch("main.parse_config") as main_parse_config: |
104 | with self.subTest(debug=False): | 104 | with self.subTest(debug=False): |
105 | main_parse_args.return_value = self.market_args() | 105 | main_parse_args.return_value = self.market_args() |
106 | main_parse_config.return_value = "pg_config" | 106 | main_parse_config.return_value = ["pg_config", "redis_config"] |
107 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] | 107 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] |
108 | m = main.get_user_market("config_path.ini", 1) | 108 | m = main.get_user_market("config_path.ini", 1) |
109 | 109 | ||
@@ -114,7 +114,7 @@ class MainTest(WebMockTestCase): | |||
114 | main_parse_args.reset_mock() | 114 | main_parse_args.reset_mock() |
115 | with self.subTest(debug=True): | 115 | with self.subTest(debug=True): |
116 | main_parse_args.return_value = self.market_args(debug=True) | 116 | main_parse_args.return_value = self.market_args(debug=True) |
117 | main_parse_config.return_value = "pg_config" | 117 | main_parse_config.return_value = ["pg_config", "redis_config"] |
118 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] | 118 | main_fetch_markets.return_value = [(1, {"key": "market_config"}, 3)] |
119 | m = main.get_user_market("config_path.ini", 1, debug=True) | 119 | m = main.get_user_market("config_path.ini", 1, debug=True) |
120 | 120 | ||