Merge pull request #2779 from freqtrade/stoploss_market

Stoploss on exchange for Kraken
This commit is contained in:
hroff-1902 2020-02-02 14:48:45 +03:00 committed by GitHub
commit 6594679e52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 264 additions and 75 deletions

View File

@ -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. 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. 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$. 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$. Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.

View File

@ -5,7 +5,7 @@ This page combines common gotchas and informations which are exchange-specific a
## Binance ## Binance
!!! Tip "Stoploss on Exchange" !!! 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 ### Blacklists
@ -22,6 +22,9 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
## Kraken ## 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/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 ### 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. 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.

View File

@ -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. This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
!!! Note !!! 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 ## Static Stop Loss

View File

@ -32,13 +32,23 @@ class Binance(Exchange):
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: 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. creates a stoploss limit order.
this stoploss-limit is binance-specific. this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet. 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" ordertype = "stop_loss_limit"
stop_price = self.price_to_precision(pair, stop_price) stop_price = self.price_to_precision(pair, stop_price)
@ -61,8 +71,8 @@ class Binance(Exchange):
rate = self.price_to_precision(pair, rate) rate = self.price_to_precision(pair, rate)
order = self._api.create_order(pair, ordertype, 'sell', order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount, rate, params) amount=amount, price=stop_price, params=params)
logger.info('stoploss limit order added for %s. ' logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate) 'stop price: %s. limit: %s', pair, stop_price, rate)
return order return order

View File

@ -282,8 +282,8 @@ class Exchange:
quote_currencies = self.get_quote_currencies() quote_currencies = self.get_quote_currencies()
if stake_currency not in quote_currencies: if stake_currency not in quote_currencies:
raise OperationalException( raise OperationalException(
f"{stake_currency} is not available as stake on {self.name}. " f"{stake_currency} is not available as stake on {self.name}. "
f"Available currencies are: {', '.join(quote_currencies)}") f"Available currencies are: {', '.join(quote_currencies)}")
def validate_pairs(self, pairs: List[str]) -> None: def validate_pairs(self, pairs: List[str]) -> None:
""" """
@ -460,7 +460,7 @@ class Exchange:
"status": "closed", "status": "closed",
"filled": closed_order["amount"], "filled": closed_order["amount"],
"remaining": 0 "remaining": 0
}) })
if closed_order["type"] in ["stop_loss_limit"]: if closed_order["type"] in ["stop_loss_limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]}) closed_order["info"].update({"stopPrice": closed_order["price"]})
self._dry_run_open_orders[closed_order["id"]] = closed_order self._dry_run_open_orders[closed_order["id"]] = closed_order
@ -519,9 +519,17 @@ class Exchange:
return self.create_order(pair, ordertype, 'sell', amount, rate, params) 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_adjust(self, stop_loss: float, order: Dict) -> bool:
""" """
creates a stoploss limit order. 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.
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 Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
exchange's subclass. exchange's subclass.
The exception below should never raise, since we disallow The exception below should never raise, since we disallow
@ -529,7 +537,7 @@ class Exchange:
Note: Changes to this interface need to be applied to all sub-classes too. 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 @retrier
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:

View File

@ -4,7 +4,8 @@ from typing import Dict
import ccxt import ccxt
from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange import retrier from freqtrade.exchange.exchange import retrier
@ -15,6 +16,7 @@ class Kraken(Exchange):
_params: Dict = {"trading_agreement": "agree"} _params: Dict = {"trading_agreement": "agree"}
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True,
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
} }
@ -48,3 +50,51 @@ class Kraken(Exchange):
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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.
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

View File

@ -658,13 +658,10 @@ class FreqtradeBot:
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. 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. :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: try:
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount, stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
stop_price=stop_price, stop_price=stop_price,
rate=rate * LIMIT_PRICE_PCT) order_types=self.strategy.order_types)
trade.stoploss_order_id = str(stoploss_order['id']) trade.stoploss_order_id = str(stoploss_order['id'])
return True return True
except InvalidOrderException as e: except InvalidOrderException as e:
@ -743,8 +740,7 @@ class FreqtradeBot:
:param order: Current on exchange stoploss order :param order: Current on exchange stoploss order
:return: None :return: None
""" """
if self.exchange.stoploss_adjust(trade.stop_loss, order):
if trade.stop_loss > float(order['info']['stopPrice']):
# we check if the update is neccesary # we check if the update is neccesary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:

View File

@ -9,7 +9,7 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
from tests.conftest import get_patched_exchange 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() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit' order_type = 'stop_loss_limit'
@ -28,46 +28,47 @@ def test_stoploss_limit_order(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) 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() api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
assert order['id'] == order_id assert order['id'] == order_id
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell' assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
assert api_mock.create_order.call_args[0][4] == 200 assert api_mock.create_order.call_args_list[0][1]['price'] == 220
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220} assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220}
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') 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(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock( api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') 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(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') 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(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(OperationalException, match=r".*DeadBeef.*"): with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') 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(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() api_mock = MagicMock()
order_type = 'stop_loss_limit' order_type = 'stop_loss_limit'
default_conf['dry_run'] = True default_conf['dry_run'] = True
@ -77,11 +78,12 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200) 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() api_mock.create_order.reset_mock()
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -90,3 +92,17 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
assert order['type'] == order_type assert order['type'] == order_type
assert order['price'] == 220 assert order['price'] == 220
assert order['amount'] == 1 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)

View File

@ -1758,10 +1758,13 @@ def test_get_fee(default_conf, mocker, exchange_name):
'get_fee', 'calculate_fee', symbol="ETH/BTC") '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') 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_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) 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): def test_merge_ft_has_dict(default_conf, mocker):

View File

@ -3,6 +3,11 @@
from random import randint from random import randint
from unittest.mock import MagicMock 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.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
@ -149,3 +154,98 @@ def test_get_balances_prod(default_conf, mocker):
assert balances['4ST']['used'] == 0.0 assert balances['4ST']['used'] == 0.0
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"get_balances", "fetch_balance") "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
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)

View File

@ -1023,8 +1023,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_buy_order['amount']) return_value=limit_buy_order['amount'])
stoploss_limit = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@ -1037,13 +1037,13 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
freqtrade.exit_positions(trades) freqtrade.exit_positions(trades)
assert trade.stoploss_order_id == '13434334' assert trade.stoploss_order_id == '13434334'
assert stoploss_limit.call_count == 1 assert stoploss.call_count == 1
assert trade.is_open is True assert trade.is_open is True
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
stoploss_limit = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -1056,7 +1056,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee, get_fee=fee,
stoploss_limit=stoploss_limit stoploss=stoploss
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
@ -1070,7 +1070,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
trade.stoploss_order_id = None trade.stoploss_order_id = None
assert freqtrade.handle_stoploss_on_exchange(trade) is False 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" assert trade.stoploss_order_id == "13434334"
# Second case: when stoploss is set but it is not yet hit # Second case: when stoploss is set but it is not yet hit
@ -1094,10 +1094,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order) 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 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" assert trade.stoploss_order_id == "13434334"
# Fourth case: when stoploss is set and it is hit # Fourth case: when stoploss is set and it is hit
@ -1124,7 +1124,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
assert trade.is_open is False assert trade.is_open is False
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.stoploss_limit', 'freqtrade.exchange.Exchange.stoploss',
side_effect=DependencyException() side_effect=DependencyException()
) )
freqtrade.handle_stoploss_on_exchange(trade) freqtrade.handle_stoploss_on_exchange(trade)
@ -1134,11 +1134,11 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
# Fifth case: get_order returns InvalidOrder # Fifth case: get_order returns InvalidOrder
# It should try to add stoploss order # It should try to add stoploss order
trade.stoploss_order_id = 100 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.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) 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, def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
@ -1157,7 +1157,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
sell=MagicMock(return_value={'id': limit_sell_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee, get_fee=fee,
get_order=MagicMock(return_value={'status': 'canceled'}), get_order=MagicMock(return_value={'status': 'canceled'}),
stoploss_limit=MagicMock(side_effect=DependencyException()), stoploss=MagicMock(side_effect=DependencyException()),
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
@ -1191,7 +1191,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
sell=sell_mock, sell=sell_mock,
get_fee=fee, get_fee=fee,
get_order=MagicMock(return_value={'status': 'canceled'}), get_order=MagicMock(return_value={'status': 'canceled'}),
stoploss_limit=MagicMock(side_effect=InvalidOrderException()), stoploss=MagicMock(side_effect=InvalidOrderException()),
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
@ -1221,7 +1221,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, def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set # When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker) patch_RPCManager(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -1233,7 +1233,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee, get_fee=fee,
stoploss_limit=stoploss_limit stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
) )
# enabling TSL # enabling TSL
@ -1288,7 +1289,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock() stoploss_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) 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 # stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -1307,7 +1308,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828, stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
pair='ETH/BTC', pair='ETH/BTC',
rate=0.00002344 * 0.95 * 0.99, order_types=freqtrade.strategy.order_types,
stop_price=0.00002344 * 0.95) stop_price=0.00002344 * 0.95)
# price fell below stoploss, so dry-run sells trade. # price fell below stoploss, so dry-run sells trade.
@ -1322,7 +1323,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, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set # When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -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']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee, get_fee=fee,
stoploss_limit=stoploss_limit stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
) )
# enabling TSL # enabling TSL
@ -1375,12 +1377,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) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
# Still try to create order # Still try to create order
assert stoploss_limit.call_count == 1 assert stoploss.call_count == 1
# Fail creating stoploss order # Fail creating stoploss order
caplog.clear() caplog.clear()
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock()) 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) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1 assert cancel_mock.call_count == 1
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
@ -1390,12 +1392,13 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set # When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
patch_edge(mocker) patch_edge(mocker)
edge_conf['max_open_trades'] = float('inf') edge_conf['max_open_trades'] = float('inf')
edge_conf['dry_run_wallet'] = 999.9 edge_conf['dry_run_wallet'] = 999.9
edge_conf['exchange']['name'] = 'binance'
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={ 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']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee, get_fee=fee,
stoploss_limit=stoploss_limit stoploss=stoploss,
) )
# enabling TSL # enabling TSL
@ -1459,7 +1462,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock() stoploss_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
# price goes down 5% # price goes down 5%
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
@ -1492,7 +1495,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954, stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
pair='NEO/BTC', pair='NEO/BTC',
rate=0.00002344 * 0.99 * 0.99, order_types=freqtrade.strategy.order_types,
stop_price=0.00002344 * 0.99) stop_price=0.00002344 * 0.99)
@ -2423,7 +2426,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
stoploss_limit = MagicMock(return_value={ stoploss = MagicMock(return_value={
'id': 123, 'id': 123,
'info': { 'info': {
'foo': 'bar' 'foo': 'bar'
@ -2437,7 +2440,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
get_fee=fee, get_fee=fee,
amount_to_precision=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
price_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, cancel_order=cancel_order,
) )
@ -2482,14 +2485,14 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
price_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
) )
stoploss_limit = MagicMock(return_value={ stoploss = MagicMock(return_value={
'id': 123, 'id': 123,
'info': { 'info': {
'foo': 'bar' 'foo': 'bar'
} }
}) })
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@ -2507,7 +2510,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
# Assuming stoploss on exchnage is hit # Assuming stoploss on exchnage is hit
# stoploss_order_id should become None # stoploss_order_id should become None
# and trade should be sold at the price of stoploss # and trade should be sold at the price of stoploss
stoploss_limit_executed = MagicMock(return_value={ stoploss_executed = MagicMock(return_value={
"id": "123", "id": "123",
"timestamp": 1542707426845, "timestamp": 1542707426845,
"datetime": "2018-11-20T09:50:26.845Z", "datetime": "2018-11-20T09:50:26.845Z",
@ -2525,7 +2528,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
"fee": None, "fee": None,
"trades": 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) freqtrade.exit_positions(trades)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None

View File

@ -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['max_open_trades'] = 3
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
stoploss_limit = { stoploss = {
'id': 123, 'id': 123,
'info': {} '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)] SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
) )
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,