Merge pull request #2178 from freqtrade/refactor/stoploss_on_e_to_binance

[refactor] Move stoploss on exchange implementation to binance
This commit is contained in:
Matthias 2019-08-25 19:16:05 +02:00 committed by GitHub
commit 626b9bbf64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 164 additions and 111 deletions

View File

@ -11,7 +11,7 @@ class DependencyException(Exception):
class OperationalException(Exception): class OperationalException(Exception):
""" """
Requires manual intervention. Requires manual intervention and will usually stop the bot.
This happens when an exchange returns an unexpected error during runtime This happens when an exchange returns an unexpected error during runtime
or given configuration is invalid. or given configuration is invalid.
""" """

View File

@ -2,6 +2,9 @@
import logging import logging
from typing import Dict from typing import Dict
import ccxt
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,3 +28,53 @@ class Binance(Exchange):
limit = min(list(filter(lambda x: limit <= x, limit_range))) limit = min(list(filter(lambda x: limit <= x, limit_range)))
return super().get_order_book(pair, limit) return super().get_order_book(pair, limit)
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
"""
creates a stoploss limit order.
this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet.
"""
ordertype = "stop_loss_limit"
stop_price = self.symbol_price_prec(pair, stop_price)
# Ensure rate is less than stop price
if stop_price <= rate:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
try:
params = self._params.copy()
params.update({'stopPrice': stop_price})
amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate)
order = self._api.create_order(pair, ordertype, 'sell',
amount, rate, params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
return order
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate}.'
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}.'
f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e

View File

@ -320,7 +320,7 @@ class Exchange(object):
if (order_types.get("stoploss_on_exchange") if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)): and not self._ft_has.get("stoploss_on_exchange", False)):
raise OperationalException( raise OperationalException(
'On exchange stoploss is not supported for %s.' % self.name f'On exchange stoploss is not supported for {self.name}.'
) )
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
@ -450,30 +450,14 @@ class Exchange(object):
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
""" """
creates a stoploss limit order. creates a stoploss limit order.
NOTICE: it is not supported by all exchanges. only binance is tested for now. Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
TODO: implementation maybe needs to be moved to the binance subclass exchange's subclass.
The exception below should never raise, since we disallow
starting the bot in validate_ordertypes()
Note: Changes to this interface need to be applied to all sub-classes too.
""" """
ordertype = "stop_loss_limit"
stop_price = self.symbol_price_prec(pair, stop_price) raise OperationalException(f"stoploss_limit is not implemented for {self.name}.")
# Ensure rate is less than stop price
if stop_price <= rate:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
params = self._params.copy()
params.update({'stopPrice': stop_price})
order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
return order
@retrier @retrier
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:

View File

@ -0,0 +1,90 @@
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.tests.conftest import get_patched_exchange
def test_stoploss_limit_order(default_conf, mocker):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] == 200
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
def test_stoploss_limit_order_dry_run(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop_loss_limit'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1

View File

@ -101,18 +101,21 @@ def test_destroy(default_conf, mocker, caplog):
def test_init_exception(default_conf, mocker): def test_init_exception(default_conf, mocker):
default_conf['exchange']['name'] = 'wrong_exchange_name' default_conf['exchange']['name'] = 'wrong_exchange_name'
with pytest.raises( with pytest.raises(OperationalException,
OperationalException, match=f"Exchange {default_conf['exchange']['name']} is not supported"):
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
Exchange(default_conf) Exchange(default_conf)
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
with pytest.raises( with pytest.raises(OperationalException,
OperationalException, match=f"Exchange {default_conf['exchange']['name']} is not supported"):
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError)) mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError))
Exchange(default_conf) Exchange(default_conf)
with pytest.raises(OperationalException,
match=r"Initialization of ccxt failed. Reason: DeadBeef"):
mocker.patch("ccxt.binance", MagicMock(side_effect=ccxt.BaseError("DeadBeef")))
Exchange(default_conf)
def test_exchange_resolver(default_conf, mocker, caplog): def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock()))
@ -1436,87 +1439,11 @@ def test_get_fee(default_conf, mocker, exchange_name):
'get_fee', 'calculate_fee') 'get_fee', 'calculate_fee')
def test_stoploss_limit_order(default_conf, mocker): def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker):
api_mock = MagicMock() exchange = get_patched_exchange(mocker, default_conf, 'bittrex')
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"):
order_type = 'stop_loss_limit'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] == 200
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(OperationalException):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
def test_stoploss_limit_order_dry_run(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop_loss_limit'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1
def test_merge_ft_has_dict(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker):
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',

View File

@ -2414,7 +2414,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@ -2454,7 +2454,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
freqtrade.process_maybe_execute_sell(trade) freqtrade.process_maybe_execute_sell(trade)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.is_open is False assert trade.is_open is False
print(trade.sell_reason)
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2