From 8d2e0bfd628c13ff51e5962107b7c0fed5db78b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 13:12:28 +0100 Subject: [PATCH 01/10] Move rate-calcuation for stoploss-limit order to exchange --- freqtrade/exchange/binance.py | 8 ++++++-- freqtrade/exchange/exchange.py | 3 ++- freqtrade/freqtradebot.py | 5 +---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 12326f083..15796bdcb 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -32,13 +32,17 @@ class Binance(Exchange): return super().get_order_book(pair, limit) - 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, + order_types: Dict) -> 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. - """ + # Limit price threshold: As limit price should always be below stop-price + LIMIT_PRICE_PCT = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + rate = stop_price * LIMIT_PRICE_PCT + ordertype = "stop_loss_limit" stop_price = self.price_to_precision(pair, stop_price) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 87c189457..4c5ef823b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -519,7 +519,8 @@ class Exchange: return self.create_order(pair, ordertype, 'sell', amount, rate, params) - 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, + order_types: Dict) -> Dict: """ creates a stoploss limit order. Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e712892f1..1a3097c25 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -636,13 +636,10 @@ class FreqtradeBot: Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. :return: True if the order succeeded, and False in case of problems. """ - # Limit price threshold: As limit price should always be below stop-price - LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - try: stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount, stop_price=stop_price, - rate=rate * LIMIT_PRICE_PCT) + order_types=self.strategy.order_types) trade.stoploss_order_id = str(stoploss_order['id']) return True except InvalidOrderException as e: From da0af489a2cd8cc437752e308cab9e44ffc38c7a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 13:25:41 +0100 Subject: [PATCH 02/10] Adjust tests to pass in order_types instead of rate --- tests/exchange/test_binance.py | 19 ++++++++++--------- tests/exchange/test_exchange.py | 2 +- tests/test_freqtradebot.py | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 4bc918c3d..bda4946b4 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -28,11 +28,12 @@ def test_stoploss_limit_order(default_conf, mocker): 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) + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) api_mock.create_order.reset_mock() - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) assert 'id' in order assert 'info' in order @@ -41,30 +42,29 @@ def test_stoploss_limit_order(default_conf, mocker): 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) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) 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) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) 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) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_limit_order_dry_run(default_conf, mocker): @@ -77,11 +77,12 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): 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) + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) api_mock.create_order.reset_mock() - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) assert 'id' in order assert 'info' in order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7064d76e1..be40f2192 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1761,7 +1761,7 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, 'bittrex') with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"): - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) + exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_merge_ft_has_dict(default_conf, mocker): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5a4820f2f..2aa1548f8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1315,7 +1315,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') stoploss_order_mock.assert_called_once_with(amount=85.25149190110828, pair='ETH/BTC', - rate=0.00002344 * 0.95 * 0.99, + order_types=freqtrade.strategy.order_types, stop_price=0.00002344 * 0.95) @@ -1492,7 +1492,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') stoploss_order_mock.assert_called_once_with(amount=2131074.168797954, pair='NEO/BTC', - rate=0.00002344 * 0.99 * 0.99, + order_types=freqtrade.strategy.order_types, stop_price=0.00002344 * 0.99) From 256fc2e78cc532714b6a8f91d0783b809349bb0c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 13:30:56 +0100 Subject: [PATCH 03/10] Rename stoploss_limit to stoploss --- freqtrade/exchange/binance.py | 3 +-- freqtrade/exchange/exchange.py | 6 +++--- freqtrade/freqtradebot.py | 6 +++--- tests/exchange/test_binance.py | 20 ++++++++++---------- tests/exchange/test_exchange.py | 2 +- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 15796bdcb..d08726cf0 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -32,8 +32,7 @@ class Binance(Exchange): return super().get_order_book(pair, limit) - def stoploss_limit(self, pair: str, amount: float, stop_price: float, - order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss limit order. this stoploss-limit is binance-specific. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4c5ef823b..121a8c636 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -519,10 +519,10 @@ class Exchange: return self.create_order(pair, ordertype, 'sell', amount, rate, params) - def stoploss_limit(self, pair: str, amount: float, stop_price: float, - order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ - creates a stoploss limit order. + creates a stoploss order. + The precise ordertype is determined by the order_types dict or exchange default. Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each exchange's subclass. The exception below should never raise, since we disallow diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1a3097c25..a4b0ab806 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -637,9 +637,9 @@ class FreqtradeBot: :return: True if the order succeeded, and False in case of problems. """ try: - stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount, - stop_price=stop_price, - order_types=self.strategy.order_types) + stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, + stop_price=stop_price, + order_types=self.strategy.order_types) trade.stoploss_order_id = str(stoploss_order['id']) return True except InvalidOrderException as e: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index bda4946b4..fdf3d7435 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -28,12 +28,12 @@ def test_stoploss_limit_order(default_conf, mocker): 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, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) api_mock.create_order.reset_mock() - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) assert 'id' in order assert 'info' in order @@ -48,23 +48,23 @@ def test_stoploss_limit_order(default_conf, mocker): 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, order_types={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) 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, order_types={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) 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, order_types={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_limit_order_dry_run(default_conf, mocker): @@ -77,12 +77,12 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): 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, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) api_mock.create_order.reset_mock() - order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) assert 'id' in order assert 'info' in order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index be40f2192..7c0c72491 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1761,7 +1761,7 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, 'bittrex') with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"): - exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_merge_ft_has_dict(default_conf, mocker): From 16b34e11cad56216d7a8afde5a4ad73e98cc513b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 14:39:51 +0100 Subject: [PATCH 04/10] Complete rename of stoploss_limit to stoploss --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 4 +-- tests/test_freqtradebot.py | 60 ++++++++++++++++----------------- tests/test_integration.py | 4 +-- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 121a8c636..bef92750c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -530,7 +530,7 @@ class Exchange: Note: Changes to this interface need to be applied to all sub-classes too. """ - raise OperationalException(f"stoploss_limit is not implemented for {self.name}.") + raise OperationalException(f"stoploss is not implemented for {self.name}.") @retrier def get_balance(self, currency: str) -> float: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7c0c72491..680e69764 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1758,9 +1758,9 @@ def test_get_fee(default_conf, mocker, exchange_name): 'get_fee', 'calculate_fee', symbol="ETH/BTC") -def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): +def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, 'bittrex') - with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"): + with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2aa1548f8..a33d47f34 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1031,8 +1031,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) - stoploss_limit = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + stoploss = MagicMock(return_value={'id': 13434334}) + mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1045,13 +1045,13 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None freqtrade.exit_positions(trades) assert trade.stoploss_order_id == '13434334' - assert stoploss_limit.call_count == 1 + assert stoploss.call_count == 1 assert trade.is_open is True def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: - stoploss_limit = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1064,7 +1064,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - stoploss_limit=stoploss_limit + stoploss=stoploss ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1078,7 +1078,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert stoploss_limit.call_count == 1 + assert stoploss.call_count == 1 assert trade.stoploss_order_id == "13434334" # Second case: when stoploss is set but it is not yet hit @@ -1102,10 +1102,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order) - stoploss_limit.reset_mock() + stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert stoploss_limit.call_count == 1 + assert stoploss.call_count == 1 assert trade.stoploss_order_id == "13434334" # Fourth case: when stoploss is set and it is hit @@ -1132,7 +1132,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert trade.is_open is False mocker.patch( - 'freqtrade.exchange.Exchange.stoploss_limit', + 'freqtrade.exchange.Exchange.stoploss', side_effect=DependencyException() ) freqtrade.handle_stoploss_on_exchange(trade) @@ -1142,11 +1142,11 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # Fifth case: get_order returns InvalidOrder # It should try to add stoploss order trade.stoploss_order_id = 100 - stoploss_limit.reset_mock() + stoploss.reset_mock() mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) + mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) - assert stoploss_limit.call_count == 1 + assert stoploss.call_count == 1 def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, @@ -1165,7 +1165,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, get_order=MagicMock(return_value={'status': 'canceled'}), - stoploss_limit=MagicMock(side_effect=DependencyException()), + stoploss=MagicMock(side_effect=DependencyException()), ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1199,7 +1199,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, sell=sell_mock, get_fee=fee, get_order=MagicMock(return_value={'status': 'canceled'}), - stoploss_limit=MagicMock(side_effect=InvalidOrderException()), + stoploss=MagicMock(side_effect=InvalidOrderException()), ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1229,7 +1229,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set - stoploss_limit = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1241,7 +1241,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - stoploss_limit=stoploss_limit + stoploss=stoploss ) # enabling TSL @@ -1296,7 +1296,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1322,7 +1322,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set - stoploss_limit = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) mocker.patch.multiple( @@ -1335,7 +1335,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - stoploss_limit=stoploss_limit + stoploss=stoploss ) # enabling TSL @@ -1375,12 +1375,12 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) # Still try to create order - assert stoploss_limit.call_count == 1 + assert stoploss.call_count == 1 # Fail creating stoploss order caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException()) + mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1390,7 +1390,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set - stoploss_limit = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) @@ -1406,7 +1406,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - stoploss_limit=stoploss_limit + stoploss=stoploss ) # enabling TSL @@ -1459,7 +1459,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) # price goes down 5% mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -2423,7 +2423,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke default_conf['exchange']['name'] = 'binance' rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) - stoploss_limit = MagicMock(return_value={ + stoploss = MagicMock(return_value={ 'id': 123, 'info': { 'foo': 'bar' @@ -2437,7 +2437,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - stoploss_limit=stoploss_limit, + stoploss=stoploss, cancel_order=cancel_order, ) @@ -2482,14 +2482,14 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f price_to_precision=lambda s, x, y: y, ) - stoploss_limit = MagicMock(return_value={ + stoploss = MagicMock(return_value={ 'id': 123, 'info': { 'foo': 'bar' } }) - mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -2507,7 +2507,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f # Assuming stoploss on exchnage is hit # stoploss_order_id should become None # and trade should be sold at the price of stoploss - stoploss_limit_executed = MagicMock(return_value={ + stoploss_executed = MagicMock(return_value={ "id": "123", "timestamp": 1542707426845, "datetime": "2018-11-20T09:50:26.845Z", @@ -2525,7 +2525,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed) + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None diff --git a/tests/test_integration.py b/tests/test_integration.py index 9cb071bb8..c40da7e9d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -20,7 +20,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, default_conf['max_open_trades'] = 3 default_conf['exchange']['name'] = 'binance' - stoploss_limit = { + stoploss = { 'id': 123, 'info': {} } @@ -53,7 +53,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] ) cancel_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, From e6f1912443fa4a1229ac53ee2f1af3f5be3804ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 14:07:59 +0100 Subject: [PATCH 05/10] Use named arguments for stoploss create_order call --- freqtrade/exchange/binance.py | 4 ++-- tests/exchange/test_binance.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d08726cf0..8a3e28379 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -64,8 +64,8 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order(pair, ordertype, 'sell', - amount, rate, params) + order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + amount=amount, price=stop_price, params=params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) return order diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index fdf3d7435..a1b24913e 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -9,7 +9,7 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException, from tests.conftest import get_patched_exchange -def test_stoploss_limit_order(default_conf, mocker): +def test_stoploss_order_binance(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'stop_loss_limit' @@ -38,11 +38,12 @@ def test_stoploss_limit_order(default_conf, mocker): 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][5] == {'stopPrice': 220} + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == order_type + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220} # test exception handling with pytest.raises(DependencyException): @@ -67,7 +68,7 @@ def test_stoploss_limit_order(default_conf, mocker): exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) -def test_stoploss_limit_order_dry_run(default_conf, mocker): +def test_stoploss_order_dry_run_binance(default_conf, mocker): api_mock = MagicMock() order_type = 'stop_loss_limit' default_conf['dry_run'] = True From f1629c907a3ea88d08c2976223d25c2ce82e56f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 14:08:47 +0100 Subject: [PATCH 06/10] Implement stoploss for kraken --- freqtrade/exchange/kraken.py | 45 +++++++++++++++++- tests/exchange/test_kraken.py | 87 +++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 9bcd9cc1f..88c414772 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,7 +4,8 @@ from typing import Dict import ccxt -from freqtrade.exceptions import OperationalException, TemporaryError +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.exchange import retrier @@ -15,6 +16,7 @@ class Kraken(Exchange): _params: Dict = {"trading_agreement": "agree"} _ft_has: Dict = { + "stoploss_on_exchange": True, "trades_pagination": "id", "trades_pagination_arg": "since", } @@ -48,3 +50,44 @@ class Kraken(Exchange): f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + """ + Creates a stoploss market order. + Stoploss market orders is the only stoploss type supported by kraken. + """ + + ordertype = "stop-loss" + + stop_price = self.price_to_precision(pair, stop_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() + + amount = self.amount_to_precision(pair, amount) + + order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + amount=amount, price=stop_price, params=params) + logger.info('stoploss order added for %s. ' + 'stop price: %s.', pair, stop_price) + return order + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' + f'Message: {e}') from e + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Could not create {ordertype} sell order on market {pair}. ' + f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' + 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 diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 8490ee1a2..241d15772 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -3,6 +3,11 @@ from random import randint from unittest.mock import MagicMock +import ccxt +import pytest + +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -149,3 +154,85 @@ def test_get_balances_prod(default_conf, mocker): assert balances['4ST']['used'] == 0.0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "get_balances", "fetch_balance") + + +def test_stoploss_order_kraken(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop-loss' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') + + # stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + assert api_mock.create_order.call_count == 1 + + api_mock.create_order.reset_mock() + + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == order_type + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'} + + # 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, 'kraken') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(InvalidOrderException): + api_mock.create_order = MagicMock( + side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(TemporaryError): + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + 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, 'kraken') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + +def test_stoploss_order_dry_run_kraken(default_conf, mocker): + api_mock = MagicMock() + order_type = 'stop-loss' + default_conf['dry_run'] = True + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') + + api_mock.create_order.reset_mock() + + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + 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 From 7a22aaa11144dc95cb1f855642fdac555d866537 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 14:30:09 +0100 Subject: [PATCH 07/10] UPdate documentation to reflect that stoploss-on-exchange is also available for kraken --- docs/configuration.md | 2 +- docs/exchanges.md | 5 ++++- docs/stoploss.md | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index fe692eacb..f2d0fa5f2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -278,7 +278,7 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and The below is the default which is used if this is not configured in either strategy or configuration file. Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. -`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%. +`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). Calculation example: we bought the asset at 100$. Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. diff --git a/docs/exchanges.md b/docs/exchanges.md index 76fa81f4a..18a9f1cba 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -5,7 +5,7 @@ This page combines common gotchas and informations which are exchange-specific a ## Binance !!! Tip "Stoploss on Exchange" - Binance is currently the only exchange supporting `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it. + Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. ### Blacklists @@ -22,6 +22,9 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken +!!! Tip "Stoploss on Exchange" + Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled. + ### Historic Kraken data The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting. diff --git a/docs/stoploss.md b/docs/stoploss.md index 105488296..f6d56fd41 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. !!! Note - Stoploss on exchange is only supported for Binance as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now. ## Static Stop Loss From cf9331919fdf658ee86a0b21642c8c717cf0d1b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 19:54:30 +0100 Subject: [PATCH 08/10] move exchange-specific order-parsing to exchange class Related to stoploss_on_exchange in combination with trailing stoploss. Binance contains stopPrice in the info, while kraken returns the same value as "price". --- freqtrade/exchange/binance.py | 7 +++++++ freqtrade/exchange/exchange.py | 13 ++++++++++--- freqtrade/exchange/kraken.py | 7 +++++++ freqtrade/freqtradebot.py | 3 +-- tests/exchange/test_binance.py | 14 ++++++++++++++ tests/exchange/test_exchange.py | 3 +++ tests/exchange/test_kraken.py | 13 +++++++++++++ 7 files changed, 55 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8a3e28379..45102359d 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -32,6 +32,13 @@ class Binance(Exchange): return super().get_order_book(pair, limit) + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss limit order. diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bef92750c..a8df4c1bb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -282,8 +282,8 @@ class Exchange: quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( - f"{stake_currency} is not available as stake on {self.name}. " - f"Available currencies are: {', '.join(quote_currencies)}") + f"{stake_currency} is not available as stake on {self.name}. " + f"Available currencies are: {', '.join(quote_currencies)}") def validate_pairs(self, pairs: List[str]) -> None: """ @@ -460,7 +460,7 @@ class Exchange: "status": "closed", "filled": closed_order["amount"], "remaining": 0 - }) + }) if closed_order["type"] in ["stop_loss_limit"]: closed_order["info"].update({"stopPrice": closed_order["price"]}) self._dry_run_open_orders[closed_order["id"]] = closed_order @@ -519,6 +519,13 @@ class Exchange: return self.create_order(pair, ordertype, 'sell', amount, rate, params) + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + raise OperationalException(f"stoploss is not implemented for {self.name}.") + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss order. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 88c414772..243f1a6d6 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -51,6 +51,13 @@ class Kraken(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss market order. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a4b0ab806..fa9a8424a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -718,8 +718,7 @@ class FreqtradeBot: :param order: Current on exchange stoploss order :return: None """ - - if trade.stop_loss > float(order['info']['stopPrice']): + if self.exchange.stoploss_adjust(trade.stop_loss, order): # we check if the update is neccesary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index a1b24913e..e4599dcd7 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -92,3 +92,17 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): assert order['type'] == order_type assert order['price'] == 220 assert order['amount'] == 1 + + +def test_stoploss_adjust_binance(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='binance') + order = { + 'type': 'stop_loss_limit', + 'price': 1500, + 'info': {'stopPrice': 1500}, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case + order['type'] = 'stop_loss' + assert not exchange.stoploss_adjust(1501, order) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 680e69764..3a664a9ec 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1763,6 +1763,9 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): + exchange.stoploss_adjust(1, {}) + def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 241d15772..d63dd66cc 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -236,3 +236,16 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): assert order['type'] == order_type assert order['price'] == 220 assert order['amount'] == 1 + + +def test_stoploss_adjust_kraken(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='kraken') + order = { + 'type': 'stop-loss', + 'price': 1500, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case ... + order['type'] = 'stop_loss_limit' + assert not exchange.stoploss_adjust(1501, order) From 10d9db72a851769c1996f132e170588b4f708ee2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jan 2020 20:06:04 +0100 Subject: [PATCH 09/10] Adjust tests slightly --- tests/test_freqtradebot.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a33d47f34..48bd2deb5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1241,7 +1241,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - stoploss=stoploss + stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), ) # enabling TSL @@ -1335,7 +1336,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - stoploss=stoploss + stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), ) # enabling TSL @@ -1396,6 +1398,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, patch_edge(mocker) edge_conf['max_open_trades'] = float('inf') edge_conf['dry_run_wallet'] = 999.9 + edge_conf['exchange']['name'] = 'binance' mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ @@ -1406,7 +1409,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - stoploss=stoploss + stoploss=stoploss, ) # enabling TSL @@ -1459,7 +1462,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # price goes down 5% mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ From d64751687b50e97d3b31d1262f7aeef49fc7aab7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Feb 2020 10:47:44 +0100 Subject: [PATCH 10/10] Fix link and lowercase variable --- docs/exchanges.md | 2 +- freqtrade/exchange/binance.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 18a9f1cba..3c861ce44 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled. + Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled. ### Historic Kraken data diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 45102359d..875628af9 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -46,8 +46,8 @@ class Binance(Exchange): It may work with a limited number of other exchanges, but this has not been tested yet. """ # Limit price threshold: As limit price should always be below stop-price - LIMIT_PRICE_PCT = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - rate = stop_price * LIMIT_PRICE_PCT + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + rate = stop_price * limit_price_pct ordertype = "stop_loss_limit"