diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-26 14:18:34 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2018-02-26 14:18:34 +0100 |
commit | f70bb858007cd3be6766ee0aa4a3d9133952eb98 (patch) | |
tree | 0c56dd45d99ab6409cda4ef240759ec56981a351 | |
parent | d24bb10c3cad1f144b76022481f46b4524873f4b (diff) | |
download | Trader-f70bb858007cd3be6766ee0aa4a3d9133952eb98.tar.gz Trader-f70bb858007cd3be6766ee0aa4a3d9133952eb98.tar.zst Trader-f70bb858007cd3be6766ee0aa4a3d9133952eb98.zip |
Retry running order when available balance is insufficient
-rw-r--r-- | helper.py | 1 | ||||
-rw-r--r-- | portfolio.py | 19 | ||||
-rw-r--r-- | requirements.txt | 11 | ||||
-rw-r--r-- | test.py | 59 |
4 files changed, 75 insertions, 15 deletions
@@ -98,6 +98,7 @@ def print_orders(market, base_currency="BTC"): | |||
98 | market.trades.prepare_orders(compute_value="average") | 98 | market.trades.prepare_orders(compute_value="average") |
99 | 99 | ||
100 | def print_balances(market, base_currency="BTC"): | 100 | def print_balances(market, base_currency="BTC"): |
101 | market.report.log_stage("print_balances") | ||
101 | market.balances.fetch_balances() | 102 | market.balances.fetch_balances() |
102 | if base_currency is not None: | 103 | if base_currency is not None: |
103 | market.report.print_log("total:") | 104 | market.report.print_log("total:") |
diff --git a/portfolio.py b/portfolio.py index 0797de0..6763fc6 100644 --- a/portfolio.py +++ b/portfolio.py | |||
@@ -3,7 +3,8 @@ from datetime import datetime, timedelta | |||
3 | from decimal import Decimal as D, ROUND_DOWN | 3 | from decimal import Decimal as D, ROUND_DOWN |
4 | from json import JSONDecodeError | 4 | from json import JSONDecodeError |
5 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError | 5 | from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError |
6 | from ccxt import ExchangeError, ExchangeNotAvailable, InvalidOrder | 6 | from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder |
7 | from retry import retry | ||
7 | import requests | 8 | import requests |
8 | 9 | ||
9 | # FIXME: correctly handle web call timeouts | 10 | # FIXME: correctly handle web call timeouts |
@@ -476,6 +477,7 @@ class Order: | |||
476 | self.close_if_possible = close_if_possible | 477 | self.close_if_possible = close_if_possible |
477 | self.id = None | 478 | self.id = None |
478 | self.fetch_cache_timestamp = None | 479 | self.fetch_cache_timestamp = None |
480 | self.tries = 0 | ||
479 | 481 | ||
480 | def as_json(self): | 482 | def as_json(self): |
481 | return { | 483 | return { |
@@ -521,7 +523,9 @@ class Order: | |||
521 | def finished(self): | 523 | def finished(self): |
522 | return self.status == "closed" or self.status == "canceled" or self.status == "error" | 524 | return self.status == "closed" or self.status == "canceled" or self.status == "error" |
523 | 525 | ||
526 | @retry(InsufficientFunds) | ||
524 | def run(self): | 527 | def run(self): |
528 | self.tries += 1 | ||
525 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) | 529 | symbol = "{}/{}".format(self.amount.currency, self.base_currency) |
526 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value | 530 | amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value |
527 | 531 | ||
@@ -530,16 +534,25 @@ class Order: | |||
530 | symbol, self.action, amount, self.rate, self.account)) | 534 | symbol, self.action, amount, self.rate, self.account)) |
531 | self.results.append({"debug": True, "id": -1}) | 535 | self.results.append({"debug": True, "id": -1}) |
532 | else: | 536 | else: |
537 | action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) | ||
533 | try: | 538 | try: |
534 | self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) | 539 | self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account)) |
535 | except (ExchangeNotAvailable, InvalidOrder): | 540 | except InvalidOrder: |
536 | # Impossible to honor the order (dust amount) | 541 | # Impossible to honor the order (dust amount) |
537 | self.status = "closed" | 542 | self.status = "closed" |
538 | self.mark_finished_order() | 543 | self.mark_finished_order() |
539 | return | 544 | return |
545 | except InsufficientFunds as e: | ||
546 | if self.tries < 5: | ||
547 | self.market.report.log_error(action, message="Retrying with reduced amount", exception=e) | ||
548 | self.amount = self.amount * D("0.99") | ||
549 | raise e | ||
550 | else: | ||
551 | self.market.report.log_error(action, message="Giving up {}".format(self), exception=e) | ||
552 | self.status = "error" | ||
553 | return | ||
540 | except Exception as e: | 554 | except Exception as e: |
541 | self.status = "error" | 555 | self.status = "error" |
542 | action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account) | ||
543 | self.market.report.log_error(action, exception=e) | 556 | self.market.report.log_error(action, exception=e) |
544 | return | 557 | return |
545 | self.id = self.results[0]["id"] | 558 | self.id = self.results[0]["id"] |
diff --git a/requirements.txt b/requirements.txt index 8e78c05..5a25dbf 100644 --- a/requirements.txt +++ b/requirements.txt | |||
@@ -1,5 +1,6 @@ | |||
1 | ccxt>=1.10.593 | 1 | ccxt==1.10.1216 |
2 | simplejson>=3.13.2 | 2 | simplejson==3.13.2 |
3 | requests>=2.11.1 | 3 | requests>=2.18.4 |
4 | requests_mock>=1.4.0 | 4 | requests_mock==1.4.0 |
5 | psycopg2>=2.7 | 5 | psycopg2==2.7.4 |
6 | retry==0.9.2 | ||
@@ -2073,24 +2073,68 @@ class OrderTest(WebMockTestCase): | |||
2073 | mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order: | 2073 | mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order: |
2074 | order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001), | 2074 | order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001), |
2075 | D("0.1"), "BTC", "long", self.m, "trade") | 2075 | D("0.1"), "BTC", "long", self.m, "trade") |
2076 | self.m.ccxt.create_order.side_effect = portfolio.ExchangeNotAvailable | 2076 | self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder |
2077 | order.run() | 2077 | order.run() |
2078 | self.m.ccxt.create_order.assert_called_once() | 2078 | self.m.ccxt.create_order.assert_called_once() |
2079 | self.assertEqual(0, len(order.results)) | 2079 | self.assertEqual(0, len(order.results)) |
2080 | self.assertEqual("closed", order.status) | 2080 | self.assertEqual("closed", order.status) |
2081 | mark_finished_order.assert_called_once() | 2081 | mark_finished_order.assert_called_once() |
2082 | 2082 | ||
2083 | self.m.ccxt.order_precision.return_value = 8 | ||
2083 | self.m.ccxt.create_order.reset_mock() | 2084 | self.m.ccxt.create_order.reset_mock() |
2084 | with self.subTest(dust_amount_exception=True),\ | 2085 | with self.subTest(insufficient_funds=True),\ |
2085 | mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order: | 2086 | mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order: |
2086 | order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001), | 2087 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), |
2087 | D("0.1"), "BTC", "long", self.m, "trade") | 2088 | D("0.1"), "BTC", "long", self.m, "trade") |
2088 | self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder | 2089 | self.m.ccxt.create_order.side_effect = [ |
2090 | portfolio.InsufficientFunds, | ||
2091 | portfolio.InsufficientFunds, | ||
2092 | portfolio.InsufficientFunds, | ||
2093 | { "id": 123 }, | ||
2094 | ] | ||
2089 | order.run() | 2095 | order.run() |
2090 | self.m.ccxt.create_order.assert_called_once() | 2096 | self.m.ccxt.create_order.assert_has_calls([ |
2097 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
2098 | mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')), | ||
2099 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')), | ||
2100 | mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')), | ||
2101 | ]) | ||
2102 | self.assertEqual(4, self.m.ccxt.create_order.call_count) | ||
2103 | self.assertEqual(1, len(order.results)) | ||
2104 | self.assertEqual("open", order.status) | ||
2105 | self.assertEqual(4, order.tries) | ||
2106 | self.m.report.log_error.assert_called() | ||
2107 | self.assertEqual(4, self.m.report.log_error.call_count) | ||
2108 | |||
2109 | self.m.ccxt.order_precision.return_value = 8 | ||
2110 | self.m.ccxt.create_order.reset_mock() | ||
2111 | self.m.report.log_error.reset_mock() | ||
2112 | with self.subTest(insufficient_funds=True),\ | ||
2113 | mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order: | ||
2114 | order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"), | ||
2115 | D("0.1"), "BTC", "long", self.m, "trade") | ||
2116 | self.m.ccxt.create_order.side_effect = [ | ||
2117 | portfolio.InsufficientFunds, | ||
2118 | portfolio.InsufficientFunds, | ||
2119 | portfolio.InsufficientFunds, | ||
2120 | portfolio.InsufficientFunds, | ||
2121 | portfolio.InsufficientFunds, | ||
2122 | ] | ||
2123 | order.run() | ||
2124 | self.m.ccxt.create_order.assert_has_calls([ | ||
2125 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')), | ||
2126 | mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')), | ||
2127 | mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')), | ||
2128 | mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')), | ||
2129 | mock.call('ETH/BTC', 'limit', 'buy', D('0.00096059'), account='exchange', price=D('0.1')), | ||
2130 | ]) | ||
2131 | self.assertEqual(5, self.m.ccxt.create_order.call_count) | ||
2091 | self.assertEqual(0, len(order.results)) | 2132 | self.assertEqual(0, len(order.results)) |
2092 | self.assertEqual("closed", order.status) | 2133 | self.assertEqual("error", order.status) |
2093 | mark_finished_order.assert_called_once() | 2134 | self.assertEqual(5, order.tries) |
2135 | self.m.report.log_error.assert_called() | ||
2136 | self.assertEqual(5, self.m.report.log_error.call_count) | ||
2137 | 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) | ||
2094 | 2138 | ||
2095 | 2139 | ||
2096 | @unittest.skipUnless("unit" in limits, "Unit skipped") | 2140 | @unittest.skipUnless("unit" in limits, "Unit skipped") |
@@ -2750,6 +2794,7 @@ class HelperTest(WebMockTestCase): | |||
2750 | 2794 | ||
2751 | helper.print_balances(self.m) | 2795 | helper.print_balances(self.m) |
2752 | 2796 | ||
2797 | self.m.report.log_stage.assert_called_once_with("print_balances") | ||
2753 | self.m.balances.fetch_balances.assert_called_with() | 2798 | self.m.balances.fetch_balances.assert_called_with() |
2754 | self.m.report.print_log.assert_has_calls([ | 2799 | self.m.report.print_log.assert_has_calls([ |
2755 | mock.call("total:"), | 2800 | mock.call("total:"), |