from decimal import Decimal as D, ROUND_DOWN
from json import JSONDecodeError
from simplejson.errors import JSONDecodeError as SimpleJSONDecodeError
-from ccxt import ExchangeError, ExchangeNotAvailable, InvalidOrder
+from ccxt import ExchangeError, InsufficientFunds, ExchangeNotAvailable, InvalidOrder
+from retry import retry
import requests
# FIXME: correctly handle web call timeouts
self.close_if_possible = close_if_possible
self.id = None
self.fetch_cache_timestamp = None
+ self.tries = 0
def as_json(self):
return {
def finished(self):
return self.status == "closed" or self.status == "canceled" or self.status == "error"
+ @retry(InsufficientFunds)
def run(self):
+ self.tries += 1
symbol = "{}/{}".format(self.amount.currency, self.base_currency)
amount = round(self.amount, self.market.ccxt.order_precision(symbol)).value
symbol, self.action, amount, self.rate, self.account))
self.results.append({"debug": True, "id": -1})
else:
+ action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
try:
self.results.append(self.market.ccxt.create_order(symbol, 'limit', self.action, amount, price=self.rate, account=self.account))
- except (ExchangeNotAvailable, InvalidOrder):
+ except InvalidOrder:
# Impossible to honor the order (dust amount)
self.status = "closed"
self.mark_finished_order()
return
+ except InsufficientFunds as e:
+ if self.tries < 5:
+ self.market.report.log_error(action, message="Retrying with reduced amount", exception=e)
+ self.amount = self.amount * D("0.99")
+ raise e
+ else:
+ self.market.report.log_error(action, message="Giving up {}".format(self), exception=e)
+ self.status = "error"
+ return
except Exception as e:
self.status = "error"
- action = "market.ccxt.create_order('{}', 'limit', '{}', {}, price={}, account={})".format(symbol, self.action, amount, self.rate, self.account)
self.market.report.log_error(action, exception=e)
return
self.id = self.results[0]["id"]
mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001),
D("0.1"), "BTC", "long", self.m, "trade")
- self.m.ccxt.create_order.side_effect = portfolio.ExchangeNotAvailable
+ self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder
order.run()
self.m.ccxt.create_order.assert_called_once()
self.assertEqual(0, len(order.results))
self.assertEqual("closed", order.status)
mark_finished_order.assert_called_once()
+ self.m.ccxt.order_precision.return_value = 8
self.m.ccxt.create_order.reset_mock()
- with self.subTest(dust_amount_exception=True),\
+ with self.subTest(insufficient_funds=True),\
mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
- order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001),
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
D("0.1"), "BTC", "long", self.m, "trade")
- self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder
+ self.m.ccxt.create_order.side_effect = [
+ portfolio.InsufficientFunds,
+ portfolio.InsufficientFunds,
+ portfolio.InsufficientFunds,
+ { "id": 123 },
+ ]
order.run()
- self.m.ccxt.create_order.assert_called_once()
+ self.m.ccxt.create_order.assert_has_calls([
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
+ ])
+ self.assertEqual(4, self.m.ccxt.create_order.call_count)
+ self.assertEqual(1, len(order.results))
+ self.assertEqual("open", order.status)
+ self.assertEqual(4, order.tries)
+ self.m.report.log_error.assert_called()
+ self.assertEqual(4, self.m.report.log_error.call_count)
+
+ self.m.ccxt.order_precision.return_value = 8
+ self.m.ccxt.create_order.reset_mock()
+ self.m.report.log_error.reset_mock()
+ with self.subTest(insufficient_funds=True),\
+ mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
+ order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
+ D("0.1"), "BTC", "long", self.m, "trade")
+ self.m.ccxt.create_order.side_effect = [
+ portfolio.InsufficientFunds,
+ portfolio.InsufficientFunds,
+ portfolio.InsufficientFunds,
+ portfolio.InsufficientFunds,
+ portfolio.InsufficientFunds,
+ ]
+ order.run()
+ self.m.ccxt.create_order.assert_has_calls([
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
+ mock.call('ETH/BTC', 'limit', 'buy', D('0.00096059'), account='exchange', price=D('0.1')),
+ ])
+ self.assertEqual(5, self.m.ccxt.create_order.call_count)
self.assertEqual(0, len(order.results))
- self.assertEqual("closed", order.status)
- mark_finished_order.assert_called_once()
+ self.assertEqual("error", order.status)
+ self.assertEqual(5, order.tries)
+ self.m.report.log_error.assert_called()
+ self.assertEqual(5, self.m.report.log_error.call_count)
+ 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)
@unittest.skipUnless("unit" in limits, "Unit skipped")
helper.print_balances(self.m)
+ self.m.report.log_stage.assert_called_once_with("print_balances")
self.m.balances.fetch_balances.assert_called_with()
self.m.report.print_log.assert_has_calls([
mock.call("total:"),