mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
8469484998
stoploss on exchange tests are quiet extensive, and deserve their own test file.
1335 lines
45 KiB
Python
1335 lines
45 KiB
Python
from copy import deepcopy
|
|
from datetime import timedelta
|
|
from unittest.mock import ANY, MagicMock
|
|
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
from freqtrade.enums import ExitCheckTuple, ExitType, RPCMessageType
|
|
from freqtrade.exceptions import ExchangeError, InsufficientFundsError, InvalidOrderException
|
|
from freqtrade.freqtradebot import FreqtradeBot
|
|
from freqtrade.persistence import Order, Trade
|
|
from freqtrade.persistence.models import PairLock
|
|
from freqtrade.util.datetime_helpers import dt_now
|
|
from tests.conftest import (EXMS, get_patched_freqtradebot, log_has, log_has_re, patch_edge,
|
|
patch_exchange, patch_get_signal, patch_whitelist)
|
|
from tests.conftest_trades import entry_side, exit_side
|
|
from tests.freqtradebot.test_freqtradebot import patch_RPCManager
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short, fee) -> None:
|
|
patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
|
|
get_fee=fee,
|
|
)
|
|
order = limit_order[entry_side(is_short)]
|
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
|
|
mocker.patch(f'{EXMS}.fetch_order', return_value=order)
|
|
mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
|
|
|
|
stoploss = MagicMock(return_value={'id': 13434334})
|
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
|
|
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
trade.is_open = True
|
|
trades = [trade]
|
|
|
|
freqtrade.exit_positions(trades)
|
|
assert trade.has_open_sl_orders is True
|
|
assert stoploss.call_count == 1
|
|
assert trade.is_open is True
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short,
|
|
limit_order) -> None:
|
|
stop_order_dict = {'id': "13434334"}
|
|
stoploss = MagicMock(return_value=stop_order_dict)
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
create_stoploss=stoploss
|
|
)
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
# First case: when stoploss is not yet set but the order is open
|
|
# should get the stoploss order id immediately
|
|
# and should return false as no trade actually happened
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
assert trade.is_short == is_short
|
|
assert trade.is_open
|
|
assert trade.has_open_sl_orders is False
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
assert stoploss.call_count == 1
|
|
assert trade.open_sl_orders[-1].order_id == "13434334"
|
|
|
|
# Second case: when stoploss is set but it is not yet hit
|
|
# should do nothing and return false
|
|
trade.is_open = True
|
|
|
|
hanging_stoploss_order = MagicMock(return_value={'id': '13434334', 'status': 'open'})
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order)
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
hanging_stoploss_order.assert_called_once_with('13434334', trade.pair)
|
|
assert len(trade.open_sl_orders) == 1
|
|
assert trade.open_sl_orders[-1].order_id == "13434334"
|
|
|
|
# Third case: when stoploss was set but it was canceled for some reason
|
|
# should set a stoploss immediately and return False
|
|
caplog.clear()
|
|
trade.is_open = True
|
|
|
|
canceled_stoploss_order = MagicMock(return_value={'id': '13434334', 'status': 'canceled'})
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order)
|
|
stoploss.reset_mock()
|
|
amount_before = trade.amount
|
|
|
|
stop_order_dict.update({'id': "103_1"})
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
assert stoploss.call_count == 1
|
|
assert len(trade.open_sl_orders) == 1
|
|
assert trade.open_sl_orders[-1].order_id == "103_1"
|
|
assert trade.amount == amount_before
|
|
|
|
# Fourth case: when stoploss is set and it is hit
|
|
# should return true as a trade actually happened
|
|
caplog.clear()
|
|
stop_order_dict.update({'id': "103_1"})
|
|
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
trade.is_open = True
|
|
|
|
stoploss_order_hit = MagicMock(return_value={
|
|
'id': "103_1",
|
|
'status': 'closed',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'filled': enter_order['amount'],
|
|
'remaining': 0,
|
|
'amount': enter_order['amount'],
|
|
})
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
|
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
|
assert len(trade.open_sl_orders) == 0
|
|
assert trade.is_open is False
|
|
caplog.clear()
|
|
|
|
mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError())
|
|
trade.is_open = True
|
|
freqtrade.handle_stoploss_on_exchange(trade)
|
|
assert log_has('Unable to place a stoploss order on exchange.', caplog)
|
|
assert len(trade.open_sl_orders) == 0
|
|
|
|
# Fifth case: fetch_order returns InvalidOrder
|
|
# It should try to add stoploss order
|
|
stop_order_dict.update({'id': "105"})
|
|
stoploss.reset_mock()
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException())
|
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
|
|
freqtrade.handle_stoploss_on_exchange(trade)
|
|
assert len(trade.open_sl_orders) == 1
|
|
assert stoploss.call_count == 1
|
|
|
|
# Sixth case: Closed Trade
|
|
# Should not create new order
|
|
trade.is_open = False
|
|
trade.open_sl_orders[-1].ft_is_open = False
|
|
stoploss.reset_mock()
|
|
mocker.patch(f'{EXMS}.fetch_order')
|
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
assert trade.has_open_sl_orders is False
|
|
assert stoploss.call_count == 0
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, is_short,
|
|
limit_order) -> None:
|
|
stop_order_dict = {'id': "13434334"}
|
|
stoploss = MagicMock(return_value=stop_order_dict)
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
create_stoploss=stoploss
|
|
)
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
assert trade.is_short == is_short
|
|
assert trade.is_open
|
|
assert trade.has_open_sl_orders is False
|
|
|
|
# emergency exit triggered
|
|
# Trailing stop should not act anymore
|
|
stoploss_order_cancelled = MagicMock(side_effect=[{
|
|
'id': "107",
|
|
'status': 'canceled',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'amount': enter_order['amount'],
|
|
'filled': 0,
|
|
'remaining': enter_order['amount'],
|
|
'info': {'stopPrice': 22},
|
|
}])
|
|
trade.stoploss_last_update = dt_now() - timedelta(hours=1)
|
|
trade.stop_loss = 24
|
|
trade.exit_reason = None
|
|
trade.orders.append(
|
|
Order(
|
|
ft_order_side='stoploss',
|
|
ft_pair=trade.pair,
|
|
ft_is_open=True,
|
|
ft_amount=trade.amount,
|
|
ft_price=trade.stop_loss,
|
|
order_id='107',
|
|
status='open',
|
|
)
|
|
)
|
|
freqtrade.config['trailing_stop'] = True
|
|
stoploss = MagicMock(side_effect=InvalidOrderException())
|
|
assert trade.has_open_sl_orders is True
|
|
Trade.commit()
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result',
|
|
side_effect=InvalidOrderException())
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled)
|
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
assert trade.has_open_sl_orders is False
|
|
assert trade.is_open is False
|
|
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_handle_stoploss_on_exchange_partial(
|
|
mocker, default_conf_usdt, fee, is_short, limit_order) -> None:
|
|
stop_order_dict = {'id': "101", "status": "open"}
|
|
stoploss = MagicMock(return_value=stop_order_dict)
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
create_stoploss=stoploss
|
|
)
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
trade.is_open = True
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
assert stoploss.call_count == 1
|
|
assert trade.has_open_sl_orders is True
|
|
assert trade.open_sl_orders[-1].order_id == "101"
|
|
assert trade.amount == 30
|
|
stop_order_dict.update({'id': "102"})
|
|
# Stoploss on exchange is cancelled on exchange, but filled partially.
|
|
# Must update trade amount to guarantee successful exit.
|
|
stoploss_order_hit = MagicMock(return_value={
|
|
'id': "101",
|
|
'status': 'canceled',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'filled': trade.amount / 2,
|
|
'remaining': trade.amount / 2,
|
|
'amount': enter_order['amount'],
|
|
})
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
# Stoploss filled partially ...
|
|
assert trade.amount == 15
|
|
|
|
assert trade.open_sl_orders[-1].order_id == "102"
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_handle_stoploss_on_exchange_partial_cancel_here(
|
|
mocker, default_conf_usdt, fee, is_short, limit_order, caplog, time_machine) -> None:
|
|
stop_order_dict = {'id': "101", "status": "open"}
|
|
time_machine.move_to(dt_now())
|
|
default_conf_usdt['trailing_stop'] = True
|
|
stoploss = MagicMock(return_value=stop_order_dict)
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
create_stoploss=stoploss
|
|
)
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
trade.is_open = True
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
assert stoploss.call_count == 1
|
|
assert trade.has_open_sl_orders is True
|
|
assert trade.open_sl_orders[-1].order_id == "101"
|
|
assert trade.amount == 30
|
|
stop_order_dict.update({'id': "102"})
|
|
# Stoploss on exchange is open.
|
|
# Freqtrade cancels the stop - but cancel returns a partial filled order.
|
|
stoploss_order_hit = MagicMock(return_value={
|
|
'id': "101",
|
|
'status': 'open',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'filled': 0,
|
|
'remaining': trade.amount,
|
|
'amount': enter_order['amount'],
|
|
})
|
|
stoploss_order_cancel = MagicMock(return_value={
|
|
'id': "101",
|
|
'status': 'canceled',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'filled': trade.amount / 2,
|
|
'remaining': trade.amount / 2,
|
|
'amount': enter_order['amount'],
|
|
})
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel)
|
|
time_machine.shift(timedelta(minutes=15))
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
# Canceled Stoploss filled partially ...
|
|
assert log_has_re('Cancelling current stoploss on exchange.*', caplog)
|
|
|
|
assert trade.has_open_sl_orders is True
|
|
assert trade.open_sl_orders[-1].order_id == "102"
|
|
assert trade.amount == 15
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
|
|
limit_order) -> None:
|
|
# Sixth case: stoploss order was cancelled but couldn't create new one
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': '100'}),
|
|
create_stoploss=MagicMock(side_effect=ExchangeError()),
|
|
)
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
assert trade.is_short == is_short
|
|
trade.is_open = True
|
|
trade.orders.append(
|
|
Order(
|
|
ft_order_side='stoploss',
|
|
ft_pair=trade.pair,
|
|
ft_is_open=True,
|
|
ft_amount=trade.amount,
|
|
ft_price=trade.stop_loss,
|
|
order_id='100',
|
|
status='open',
|
|
)
|
|
)
|
|
assert trade
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
assert log_has_re(r'All Stoploss orders are cancelled, but unable to recreate one\.', caplog)
|
|
assert trade.has_open_sl_orders is False
|
|
assert trade.is_open is True
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_create_stoploss_order_invalid_order(
|
|
mocker, default_conf_usdt, caplog, fee, is_short, limit_order
|
|
):
|
|
open_order = limit_order[entry_side(is_short)]
|
|
order = limit_order[exit_side(is_short)]
|
|
rpc_mock = patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
create_order_mock = MagicMock(side_effect=[
|
|
open_order,
|
|
order,
|
|
])
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=create_order_mock,
|
|
get_fee=fee,
|
|
)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_order=MagicMock(return_value={'status': 'canceled'}),
|
|
create_stoploss=MagicMock(side_effect=InvalidOrderException()),
|
|
)
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
caplog.clear()
|
|
rpc_mock.reset_mock()
|
|
freqtrade.create_stoploss_order(trade, 200)
|
|
assert trade.has_open_sl_orders is False
|
|
assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value
|
|
assert log_has("Unable to place a stoploss order on exchange. ", caplog)
|
|
assert log_has("Exiting the trade forcefully", caplog)
|
|
|
|
# Should call a market sell
|
|
assert create_order_mock.call_count == 2
|
|
assert create_order_mock.call_args[1]['ordertype'] == 'market'
|
|
assert create_order_mock.call_args[1]['pair'] == trade.pair
|
|
assert create_order_mock.call_args[1]['amount'] == trade.amount
|
|
|
|
# Rpc is sending first buy, then sell
|
|
assert rpc_mock.call_count == 2
|
|
assert rpc_mock.call_args_list[0][0][0]['exit_reason'] == ExitType.EMERGENCY_EXIT.value
|
|
assert rpc_mock.call_args_list[0][0][0]['order_type'] == 'market'
|
|
assert rpc_mock.call_args_list[0][0][0]['type'] == 'exit'
|
|
assert rpc_mock.call_args_list[1][0][0]['type'] == 'exit_fill'
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_create_stoploss_order_insufficient_funds(
|
|
mocker, default_conf_usdt, caplog, fee, limit_order, is_short
|
|
):
|
|
exit_order = limit_order[exit_side(is_short)]['id']
|
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
|
|
|
mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds')
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
limit_order[entry_side(is_short)],
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
fetch_order=MagicMock(return_value={'status': 'canceled'}),
|
|
)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
create_stoploss=MagicMock(side_effect=InsufficientFundsError()),
|
|
)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
caplog.clear()
|
|
freqtrade.create_stoploss_order(trade, 200)
|
|
# stoploss_orderid was empty before
|
|
assert trade.has_open_sl_orders is False
|
|
assert mock_insuf.call_count == 1
|
|
mock_insuf.reset_mock()
|
|
|
|
freqtrade.create_stoploss_order(trade, 200)
|
|
# No change to stoploss-orderid
|
|
assert trade.has_open_sl_orders is False
|
|
assert mock_insuf.call_count == 1
|
|
|
|
|
|
@pytest.mark.parametrize("is_short,bid,ask,stop_price,hang_price", [
|
|
(False, [4.38, 4.16], [4.4, 4.17], ['2.0805', 4.4 * 0.95], 3),
|
|
(True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.09 * 1.05], 1.5),
|
|
])
|
|
@pytest.mark.usefixtures("init_persistence")
|
|
def test_handle_stoploss_on_exchange_trailing(
|
|
mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, hang_price,
|
|
time_machine,
|
|
) -> None:
|
|
# When trailing stoploss is set
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
|
|
start_dt = dt_now()
|
|
time_machine.move_to(start_dt, tick=False)
|
|
patch_RPCManager(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 2.19,
|
|
'ask': 2.2,
|
|
'last': 2.19,
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
create_stoploss=stoploss,
|
|
stoploss_adjust=MagicMock(return_value=True),
|
|
)
|
|
|
|
# enabling TSL
|
|
default_conf_usdt['trailing_stop'] = True
|
|
|
|
# disabling ROI
|
|
default_conf_usdt['minimal_roi']['0'] = 999999999
|
|
|
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
|
|
|
# enabling stoploss on exchange
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
|
|
# setting stoploss
|
|
freqtrade.strategy.stoploss = 0.05 if is_short else -0.05
|
|
|
|
# setting stoploss_on_exchange_interval to 60 seconds
|
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
|
|
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
trade.is_open = True
|
|
assert trade.has_open_sl_orders is False
|
|
trade.stoploss_last_update = dt_now() - timedelta(minutes=20)
|
|
trade.orders.append(
|
|
Order(
|
|
ft_order_side='stoploss',
|
|
ft_pair=trade.pair,
|
|
ft_is_open=True,
|
|
ft_amount=trade.amount,
|
|
ft_price=trade.stop_loss,
|
|
order_id='100',
|
|
order_date=dt_now() - timedelta(minutes=20),
|
|
)
|
|
)
|
|
|
|
stoploss_order_hanging = {
|
|
'id': '100',
|
|
'status': 'open',
|
|
'type': 'stop_loss_limit',
|
|
'price': hang_price,
|
|
'average': 2,
|
|
'fee': {},
|
|
'amount': 0,
|
|
'info': {
|
|
'stopPrice': stop_price[0]
|
|
}
|
|
}
|
|
stoploss_order_cancel = deepcopy(stoploss_order_hanging)
|
|
stoploss_order_cancel['status'] = 'canceled'
|
|
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value=stoploss_order_hanging)
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=stoploss_order_cancel)
|
|
|
|
# stoploss initially at 5%
|
|
assert freqtrade.handle_trade(trade) is False
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
|
|
assert len(trade.open_sl_orders) == 1
|
|
|
|
assert trade.open_sl_orders[-1].order_id == '13434334'
|
|
|
|
# price jumped 2x
|
|
mocker.patch(
|
|
f'{EXMS}.fetch_ticker',
|
|
MagicMock(return_value={
|
|
'bid': bid[0],
|
|
'ask': ask[0],
|
|
'last': bid[0],
|
|
})
|
|
)
|
|
|
|
cancel_order_mock = MagicMock(return_value={
|
|
'id': '13434334', 'status': 'canceled', 'fee': {}, 'amount': trade.amount})
|
|
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order')
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
|
|
mocker.patch(f'{EXMS}.create_stoploss', 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
|
|
assert len(trade.open_sl_orders) == 1
|
|
cancel_order_mock.assert_not_called()
|
|
stoploss_order_mock.assert_not_called()
|
|
|
|
# Move time by 10s ... so stoploss order should be replaced.
|
|
time_machine.move_to(start_dt + timedelta(minutes=10), tick=False)
|
|
|
|
assert freqtrade.handle_trade(trade) is False
|
|
assert trade.stop_loss == stop_price[1]
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
|
|
cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT')
|
|
stoploss_order_mock.assert_called_once_with(
|
|
amount=30,
|
|
pair='ETH/USDT',
|
|
order_types=freqtrade.strategy.order_types,
|
|
stop_price=stop_price[1],
|
|
side=exit_side(is_short),
|
|
leverage=1.0
|
|
)
|
|
|
|
# price fell below stoploss, so dry-run sells trade.
|
|
mocker.patch(
|
|
f'{EXMS}.fetch_ticker',
|
|
MagicMock(return_value={
|
|
'bid': bid[1],
|
|
'ask': ask[1],
|
|
'last': bid[1],
|
|
})
|
|
)
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result',
|
|
return_value={'id': 'so1', 'status': 'canceled'})
|
|
assert len(trade.open_sl_orders) == 1
|
|
assert trade.open_sl_orders[-1].order_id == 'so1'
|
|
|
|
assert freqtrade.handle_trade(trade) is True
|
|
assert trade.is_open is False
|
|
assert trade.has_open_sl_orders is False
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_handle_stoploss_on_exchange_trailing_error(
|
|
mocker, default_conf_usdt, fee, caplog, limit_order, is_short, time_machine
|
|
) -> None:
|
|
time_machine.move_to(dt_now() - timedelta(minutes=601))
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
# When trailing stoploss is set
|
|
stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
|
|
patch_exchange(mocker)
|
|
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
{'id': enter_order['id']},
|
|
{'id': exit_order['id']},
|
|
]),
|
|
get_fee=fee,
|
|
create_stoploss=stoploss,
|
|
stoploss_adjust=MagicMock(return_value=True),
|
|
)
|
|
|
|
# enabling TSL
|
|
default_conf_usdt['trailing_stop'] = True
|
|
|
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
|
# enabling stoploss on exchange
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
|
|
# setting stoploss
|
|
freqtrade.strategy.stoploss = 0.05 if is_short else -0.05
|
|
|
|
# setting stoploss_on_exchange_interval to 60 seconds
|
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
trade.is_open = True
|
|
trade.stop_loss = 0.2
|
|
|
|
stoploss_order_hanging = {
|
|
'id': "abcd",
|
|
'status': 'open',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'info': {
|
|
'stopPrice': '0.1'
|
|
}
|
|
}
|
|
trade.orders.append(
|
|
Order(
|
|
ft_order_side='stoploss',
|
|
ft_pair=trade.pair,
|
|
ft_is_open=True,
|
|
ft_amount=trade.amount,
|
|
ft_price=3,
|
|
order_id='abcd',
|
|
order_date=dt_now(),
|
|
)
|
|
)
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order',
|
|
side_effect=InvalidOrderException())
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order',
|
|
return_value=stoploss_order_hanging)
|
|
time_machine.shift(timedelta(minutes=50))
|
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
|
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog)
|
|
|
|
# Still try to create order
|
|
assert stoploss.call_count == 1
|
|
# TODO: Is this actually correct ? This will create a new order every time,
|
|
assert len(trade.open_sl_orders) == 2
|
|
|
|
# Fail creating stoploss order
|
|
caplog.clear()
|
|
cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order')
|
|
mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError())
|
|
time_machine.shift(timedelta(minutes=50))
|
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
|
assert cancel_mock.call_count == 2
|
|
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog)
|
|
|
|
|
|
def test_stoploss_on_exchange_price_rounding(
|
|
mocker, default_conf_usdt, fee, open_trade_usdt) -> None:
|
|
patch_RPCManager(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
get_fee=fee,
|
|
)
|
|
price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s))
|
|
stoploss_mock = MagicMock(return_value={'id': '13434334'})
|
|
adjust_mock = MagicMock(return_value=False)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
create_stoploss=stoploss_mock,
|
|
stoploss_adjust=adjust_mock,
|
|
price_to_precision=price_mock,
|
|
)
|
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
|
open_trade_usdt.stop_loss = 222.55
|
|
|
|
freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {})
|
|
assert price_mock.call_count == 1
|
|
assert adjust_mock.call_count == 1
|
|
assert adjust_mock.call_args_list[0][0][0] == 222
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
@pytest.mark.usefixtures("init_persistence")
|
|
def test_handle_stoploss_on_exchange_custom_stop(
|
|
mocker, default_conf_usdt, fee, is_short, limit_order
|
|
) -> None:
|
|
enter_order = limit_order[entry_side(is_short)]
|
|
exit_order = limit_order[exit_side(is_short)]
|
|
# When trailing stoploss is set
|
|
stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'})
|
|
patch_RPCManager(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 1.9,
|
|
'ask': 2.2,
|
|
'last': 1.9
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
is_cancel_order_result_suitable=MagicMock(return_value=True),
|
|
)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
create_stoploss=stoploss,
|
|
stoploss_adjust=MagicMock(return_value=True),
|
|
)
|
|
|
|
# enabling TSL
|
|
default_conf_usdt['use_custom_stoploss'] = True
|
|
|
|
# disabling ROI
|
|
default_conf_usdt['minimal_roi']['0'] = 999999999
|
|
|
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
|
|
|
# enabling stoploss on exchange
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
|
|
# setting stoploss
|
|
freqtrade.strategy.custom_stoploss = lambda *args, **kwargs: -0.04
|
|
|
|
# setting stoploss_on_exchange_interval to 60 seconds
|
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
|
|
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
freqtrade.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
trade.is_open = True
|
|
trade.orders.append(
|
|
Order(
|
|
ft_order_side='stoploss',
|
|
ft_pair=trade.pair,
|
|
ft_is_open=True,
|
|
ft_amount=trade.amount,
|
|
ft_price=trade.stop_loss,
|
|
order_date=dt_now() - timedelta(minutes=601),
|
|
order_id='100',
|
|
)
|
|
)
|
|
Trade.commit()
|
|
slo = {
|
|
'id': '100',
|
|
'status': 'open',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'info': {
|
|
'stopPrice': '2.0805'
|
|
}
|
|
}
|
|
slo_canceled = deepcopy(slo)
|
|
slo_canceled.update({'status': 'canceled'})
|
|
|
|
def fetch_stoploss_order_mock(order_id, *args, **kwargs):
|
|
x = deepcopy(slo)
|
|
x['id'] = order_id
|
|
return x
|
|
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', MagicMock(fetch_stoploss_order_mock))
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=slo_canceled)
|
|
|
|
assert freqtrade.handle_trade(trade) is False
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
|
|
# price jumped 2x
|
|
mocker.patch(
|
|
f'{EXMS}.fetch_ticker',
|
|
MagicMock(return_value={
|
|
'bid': 4.38 if not is_short else 1.9 / 2,
|
|
'ask': 4.4 if not is_short else 2.2 / 2,
|
|
'last': 4.38 if not is_short else 1.9 / 2,
|
|
})
|
|
)
|
|
|
|
cancel_order_mock = MagicMock()
|
|
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
|
|
mocker.patch(f'{EXMS}.create_stoploss', 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 == 4.4 * 0.96 if not is_short else 1.1
|
|
assert trade.stop_loss_pct == -0.04 if not is_short else 0.04
|
|
|
|
# setting stoploss_on_exchange_interval to 0 seconds
|
|
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
|
|
cancel_order_mock.assert_not_called()
|
|
stoploss_order_mock.assert_not_called()
|
|
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
|
|
cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT')
|
|
# Long uses modified ask - offset, short modified bid + offset
|
|
stoploss_order_mock.assert_called_once_with(
|
|
amount=pytest.approx(trade.amount),
|
|
pair='ETH/USDT',
|
|
order_types=freqtrade.strategy.order_types,
|
|
stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04,
|
|
side=exit_side(is_short),
|
|
leverage=1.0
|
|
)
|
|
|
|
# price fell below stoploss, so dry-run sells trade.
|
|
mocker.patch(
|
|
f'{EXMS}.fetch_ticker',
|
|
MagicMock(return_value={
|
|
'bid': 4.17,
|
|
'ask': 4.19,
|
|
'last': 4.17
|
|
})
|
|
)
|
|
assert freqtrade.handle_trade(trade) is True
|
|
|
|
|
|
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_order) -> None:
|
|
|
|
enter_order = limit_order['buy']
|
|
exit_order = limit_order['sell']
|
|
enter_order['average'] = 2.19
|
|
# When trailing stoploss is set
|
|
stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
|
|
patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
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(
|
|
EXMS,
|
|
fetch_ticker=MagicMock(return_value={
|
|
'bid': 2.19,
|
|
'ask': 2.2,
|
|
'last': 2.19
|
|
}),
|
|
create_order=MagicMock(side_effect=[
|
|
enter_order,
|
|
exit_order,
|
|
]),
|
|
get_fee=fee,
|
|
create_stoploss=stoploss,
|
|
)
|
|
|
|
# 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 seconds
|
|
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.enter_positions()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_open = True
|
|
|
|
trade.stoploss_last_update = dt_now()
|
|
trade.orders.append(
|
|
Order(
|
|
ft_order_side='stoploss',
|
|
ft_pair=trade.pair,
|
|
ft_is_open=True,
|
|
ft_amount=trade.amount,
|
|
ft_price=trade.stop_loss,
|
|
order_id='100',
|
|
)
|
|
)
|
|
|
|
stoploss_order_hanging = MagicMock(return_value={
|
|
'id': '100',
|
|
'status': 'open',
|
|
'type': 'stop_loss_limit',
|
|
'price': 3,
|
|
'average': 2,
|
|
'stopPrice': '2.178'
|
|
})
|
|
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_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 pytest.approx(trade.stop_loss) == 1.76
|
|
|
|
cancel_order_mock = MagicMock()
|
|
stoploss_order_mock = MagicMock()
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
|
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
|
|
|
|
# price goes down 5%
|
|
mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={
|
|
'bid': 2.19 * 0.95,
|
|
'ask': 2.2 * 0.95,
|
|
'last': 2.19 * 0.95
|
|
}))
|
|
assert freqtrade.handle_trade(trade) is False
|
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
|
|
|
# stoploss should remain the same
|
|
assert pytest.approx(trade.stop_loss) == 1.76
|
|
|
|
# stoploss on exchange should not be canceled
|
|
cancel_order_mock.assert_not_called()
|
|
|
|
# price jumped 2x
|
|
mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={
|
|
'bid': 4.38,
|
|
'ask': 4.4,
|
|
'last': 4.38
|
|
}))
|
|
|
|
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 == 4.4 * 0.99
|
|
cancel_order_mock.assert_called_once_with('100', 'NEO/BTC')
|
|
stoploss_order_mock.assert_called_once_with(
|
|
amount=30,
|
|
pair='NEO/BTC',
|
|
order_types=freqtrade.strategy.order_types,
|
|
stop_price=4.4 * 0.99,
|
|
side='sell',
|
|
leverage=1.0
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
|
|
default_conf_usdt, ticker_usdt, fee, is_short, ticker_usdt_sell_down,
|
|
ticker_usdt_sell_up, mocker) -> None:
|
|
rpc_mock = patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=ticker_usdt,
|
|
get_fee=fee,
|
|
_dry_is_price_crossed=MagicMock(return_value=False),
|
|
)
|
|
patch_whitelist(mocker, default_conf_usdt)
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
# Create some test data
|
|
freqtrade.enter_positions()
|
|
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
assert trade.is_short == is_short
|
|
assert trade
|
|
|
|
# Decrease the price and sell it
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down
|
|
)
|
|
|
|
default_conf_usdt['dry_run'] = True
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
# Setting trade stoploss to 0.01
|
|
|
|
trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99
|
|
freqtrade.execute_trade_exit(
|
|
trade=trade, limit=trade.stop_loss,
|
|
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
|
|
|
|
assert rpc_mock.call_count == 2
|
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
|
|
|
assert {
|
|
'type': RPCMessageType.EXIT,
|
|
'trade_id': 1,
|
|
'exchange': 'Binance',
|
|
'pair': 'ETH/USDT',
|
|
'direction': 'Short' if trade.is_short else 'Long',
|
|
'leverage': 1.0,
|
|
'gain': 'loss',
|
|
'limit': 2.02 if is_short else 1.98,
|
|
'order_rate': 2.02 if is_short else 1.98,
|
|
'amount': pytest.approx(29.70297029 if is_short else 30.0),
|
|
'order_type': 'limit',
|
|
'buy_tag': None,
|
|
'enter_tag': None,
|
|
'open_rate': 2.02 if is_short else 2.0,
|
|
'current_rate': 2.2 if is_short else 2.0,
|
|
'profit_amount': -0.3 if is_short else -0.8985,
|
|
'profit_ratio': -0.00501253 if is_short else -0.01493766,
|
|
'stake_currency': 'USDT',
|
|
'quote_currency': 'USDT',
|
|
'fiat_currency': 'USD',
|
|
'base_currency': 'ETH',
|
|
'exit_reason': ExitType.STOP_LOSS.value,
|
|
'open_date': ANY,
|
|
'close_date': ANY,
|
|
'close_rate': ANY,
|
|
'sub_trade': False,
|
|
'cumulative_profit': 0.0,
|
|
'stake_amount': pytest.approx(60),
|
|
'is_final_exit': False,
|
|
'final_profit_ratio': None,
|
|
} == last_msg
|
|
|
|
|
|
def test_execute_trade_exit_sloe_cancel_exception(
|
|
mocker, default_conf_usdt, ticker_usdt, fee, caplog) -> None:
|
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
|
mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException())
|
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300))
|
|
create_order_mock = MagicMock(side_effect=[
|
|
{'id': '12345554'},
|
|
{'id': '12345555'},
|
|
])
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=ticker_usdt,
|
|
get_fee=fee,
|
|
create_order=create_order_mock,
|
|
)
|
|
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
patch_get_signal(freqtrade)
|
|
freqtrade.enter_positions()
|
|
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
PairLock.session = MagicMock()
|
|
|
|
freqtrade.config['dry_run'] = False
|
|
trade.orders.append(
|
|
Order(
|
|
ft_order_side='stoploss',
|
|
ft_pair=trade.pair,
|
|
ft_is_open=True,
|
|
ft_amount=trade.amount,
|
|
ft_price=trade.stop_loss,
|
|
order_id='abcd',
|
|
status='open',
|
|
)
|
|
)
|
|
|
|
freqtrade.execute_trade_exit(trade=trade, limit=1234,
|
|
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
|
|
assert create_order_mock.call_count == 2
|
|
assert log_has('Could not cancel stoploss order abcd for pair ETH/USDT', caplog)
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_execute_trade_exit_with_stoploss_on_exchange(
|
|
default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, is_short, mocker) -> None:
|
|
|
|
default_conf_usdt['exchange']['name'] = 'binance'
|
|
rpc_mock = patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
stoploss = MagicMock(return_value={
|
|
'id': 123,
|
|
'status': 'open',
|
|
'info': {
|
|
'foo': 'bar'
|
|
}
|
|
})
|
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
|
|
|
|
cancel_order = MagicMock(return_value=True)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=ticker_usdt,
|
|
get_fee=fee,
|
|
amount_to_precision=lambda s, x, y: y,
|
|
price_to_precision=lambda s, x, y: y,
|
|
create_stoploss=stoploss,
|
|
cancel_stoploss_order=cancel_order,
|
|
_dry_is_price_crossed=MagicMock(side_effect=[True, False]),
|
|
)
|
|
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
|
|
|
|
# Create some test data
|
|
freqtrade.enter_positions()
|
|
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
assert trade
|
|
trades = [trade]
|
|
|
|
freqtrade.manage_open_orders()
|
|
freqtrade.exit_positions(trades)
|
|
|
|
# Increase the price and sell it
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=ticker_usdt_sell_up
|
|
)
|
|
|
|
freqtrade.execute_trade_exit(
|
|
trade=trade,
|
|
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
|
|
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)
|
|
)
|
|
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trade.is_short = is_short
|
|
assert trade
|
|
assert cancel_order.call_count == 1
|
|
assert rpc_mock.call_count == 4
|
|
|
|
|
|
@pytest.mark.parametrize("is_short", [False, True])
|
|
def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
|
|
default_conf_usdt, ticker_usdt, fee, mocker, is_short) -> None:
|
|
default_conf_usdt['exchange']['name'] = 'binance'
|
|
rpc_mock = patch_RPCManager(mocker)
|
|
patch_exchange(mocker)
|
|
mocker.patch.multiple(
|
|
EXMS,
|
|
fetch_ticker=ticker_usdt,
|
|
get_fee=fee,
|
|
amount_to_precision=lambda s, x, y: y,
|
|
price_to_precision=lambda s, x, y: y,
|
|
_dry_is_price_crossed=MagicMock(side_effect=[False, True]),
|
|
)
|
|
|
|
stoploss = MagicMock(return_value={
|
|
'id': 123,
|
|
'info': {
|
|
'foo': 'bar'
|
|
}
|
|
})
|
|
|
|
mocker.patch(f'{EXMS}.create_stoploss', stoploss)
|
|
|
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short)
|
|
|
|
# Create some test data
|
|
freqtrade.enter_positions()
|
|
freqtrade.manage_open_orders()
|
|
trade = Trade.session.scalars(select(Trade)).first()
|
|
trades = [trade]
|
|
assert trade.has_open_sl_orders is False
|
|
|
|
freqtrade.exit_positions(trades)
|
|
assert trade
|
|
assert trade.has_open_sl_orders is True
|
|
assert not trade.has_open_orders
|
|
|
|
# Assuming stoploss on exchange is hit
|
|
# trade should be sold at the price of stoploss, with exit_reason STOPLOSS_ON_EXCHANGE
|
|
stoploss_executed = MagicMock(return_value={
|
|
"id": "123",
|
|
"timestamp": 1542707426845,
|
|
"datetime": "2018-11-20T09:50:26.845Z",
|
|
"lastTradeTimestamp": None,
|
|
"symbol": "BTC/USDT",
|
|
"type": "stop_loss_limit",
|
|
"side": "buy" if is_short else "sell",
|
|
"price": 1.08801,
|
|
"amount": trade.amount,
|
|
"cost": 1.08801 * trade.amount,
|
|
"average": 1.08801,
|
|
"filled": trade.amount,
|
|
"remaining": 0.0,
|
|
"status": "closed",
|
|
"fee": None,
|
|
"trades": None
|
|
})
|
|
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed)
|
|
|
|
freqtrade.exit_positions(trades)
|
|
assert trade.has_open_sl_orders is False
|
|
assert trade.is_open is False
|
|
assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value
|
|
assert rpc_mock.call_count == 4
|
|
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY
|
|
assert rpc_mock.call_args_list[1][0][0]['amount'] > 20
|
|
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.ENTRY_FILL
|
|
assert rpc_mock.call_args_list[3][0][0]['type'] == RPCMessageType.EXIT_FILL
|