Merge pull request #1474 from mishaker/tsl_on_exchange
Making trailing stoploss compatible with stoploss on exchange
This commit is contained in:
@@ -112,7 +112,8 @@ CONF_SCHEMA = {
|
||||
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss_on_exchange': {'type': 'boolean'}
|
||||
'stoploss_on_exchange': {'type': 'boolean'},
|
||||
'stoploss_on_exchange_interval': {'type': 'boolean'}
|
||||
},
|
||||
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||
},
|
||||
@@ -137,7 +138,7 @@ CONF_SCHEMA = {
|
||||
'pairlist': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
|
||||
'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS},
|
||||
'config': {'type': 'object'}
|
||||
},
|
||||
'required': ['method']
|
||||
|
@@ -402,8 +402,11 @@ class Exchange(object):
|
||||
return self._dry_run_open_orders[order_id]
|
||||
|
||||
try:
|
||||
return self._api.create_order(pair, 'stop_loss_limit', 'sell',
|
||||
amount, rate, {'stopPrice': stop_price})
|
||||
order = self._api.create_order(pair, 'stop_loss_limit', 'sell',
|
||||
amount, rate, {'stopPrice': stop_price})
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s' % (pair, stop_price, rate))
|
||||
return order
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
|
@@ -613,7 +613,7 @@ class FreqtradeBot(object):
|
||||
def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
|
||||
"""
|
||||
Check if trade is fulfilled in which case the stoploss
|
||||
on exchange should be added immediately if stoploss on exchnage
|
||||
on exchange should be added immediately if stoploss on exchange
|
||||
is enabled.
|
||||
"""
|
||||
|
||||
@@ -630,13 +630,14 @@ class FreqtradeBot(object):
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
|
||||
# limit price should be less than stop price.
|
||||
# 0.98 is arbitrary here.
|
||||
limit_price = stop_price * 0.98
|
||||
# 0.99 is arbitrary here.
|
||||
limit_price = stop_price * 0.99
|
||||
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
trade.stoploss_last_update = datetime.now()
|
||||
|
||||
# Or the trade open and there is already a stoploss on exchange.
|
||||
# so we check if it is hit ...
|
||||
@@ -647,10 +648,38 @@ class FreqtradeBot(object):
|
||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||
trade.update(order)
|
||||
result = True
|
||||
else:
|
||||
result = False
|
||||
elif self.config.get('trailing_stop', False):
|
||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||
# in which case we cancel stoploss order and put another one with new
|
||||
# value immediately
|
||||
self.handle_trailing_stoploss_on_exchange(trade, order)
|
||||
|
||||
return result
|
||||
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):
|
||||
"""
|
||||
Check to see if stoploss on exchange should be updated
|
||||
in case of trailing stoploss on exchange
|
||||
:param Trade: Corresponding Trade
|
||||
:param order: Current on exchange stoploss order
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if trade.stop_loss > float(order['info']['stopPrice']):
|
||||
# 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:
|
||||
# cancelling the current stoploss on exchange first
|
||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange '
|
||||
'in order to add another one ...')
|
||||
if self.exchange.cancel_order(order['id'], trade.pair):
|
||||
# creating the new one
|
||||
stoploss_order_id = self.exchange.stoploss_limit(
|
||||
pair=trade.pair, amount=trade.amount,
|
||||
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
|
||||
)['id']
|
||||
trade.stoploss_order_id = str(stoploss_order_id)
|
||||
|
||||
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||
if self.edge:
|
||||
stoploss = self.edge.stoploss(trade.pair)
|
||||
|
@@ -83,7 +83,7 @@ def check_migrate(engine) -> None:
|
||||
logger.debug(f'trying {table_back_name}')
|
||||
|
||||
# Check for latest column
|
||||
if not has_column(cols, 'stoploss_order_id'):
|
||||
if not has_column(cols, 'stoploss_last_update'):
|
||||
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||
|
||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||
@@ -93,6 +93,7 @@ def check_migrate(engine) -> None:
|
||||
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
|
||||
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
|
||||
stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
|
||||
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
|
||||
max_rate = get_column_def(cols, 'max_rate', '0.0')
|
||||
sell_reason = get_column_def(cols, 'sell_reason', 'null')
|
||||
strategy = get_column_def(cols, 'strategy', 'null')
|
||||
@@ -111,7 +112,8 @@ def check_migrate(engine) -> None:
|
||||
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
|
||||
open_rate_requested, close_rate, close_rate_requested, close_profit,
|
||||
stake_amount, amount, open_date, close_date, open_order_id,
|
||||
stop_loss, initial_stop_loss, stoploss_order_id, max_rate, sell_reason, strategy,
|
||||
stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update,
|
||||
max_rate, sell_reason, strategy,
|
||||
ticker_interval
|
||||
)
|
||||
select id, lower(exchange),
|
||||
@@ -127,9 +129,9 @@ def check_migrate(engine) -> None:
|
||||
{close_rate_requested} close_rate_requested, close_profit,
|
||||
stake_amount, amount, open_date, close_date, open_order_id,
|
||||
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
|
||||
{stoploss_order_id} stoploss_order_id, {max_rate} max_rate,
|
||||
{sell_reason} sell_reason, {strategy} strategy,
|
||||
{ticker_interval} ticker_interval
|
||||
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
|
||||
{max_rate} max_rate, {sell_reason} sell_reason,
|
||||
{strategy} strategy, {ticker_interval} ticker_interval
|
||||
from {table_back_name}
|
||||
""")
|
||||
|
||||
@@ -185,6 +187,8 @@ class Trade(_DECL_BASE):
|
||||
initial_stop_loss = Column(Float, nullable=True, default=0.0)
|
||||
# stoploss order id which is on exchange
|
||||
stoploss_order_id = Column(String, nullable=True, index=True)
|
||||
# last update time of the stoploss order on exchange
|
||||
stoploss_last_update = Column(DateTime, nullable=True)
|
||||
# absolute value of the highest reached price
|
||||
max_rate = Column(Float, nullable=True, default=0.0)
|
||||
sell_reason = Column(String, nullable=True)
|
||||
@@ -218,11 +222,13 @@ class Trade(_DECL_BASE):
|
||||
logger.debug("assigning new stop loss")
|
||||
self.stop_loss = new_loss
|
||||
self.initial_stop_loss = new_loss
|
||||
self.stoploss_last_update = datetime.utcnow()
|
||||
|
||||
# evaluate if the stop loss needs to be updated
|
||||
else:
|
||||
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||
self.stop_loss = new_loss
|
||||
self.stoploss_last_update = datetime.utcnow()
|
||||
logger.debug("adjusted stop loss")
|
||||
else:
|
||||
logger.debug("keeping current stop loss")
|
||||
|
@@ -80,7 +80,8 @@ class IStrategy(ABC):
|
||||
'buy': 'limit',
|
||||
'sell': 'limit',
|
||||
'stoploss': 'limit',
|
||||
'stoploss_on_exchange': False
|
||||
'stoploss_on_exchange': False,
|
||||
'stoploss_on_exchange_interval': 60,
|
||||
}
|
||||
|
||||
# Optional time in force
|
||||
@@ -233,12 +234,9 @@ class IStrategy(ABC):
|
||||
current_rate = low or rate
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
|
||||
if self.order_types.get('stoploss_on_exchange'):
|
||||
stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
||||
else:
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
force_stoploss=force_stoploss)
|
||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||
current_time=date, current_profit=current_profit,
|
||||
force_stoploss=force_stoploss)
|
||||
|
||||
if stoplossflag.sell_flag:
|
||||
return stoplossflag
|
||||
@@ -276,12 +274,13 @@ class IStrategy(ABC):
|
||||
"""
|
||||
|
||||
trailing_stop = self.config.get('trailing_stop', False)
|
||||
|
||||
trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss
|
||||
else self.stoploss, initial=True)
|
||||
|
||||
# evaluate if the stoploss was hit
|
||||
if self.stoploss is not None and trade.stop_loss >= current_rate:
|
||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||
if ((self.stoploss is not None) and
|
||||
(trade.stop_loss >= current_rate) and
|
||||
(not self.order_types.get('stoploss_on_exchange'))):
|
||||
selltype = SellType.STOP_LOSS
|
||||
# If Trailing stop (and max-rate did move above open rate)
|
||||
if trailing_stop and trade.open_rate != trade.max_rate:
|
||||
@@ -301,7 +300,8 @@ class IStrategy(ABC):
|
||||
|
||||
# check if we have a special stop loss for positive condition
|
||||
# and if profit is positive
|
||||
stop_loss_value = self.stoploss
|
||||
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
|
||||
|
||||
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
|
||||
|
||||
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
|
||||
|
@@ -1014,6 +1014,211 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
assert trade.is_open is False
|
||||
|
||||
|
||||
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||
markets, limit_buy_order, limit_sell_order) -> None:
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00001172,
|
||||
'ask': 0.00001173,
|
||||
'last': 0.00001172
|
||||
}),
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
get_markets=markets,
|
||||
stoploss_limit=stoploss_limit
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
default_conf['trailing_stop'] = True
|
||||
|
||||
# disabling ROI
|
||||
default_conf['minimal_roi']['0'] = 999999999
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
|
||||
# enabling stoploss on exchange
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
|
||||
# setting stoploss
|
||||
freqtrade.strategy.stoploss = -0.05
|
||||
|
||||
# setting stoploss_on_exchange_interval to 60 seconds
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
|
||||
|
||||
patch_get_signal(freqtrade)
|
||||
|
||||
freqtrade.create_trade()
|
||||
trade = Trade.query.first()
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = 100
|
||||
|
||||
stoploss_order_hanging = MagicMock(return_value={
|
||||
'id': 100,
|
||||
'status': 'open',
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'info': {
|
||||
'stopPrice': '0.000011134'
|
||||
}
|
||||
})
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
||||
|
||||
# stoploss initially at 5%
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
|
||||
# price jumped 2x
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
|
||||
'bid': 0.00002344,
|
||||
'ask': 0.00002346,
|
||||
'last': 0.00002344
|
||||
}))
|
||||
|
||||
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)
|
||||
|
||||
# stoploss should not be updated as the interval is 60 seconds
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
cancel_order_mock.assert_not_called()
|
||||
stoploss_order_mock.assert_not_called()
|
||||
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert trade.stop_loss == 0.00002344 * 0.95
|
||||
|
||||
# setting stoploss_on_exchange_interval to 0 seconds
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
|
||||
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
|
||||
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,
|
||||
stop_price=0.00002344 * 0.95)
|
||||
|
||||
|
||||
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
markets, limit_buy_order, limit_sell_order) -> None:
|
||||
|
||||
# When trailing stoploss is set
|
||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
patch_edge(mocker)
|
||||
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00001172,
|
||||
'ask': 0.00001173,
|
||||
'last': 0.00001172
|
||||
}),
|
||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
get_markets=markets,
|
||||
stoploss_limit=stoploss_limit
|
||||
)
|
||||
|
||||
# enabling TSL
|
||||
edge_conf['trailing_stop'] = True
|
||||
edge_conf['trailing_stop_positive'] = 0.01
|
||||
edge_conf['trailing_stop_positive_offset'] = 0.011
|
||||
|
||||
# disabling ROI
|
||||
edge_conf['minimal_roi']['0'] = 999999999
|
||||
|
||||
freqtrade = FreqtradeBot(edge_conf)
|
||||
|
||||
# enabling stoploss on exchange
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
|
||||
# setting stoploss
|
||||
freqtrade.strategy.stoploss = -0.02
|
||||
|
||||
# setting stoploss_on_exchange_interval to 0 second
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
|
||||
|
||||
patch_get_signal(freqtrade)
|
||||
|
||||
freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
|
||||
|
||||
freqtrade.create_trade()
|
||||
trade = Trade.query.first()
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.stoploss_order_id = 100
|
||||
|
||||
stoploss_order_hanging = MagicMock(return_value={
|
||||
'id': 100,
|
||||
'status': 'open',
|
||||
'type': 'stop_loss_limit',
|
||||
'price': 3,
|
||||
'average': 2,
|
||||
'info': {
|
||||
'stopPrice': '0.000009384'
|
||||
}
|
||||
})
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
|
||||
|
||||
# stoploss initially at 20% as edge dictated it.
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
assert trade.stop_loss == 0.000009384
|
||||
|
||||
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)
|
||||
|
||||
# price goes down 5%
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
|
||||
'bid': 0.00001172 * 0.95,
|
||||
'ask': 0.00001173 * 0.95,
|
||||
'last': 0.00001172 * 0.95
|
||||
}))
|
||||
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
|
||||
# stoploss should remain the same
|
||||
assert trade.stop_loss == 0.000009384
|
||||
|
||||
# stoploss on exchange should not be canceled
|
||||
cancel_order_mock.assert_not_called()
|
||||
|
||||
# price jumped 2x
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={
|
||||
'bid': 0.00002344,
|
||||
'ask': 0.00002346,
|
||||
'last': 0.00002344
|
||||
}))
|
||||
|
||||
assert freqtrade.handle_trade(trade) is False
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
|
||||
# stoploss should be set to 1% as trailing is on
|
||||
assert trade.stop_loss == 0.00002344 * 0.99
|
||||
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,
|
||||
stop_price=0.00002344 * 0.99)
|
||||
|
||||
|
||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
|
||||
|
@@ -516,6 +516,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||
assert trade.strategy is None
|
||||
assert trade.ticker_interval is None
|
||||
assert trade.stoploss_order_id is None
|
||||
assert trade.stoploss_last_update is None
|
||||
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||
assert log_has("Running database migration - backup available as trades_bak2",
|
||||
|
Reference in New Issue
Block a user