aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-26 14:18:34 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2018-02-26 14:18:34 +0100
commitf70bb858007cd3be6766ee0aa4a3d9133952eb98 (patch)
tree0c56dd45d99ab6409cda4ef240759ec56981a351
parentd24bb10c3cad1f144b76022481f46b4524873f4b (diff)
downloadTrader-f70bb858007cd3be6766ee0aa4a3d9133952eb98.tar.gz
Trader-f70bb858007cd3be6766ee0aa4a3d9133952eb98.tar.zst
Trader-f70bb858007cd3be6766ee0aa4a3d9133952eb98.zip
Retry running order when available balance is insufficient
-rw-r--r--helper.py1
-rw-r--r--portfolio.py19
-rw-r--r--requirements.txt11
-rw-r--r--test.py59
4 files changed, 75 insertions, 15 deletions
diff --git a/helper.py b/helper.py
index 6d28c3f..4d73078 100644
--- a/helper.py
+++ b/helper.py
@@ -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
100def print_balances(market, base_currency="BTC"): 100def 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
3from decimal import Decimal as D, ROUND_DOWN 3from decimal import Decimal as D, ROUND_DOWN
4from json import JSONDecodeError 4from json import JSONDecodeError
5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError 5from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
6from ccxt import ExchangeError, ExchangeNotAvailable, InvalidOrder 6from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder
7from retry import retry
7import requests 8import 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 @@
1ccxt>=1.10.593 1ccxt==1.10.1216
2simplejson>=3.13.2 2simplejson==3.13.2
3requests>=2.11.1 3requests>=2.18.4
4requests_mock>=1.4.0 4requests_mock==1.4.0
5psycopg2>=2.7 5psycopg2==2.7.4
6retry==0.9.2
diff --git a/test.py b/test.py
index 52d737d..955e2a1 100644
--- a/test.py
+++ b/test.py
@@ -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:"),