Merge pull request #9267 from Axel-CH/feature/update_sl_order_mgt

Update stoploss order management
This commit is contained in:
Matthias 2024-02-02 06:43:02 +01:00 committed by GitHub
commit 9bfd34a4f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 241 additions and 189 deletions

View File

@ -432,10 +432,6 @@ class FreqtradeBot(LoggingMixin):
try: try:
fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
order.ft_order_side == 'stoploss') order.ft_order_side == 'stoploss')
if order.ft_order_side == 'stoploss':
if fo and fo['status'] == 'open':
# Assume this as the open stoploss order
trade.stoploss_order_id = order.order_id
if fo: if fo:
logger.info(f"Found {order} for trade {trade}.") logger.info(f"Found {order} for trade {trade}.")
self.update_trade_state(trade, order.order_id, fo, self.update_trade_state(trade, order.order_id, fo,
@ -895,17 +891,15 @@ class FreqtradeBot(LoggingMixin):
def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
# First cancelling stoploss on exchange ... # First cancelling stoploss on exchange ...
if trade.stoploss_order_id: for oslo in trade.open_sl_orders:
try: try:
logger.info(f"Cancelling stoploss on exchange for {trade}") logger.info(f"Cancelling stoploss on exchange for {trade} "
f"order: {oslo.order_id}")
co = self.exchange.cancel_stoploss_order_with_result( co = self.exchange.cancel_stoploss_order_with_result(
trade.stoploss_order_id, trade.pair, trade.amount) oslo.order_id, trade.pair, trade.amount)
self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) self.update_trade_state(trade, oslo.order_id, co, stoploss_order=True)
# Reset stoploss order id.
trade.stoploss_order_id = None
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} " logger.exception(f"Could not cancel stoploss order {oslo.order_id} "
f"for pair {trade.pair}") f"for pair {trade.pair}")
return trade return trade
@ -1080,7 +1074,7 @@ class FreqtradeBot(LoggingMixin):
if ( if (
not trade.has_open_orders not trade.has_open_orders
and not trade.stoploss_order_id and not trade.has_open_sl_orders
and not self.wallets.check_exit_amount(trade) and not self.wallets.check_exit_amount(trade)
): ):
logger.warning( logger.warning(
@ -1190,8 +1184,6 @@ class FreqtradeBot(LoggingMixin):
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss', order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss',
trade.amount, stop_price) trade.amount, stop_price)
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.stoploss_order_id = str(stoploss_order['id'])
trade.stoploss_last_update = datetime.now(timezone.utc)
return True return True
except InsufficientFundsError as e: except InsufficientFundsError as e:
logger.warning(f"Unable to place stoploss order {e}.") logger.warning(f"Unable to place stoploss order {e}.")
@ -1199,13 +1191,11 @@ class FreqtradeBot(LoggingMixin):
self.handle_insufficient_funds(trade) self.handle_insufficient_funds(trade)
except InvalidOrderException as e: except InvalidOrderException as e:
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Exiting the trade forcefully') logger.warning('Exiting the trade forcefully')
self.emergency_exit(trade, stop_price) self.emergency_exit(trade, stop_price)
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None
logger.exception('Unable to place a stoploss order on exchange.') logger.exception('Unable to place a stoploss order on exchange.')
return False return False
@ -1219,27 +1209,28 @@ class FreqtradeBot(LoggingMixin):
""" """
logger.debug('Handling stoploss on exchange %s ...', trade) logger.debug('Handling stoploss on exchange %s ...', trade)
stoploss_order = None
try: stoploss_orders = []
# First we check if there is already a stoploss on exchange for slo in trade.open_sl_orders:
stoploss_order = self.exchange.fetch_stoploss_order( stoploss_order = None
trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None try:
except InvalidOrderException as exception: # First we check if there is already a stoploss on exchange
logger.warning('Unable to fetch stoploss order: %s', exception) stoploss_order = self.exchange.fetch_stoploss_order(
slo.order_id, trade.pair) if slo.order_id else None
except InvalidOrderException as exception:
logger.warning('Unable to fetch stoploss order: %s', exception)
if stoploss_order: if stoploss_order:
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_orders.append(stoploss_order)
stoploss_order=True) self.update_trade_state(trade, slo.order_id, stoploss_order,
stoploss_order=True)
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, self._notify_exit(trade, "stoploss", True)
stoploss_order=True) self.handle_protections(trade.pair, trade.trade_direction)
self._notify_exit(trade, "stoploss", True) return True
self.handle_protections(trade.pair, trade.trade_direction)
return True
if trade.has_open_orders or not trade.is_open: if trade.has_open_orders or not trade.is_open:
# Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
@ -1248,7 +1239,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
if not stoploss_order: if len(stoploss_orders) == 0:
stop_price = trade.stoploss_or_liquidation stop_price = trade.stoploss_or_liquidation
if self.edge: if self.edge:
stoploss = self.edge.get_stoploss(pair=trade.pair) stoploss = self.edge.get_stoploss(pair=trade.pair)
@ -1262,27 +1253,7 @@ class FreqtradeBot(LoggingMixin):
# in which case the trade will be closed - which we must check below. # in which case the trade will be closed - which we must check below.
return False return False
# If stoploss order is canceled for some reason we add it again self.manage_trade_stoploss_orders(trade, stoploss_orders)
if (trade.is_open
and stoploss_order
and stoploss_order['status'] in ('canceled', 'cancelled')):
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
return False
else:
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
# Finally we check if stoploss on exchange should be moved up because of trailing.
# Triggered Orders are now real orders - so don't replace stoploss anymore
if (
trade.is_open and stoploss_order
and stoploss_order.get('status_stop') != 'triggered'
and (self.config.get('trailing_stop', False)
or self.config.get('use_custom_stoploss', 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, stoploss_order)
return False return False
@ -1318,6 +1289,42 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Could not create trailing stoploss order " logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: List[Dict]):
"""
Perform required actions acording to existing stoploss orders of trade
:param trade: Corresponding Trade
:param stoploss_orders: Current on exchange stoploss orders
:return: None
"""
# If all stoploss orderd are canceled for some reason we add it again
canceled_sl_orders = [o for o in stoploss_orders
if o['status'] in ('canceled', 'cancelled')]
if (
trade.is_open and
len(stoploss_orders) > 0 and
len(stoploss_orders) == len(canceled_sl_orders)
):
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
return False
else:
logger.warning('All Stoploss orders are cancelled, but unable to recreate one.')
active_sl_orders = [o for o in stoploss_orders if o not in canceled_sl_orders]
if len(active_sl_orders) > 0:
last_active_sl_order = active_sl_orders[-1]
# Finally we check if stoploss on exchange should be moved up because of trailing.
# Triggered Orders are now real orders - so don't replace stoploss anymore
if (trade.is_open and
last_active_sl_order.get('status_stop') != 'triggered' and
(self.config.get('trailing_stop', False) or
self.config.get('use_custom_stoploss', 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, last_active_sl_order)
return
def manage_open_orders(self) -> None: def manage_open_orders(self) -> None:
""" """
Management of open orders on exchange. Unfilled orders might be cancelled if timeout Management of open orders on exchange. Unfilled orders might be cancelled if timeout

View File

@ -1,7 +1,7 @@
import logging import logging
from typing import List, Optional from typing import List, Optional
from sqlalchemy import inspect, select, text, tuple_, update from sqlalchemy import inspect, select, text, update
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.persistence.trade_model import Order, Trade from freqtrade.persistence.trade_model import Order, Trade
@ -91,8 +91,6 @@ def migrate_trades_and_orders_table(
is_stop_loss_trailing = get_column_def( is_stop_loss_trailing = get_column_def(
cols, 'is_stop_loss_trailing', cols, 'is_stop_loss_trailing',
f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)') f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 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') max_rate = get_column_def(cols, 'max_rate', '0.0')
min_rate = get_column_def(cols, 'min_rate', 'null') min_rate = get_column_def(cols, 'min_rate', 'null')
exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null')) exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null'))
@ -160,7 +158,7 @@ def migrate_trades_and_orders_table(
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, amount_requested, open_date, close_date, stake_amount, amount, amount_requested, open_date, close_date,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
is_stop_loss_trailing, stoploss_order_id, stoploss_last_update, is_stop_loss_trailing,
max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs, timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short, trading_mode, leverage, liquidation_price, is_short,
@ -180,7 +178,6 @@ def migrate_trades_and_orders_table(
{initial_stop_loss} initial_stop_loss, {initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{is_stop_loss_trailing} is_stop_loss_trailing, {is_stop_loss_trailing} is_stop_loss_trailing,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {max_rate} max_rate, {min_rate} min_rate,
case when {exit_reason} = 'sell_signal' then 'exit_signal' case when {exit_reason} = 'sell_signal' then 'exit_signal'
when {exit_reason} = 'custom_sell' then 'custom_exit' when {exit_reason} = 'custom_sell' then 'custom_exit'
@ -279,6 +276,8 @@ def fix_old_dry_orders(engine):
with engine.begin() as connection: with engine.begin() as connection:
# Update current dry-run Orders where # Update current dry-run Orders where
# - stoploss order is Open (will be replaced eventually)
# 2nd query:
# - current Order is open # - current Order is open
# - current Trade is closed # - current Trade is closed
# - current Order trade_id not equal to current Trade.id # - current Order trade_id not equal to current Trade.id
@ -286,11 +285,6 @@ def fix_old_dry_orders(engine):
stmt = update(Order).where( stmt = update(Order).where(
Order.ft_is_open.is_(True), Order.ft_is_open.is_(True),
tuple_(Order.ft_trade_id, Order.order_id).not_in(
select(
Trade.id, Trade.stoploss_order_id
).where(Trade.stoploss_order_id.is_not(None))
),
Order.ft_order_side == 'stoploss', Order.ft_order_side == 'stoploss',
Order.order_id.like('dry%'), Order.order_id.like('dry%'),

View File

@ -177,6 +177,8 @@ class Order(ModelBase):
order_date = safe_value_fallback(order, 'timestamp') order_date = safe_value_fallback(order, 'timestamp')
if order_date: if order_date:
self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc) self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc)
elif not self.order_date:
self.order_date = dt_now()
self.ft_is_open = True self.ft_is_open = True
if self.status in NON_OPEN_EXCHANGE_STATES: if self.status in NON_OPEN_EXCHANGE_STATES:
@ -376,10 +378,6 @@ class LocalTrade:
# percentage value of the initial stop loss # percentage value of the initial stop loss
initial_stop_loss_pct: Optional[float] = None initial_stop_loss_pct: Optional[float] = None
is_stop_loss_trailing: bool = False is_stop_loss_trailing: bool = False
# stoploss order id which is on exchange
stoploss_order_id: Optional[str] = None
# last update time of the stoploss order on exchange
stoploss_last_update: Optional[datetime] = None
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate: Optional[float] = None max_rate: Optional[float] = None
# Lowest price reached # Lowest price reached
@ -469,8 +467,8 @@ class LocalTrade:
@property @property
def stoploss_last_update_utc(self): def stoploss_last_update_utc(self):
if self.stoploss_last_update: if self.has_open_sl_orders:
return self.stoploss_last_update.replace(tzinfo=timezone.utc) return max(o.order_date_utc for o in self.open_sl_orders)
return None return None
@property @property
@ -526,7 +524,7 @@ class LocalTrade:
return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss'] return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss']
@property @property
def has_open_orders(self) -> int: def has_open_orders(self) -> bool:
""" """
True if there are open orders for this trade excluding stoploss orders True if there are open orders for this trade excluding stoploss orders
""" """
@ -536,6 +534,37 @@ class LocalTrade:
] ]
return len(open_orders_wo_sl) > 0 return len(open_orders_wo_sl) > 0
@property
def open_sl_orders(self) -> List[Order]:
"""
All open stoploss orders for this trade
"""
return [
o for o in self.orders
if o.ft_order_side in ['stoploss'] and o.ft_is_open
]
@property
def has_open_sl_orders(self) -> bool:
"""
True if there are open stoploss orders for this trade
"""
open_sl_orders = [
o for o in self.orders
if o.ft_order_side in ['stoploss'] and o.ft_is_open
]
return len(open_sl_orders) > 0
@property
def sl_orders(self) -> List[Order]:
"""
All stoploss orders for this trade
"""
return [
o for o in self.orders
if o.ft_order_side in ['stoploss']
]
@property @property
def open_orders_ids(self) -> List[str]: def open_orders_ids(self) -> List[str]:
open_orders_ids_wo_sl = [ open_orders_ids_wo_sl = [
@ -628,11 +657,10 @@ class LocalTrade:
'stop_loss_abs': self.stop_loss, 'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
'stoploss_order_id': self.stoploss_order_id, 'stoploss_last_update': (self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT)
'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT) if self.stoploss_last_update_utc else None),
if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': int(self.stoploss_last_update_utc.timestamp() * 1000
'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( ) if self.stoploss_last_update_utc else None,
tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None,
'initial_stop_loss_abs': self.initial_stop_loss, 'initial_stop_loss_abs': self.initial_stop_loss,
'initial_stop_loss_ratio': (self.initial_stop_loss_pct 'initial_stop_loss_ratio': (self.initial_stop_loss_pct
if self.initial_stop_loss_pct else None), if self.initial_stop_loss_pct else None),
@ -793,7 +821,6 @@ class LocalTrade:
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): elif order.ft_order_side == 'stoploss' and order.status not in ('open', ):
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
if self.is_open and order.safe_filled > 0: if self.is_open and order.safe_filled > 0:
@ -1370,11 +1397,6 @@ class LocalTrade:
exit_order_status=data["exit_order_status"], exit_order_status=data["exit_order_status"],
stop_loss=data["stop_loss_abs"], stop_loss=data["stop_loss_abs"],
stop_loss_pct=data["stop_loss_ratio"], stop_loss_pct=data["stop_loss_ratio"],
stoploss_order_id=data["stoploss_order_id"],
stoploss_last_update=(
datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000,
tz=timezone.utc)
if data["stoploss_last_update_timestamp"] else None),
initial_stop_loss=data["initial_stop_loss_abs"], initial_stop_loss=data["initial_stop_loss_abs"],
initial_stop_loss_pct=data["initial_stop_loss_ratio"], initial_stop_loss_pct=data["initial_stop_loss_ratio"],
min_rate=data["min_rate"], min_rate=data["min_rate"],
@ -1481,11 +1503,6 @@ class Trade(ModelBase, LocalTrade):
Float(), nullable=True) # type: ignore Float(), nullable=True) # type: ignore
is_stop_loss_trailing: Mapped[bool] = mapped_column( is_stop_loss_trailing: Mapped[bool] = mapped_column(
nullable=False, default=False) # type: ignore nullable=False, default=False) # type: ignore
# stoploss order id which is on exchange
stoploss_order_id: Mapped[Optional[str]] = mapped_column(
String(255), nullable=True, index=True) # type: ignore
# last update time of the stoploss order on exchange
stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore
# absolute value of the highest reached price # absolute value of the highest reached price
max_rate: Mapped[Optional[float]] = mapped_column( max_rate: Mapped[Optional[float]] = mapped_column(
Float(), nullable=True, default=0.0) # type: ignore Float(), nullable=True, default=0.0) # type: ignore

View File

@ -315,7 +315,6 @@ class TradeSchema(BaseModel):
stop_loss_abs: Optional[float] = None stop_loss_abs: Optional[float] = None
stop_loss_ratio: Optional[float] = None stop_loss_ratio: Optional[float] = None
stop_loss_pct: Optional[float] = None stop_loss_pct: Optional[float] = None
stoploss_order_id: Optional[str] = None
stoploss_last_update: Optional[str] = None stoploss_last_update: Optional[str] = None
stoploss_last_update_timestamp: Optional[int] = None stoploss_last_update_timestamp: Optional[int] = None
initial_stop_loss_abs: Optional[float] = None initial_stop_loss_abs: Optional[float] = None

View File

@ -979,15 +979,16 @@ class RPC:
except (ExchangeError): except (ExchangeError):
pass pass
# cancel stoploss on exchange ... # cancel stoploss on exchange orders ...
if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
and trade.stoploss_order_id): and trade.has_open_sl_orders):
try:
self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id, for oslo in trade.open_sl_orders:
trade.pair) try:
c_count += 1 self._freqtrade.exchange.cancel_stoploss_order(oslo.order_id, trade.pair)
except (ExchangeError): c_count += 1
pass except (ExchangeError):
pass
trade.delete() trade.delete()
self._freqtrade.wallets.update() self._freqtrade.wallets.update()

View File

@ -266,7 +266,6 @@ def mock_trade_5(fee, is_short: bool):
exchange='binance', exchange='binance',
strategy='SampleStrategy', strategy='SampleStrategy',
enter_tag='TEST1', enter_tag='TEST1',
stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
timeframe=5, timeframe=5,
is_short=is_short, is_short=is_short,
stop_loss_pct=0.10, stop_loss_pct=0.10,

View File

@ -282,7 +282,6 @@ def mock_trade_usdt_5(fee, is_short: bool):
open_rate=2.0, open_rate=2.0,
exchange='binance', exchange='binance',
strategy='SampleStrategy', strategy='SampleStrategy',
stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}',
timeframe=5, timeframe=5,
is_short=is_short, is_short=is_short,
) )

View File

@ -74,7 +74,7 @@ def test_init_dryrun_db(default_conf, tmpdir):
assert Path(filename).is_file() assert Path(filename).is_file()
def test_migrate_new(mocker, default_conf, fee, caplog): def test_migrate(mocker, default_conf, fee, caplog):
""" """
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
@ -277,8 +277,6 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.exit_reason is None assert trade.exit_reason is None
assert trade.strategy is None assert trade.strategy is None
assert trade.timeframe == '5m' assert trade.timeframe == '5m'
assert trade.stoploss_order_id == 'dry_stop_order_id222'
assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog) assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0",
@ -294,9 +292,10 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert orders[0].order_id == 'dry_buy_order' assert orders[0].order_id == 'dry_buy_order'
assert orders[0].ft_order_side == 'buy' assert orders[0].ft_order_side == 'buy'
# All dry-run stoploss orders will be closed
assert orders[-1].order_id == 'dry_stop_order_id222' assert orders[-1].order_id == 'dry_stop_order_id222'
assert orders[-1].ft_order_side == 'stoploss' assert orders[-1].ft_order_side == 'stoploss'
assert orders[-1].ft_is_open is True assert orders[-1].ft_is_open is False
assert orders[1].order_id == 'dry_buy_order22' assert orders[1].order_id == 'dry_buy_order22'
assert orders[1].ft_order_side == 'buy' assert orders[1].ft_order_side == 'buy'

View File

@ -1432,7 +1432,6 @@ def test_to_json(fee):
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_ratio': None, 'stop_loss_ratio': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
'stoploss_order_id': None,
'stoploss_last_update': None, 'stoploss_last_update': None,
'stoploss_last_update_timestamp': None, 'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None, 'initial_stop_loss_abs': None,
@ -1500,7 +1499,6 @@ def test_to_json(fee):
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
'stop_loss_ratio': None, 'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None, 'stoploss_last_update': None,
'stoploss_last_update_timestamp': None, 'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None, 'initial_stop_loss_abs': None,

View File

@ -54,7 +54,6 @@ def test_trade_fromjson():
"stop_loss_abs": 0.1981, "stop_loss_abs": 0.1981,
"stop_loss_ratio": -0.216, "stop_loss_ratio": -0.216,
"stop_loss_pct": -21.6, "stop_loss_pct": -21.6,
"stoploss_order_id": null,
"stoploss_last_update": "2022-10-18 09:13:42", "stoploss_last_update": "2022-10-18 09:13:42",
"stoploss_last_update_timestamp": 1666077222000, "stoploss_last_update_timestamp": 1666077222000,
"initial_stop_loss_abs": 0.1981, "initial_stop_loss_abs": 0.1981,

View File

@ -63,7 +63,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stop_loss_abs': 9.89e-06, 'stop_loss_abs': 9.89e-06,
'stop_loss_pct': -10.0, 'stop_loss_pct': -10.0,
'stop_loss_ratio': -0.1, 'stop_loss_ratio': -0.1,
'stoploss_order_id': None,
'stoploss_last_update': ANY, 'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY, 'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 9.89e-06, 'initial_stop_loss_abs': 9.89e-06,
@ -355,7 +354,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
rpc._rpc_delete('200') rpc._rpc_delete('200')
trades = Trade.session.scalars(select(Trade)).all() trades = Trade.session.scalars(select(Trade)).all()
trades[2].stoploss_order_id = '102'
trades[2].orders.append( trades[2].orders.append(
Order( Order(
ft_order_side='stoploss', ft_order_side='stoploss',

View File

@ -1174,7 +1174,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'stop_loss_abs': ANY, 'stop_loss_abs': ANY,
'stop_loss_pct': ANY, 'stop_loss_pct': ANY,
'stop_loss_ratio': ANY, 'stop_loss_ratio': ANY,
'stoploss_order_id': None,
'stoploss_last_update': ANY, 'stoploss_last_update': ANY,
'stoploss_last_update_timestamp': ANY, 'stoploss_last_update_timestamp': ANY,
'initial_stop_loss_abs': 0.0, 'initial_stop_loss_abs': 0.0,
@ -1378,7 +1377,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint):
'stop_loss_abs': None, 'stop_loss_abs': None,
'stop_loss_pct': None, 'stop_loss_pct': None,
'stop_loss_ratio': None, 'stop_loss_ratio': None,
'stoploss_order_id': None,
'stoploss_last_update': None, 'stoploss_last_update': None,
'stoploss_last_update_timestamp': None, 'stoploss_last_update_timestamp': None,
'initial_stop_loss_abs': None, 'initial_stop_loss_abs': None,

View File

@ -1120,12 +1120,11 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short trade.is_short = is_short
trade.stoploss_order_id = None
trade.is_open = True trade.is_open = True
trades = [trade] trades = [trade]
freqtrade.exit_positions(trades) freqtrade.exit_positions(trades)
assert trade.stoploss_order_id == '13434334' assert trade.has_open_sl_orders is True
assert stoploss.call_count == 1 assert stoploss.call_count == 1
assert trade.is_open is True assert trade.is_open is True
@ -1164,11 +1163,11 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
assert trade.is_short == is_short assert trade.is_short == is_short
assert trade.is_open assert trade.is_open
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 1 assert stoploss.call_count == 1
assert trade.stoploss_order_id == "13434334" assert trade.open_sl_orders[-1].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
# should do nothing and return false # should do nothing and return false
@ -1179,7 +1178,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
hanging_stoploss_order.assert_called_once_with('13434334', trade.pair) hanging_stoploss_order.assert_called_once_with('13434334', trade.pair)
assert trade.stoploss_order_id == "13434334" 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 # Third case: when stoploss was set but it was canceled for some reason
# should set a stoploss immediately and return False # should set a stoploss immediately and return False
@ -1195,12 +1195,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 1 assert stoploss.call_count == 1
assert trade.stoploss_order_id == "103_1" assert len(trade.open_sl_orders) == 1
assert trade.open_sl_orders[-1].order_id == "103_1"
assert trade.amount == amount_before assert trade.amount == amount_before
# Fourth case: when stoploss is set and it is hit # Fourth case: when stoploss is set and it is hit
# should unset stoploss_order_id and return true # should return true as a trade actually happened
# as a trade actually happened
caplog.clear() caplog.clear()
stop_order_dict.update({'id': "103_1"}) stop_order_dict.update({'id': "103_1"})
@ -1221,7 +1221,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is True 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 log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
assert trade.stoploss_order_id is None assert len(trade.open_sl_orders) == 0
assert trade.is_open is False assert trade.is_open is False
caplog.clear() caplog.clear()
@ -1229,26 +1229,27 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
trade.is_open = True trade.is_open = True
freqtrade.handle_stoploss_on_exchange(trade) freqtrade.handle_stoploss_on_exchange(trade)
assert log_has('Unable to place a stoploss order on exchange.', caplog) assert log_has('Unable to place a stoploss order on exchange.', caplog)
assert trade.stoploss_order_id is None assert len(trade.open_sl_orders) == 0
# Fifth case: fetch_order returns InvalidOrder # Fifth case: fetch_order returns InvalidOrder
# It should try to add stoploss order # It should try to add stoploss order
stop_order_dict.update({'id': "105"}) stop_order_dict.update({'id': "105"})
trade.stoploss_order_id = "105"
stoploss.reset_mock() stoploss.reset_mock()
mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException())
mocker.patch(f'{EXMS}.create_stoploss', stoploss) mocker.patch(f'{EXMS}.create_stoploss', stoploss)
freqtrade.handle_stoploss_on_exchange(trade) freqtrade.handle_stoploss_on_exchange(trade)
assert len(trade.open_sl_orders) == 1
assert stoploss.call_count == 1 assert stoploss.call_count == 1
# Sixth case: Closed Trade # Sixth case: Closed Trade
# Should not create new order # Should not create new order
trade.stoploss_order_id = None
trade.is_open = False trade.is_open = False
trade.open_sl_orders[-1].ft_is_open = False
stoploss.reset_mock() stoploss.reset_mock()
mocker.patch(f'{EXMS}.fetch_order') mocker.patch(f'{EXMS}.fetch_order')
mocker.patch(f'{EXMS}.create_stoploss', stoploss) mocker.patch(f'{EXMS}.create_stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.has_open_sl_orders is False
assert stoploss.call_count == 0 assert stoploss.call_count == 0
@ -1282,7 +1283,7 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
assert trade.is_short == is_short assert trade.is_short == is_short
assert trade.is_open assert trade.is_open
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
# emergency exit triggered # emergency exit triggered
# Trailing stop should not act anymore # Trailing stop should not act anymore
@ -1297,7 +1298,6 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i
'remaining': enter_order['amount'], 'remaining': enter_order['amount'],
'info': {'stopPrice': 22}, 'info': {'stopPrice': 22},
}]) }])
trade.stoploss_order_id = "107"
trade.stoploss_last_update = dt_now() - timedelta(hours=1) trade.stoploss_last_update = dt_now() - timedelta(hours=1)
trade.stop_loss = 24 trade.stop_loss = 24
trade.exit_reason = None trade.exit_reason = None
@ -1314,14 +1314,14 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i
) )
freqtrade.config['trailing_stop'] = True freqtrade.config['trailing_stop'] = True
stoploss = MagicMock(side_effect=InvalidOrderException()) stoploss = MagicMock(side_effect=InvalidOrderException())
assert trade.has_open_sl_orders is True
Trade.commit() Trade.commit()
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled)
mocker.patch(f'{EXMS}.create_stoploss', stoploss) mocker.patch(f'{EXMS}.create_stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
assert trade.is_open is False assert trade.is_open is False
assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT) assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
@ -1356,11 +1356,11 @@ def test_handle_stoploss_on_exchange_partial(
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short trade.is_short = is_short
trade.is_open = True trade.is_open = True
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.call_count == 1 assert stoploss.call_count == 1
assert trade.stoploss_order_id == "101" assert trade.has_open_sl_orders is True
assert trade.open_sl_orders[-1].order_id == "101"
assert trade.amount == 30 assert trade.amount == 30
stop_order_dict.update({'id': "102"}) stop_order_dict.update({'id': "102"})
# Stoploss on exchange is cancelled on exchange, but filled partially. # Stoploss on exchange is cancelled on exchange, but filled partially.
@ -1380,13 +1380,14 @@ def test_handle_stoploss_on_exchange_partial(
# Stoploss filled partially ... # Stoploss filled partially ...
assert trade.amount == 15 assert trade.amount == 15
assert trade.stoploss_order_id == "102" assert trade.open_sl_orders[-1].order_id == "102"
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_handle_stoploss_on_exchange_partial_cancel_here( def test_handle_stoploss_on_exchange_partial_cancel_here(
mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None: mocker, default_conf_usdt, fee, is_short, limit_order, caplog, time_machine) -> None:
stop_order_dict = {'id': "101", "status": "open"} stop_order_dict = {'id': "101", "status": "open"}
time_machine.move_to(dt_now())
default_conf_usdt['trailing_stop'] = True default_conf_usdt['trailing_stop'] = True
stoploss = MagicMock(return_value=stop_order_dict) stoploss = MagicMock(return_value=stop_order_dict)
enter_order = limit_order[entry_side(is_short)] enter_order = limit_order[entry_side(is_short)]
@ -1414,11 +1415,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here(
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short trade.is_short = is_short
trade.is_open = True trade.is_open = True
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.call_count == 1 assert stoploss.call_count == 1
assert trade.stoploss_order_id == "101" assert trade.has_open_sl_orders is True
assert trade.open_sl_orders[-1].order_id == "101"
assert trade.amount == 30 assert trade.amount == 30
stop_order_dict.update({'id': "102"}) stop_order_dict.update({'id': "102"})
# Stoploss on exchange is open. # Stoploss on exchange is open.
@ -1445,13 +1446,14 @@ def test_handle_stoploss_on_exchange_partial_cancel_here(
}) })
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel) mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel)
trade.stoploss_last_update = dt_now() - timedelta(minutes=10) time_machine.shift(timedelta(minutes=15))
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
# Canceled Stoploss filled partially ... # Canceled Stoploss filled partially ...
assert log_has_re('Cancelling current stoploss on exchange.*', caplog) assert log_has_re('Cancelling current stoploss on exchange.*', caplog)
assert trade.stoploss_order_id == "102" assert trade.has_open_sl_orders is True
assert trade.open_sl_orders[-1].order_id == "102"
assert trade.amount == 15 assert trade.amount == 15
@ -1478,7 +1480,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
) )
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,
fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': '100'}),
create_stoploss=MagicMock(side_effect=ExchangeError()), create_stoploss=MagicMock(side_effect=ExchangeError()),
) )
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
@ -1488,7 +1490,6 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
assert trade.is_short == is_short assert trade.is_short == is_short
trade.is_open = True trade.is_open = True
trade.stoploss_order_id = "100"
trade.orders.append( trade.orders.append(
Order( Order(
ft_order_side='stoploss', ft_order_side='stoploss',
@ -1503,8 +1504,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
assert trade assert trade
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert log_has_re(r'Stoploss order was cancelled, but unable to recreate one.*', caplog) assert log_has_re(r'All Stoploss orders are cancelled, but unable to recreate one\.', caplog)
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
assert trade.is_open is True assert trade.is_open is True
@ -1545,7 +1546,7 @@ def test_create_stoploss_order_invalid_order(
caplog.clear() caplog.clear()
rpc_mock.reset_mock() rpc_mock.reset_mock()
freqtrade.create_stoploss_order(trade, 200) freqtrade.create_stoploss_order(trade, 200)
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value
assert log_has("Unable to place a stoploss order on exchange. ", caplog) assert log_has("Unable to place a stoploss order on exchange. ", caplog)
assert log_has("Exiting the trade forcefully", caplog) assert log_has("Exiting the trade forcefully", caplog)
@ -1599,14 +1600,13 @@ def test_create_stoploss_order_insufficient_funds(
caplog.clear() caplog.clear()
freqtrade.create_stoploss_order(trade, 200) freqtrade.create_stoploss_order(trade, 200)
# stoploss_orderid was empty before # stoploss_orderid was empty before
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
assert mock_insuf.call_count == 1 assert mock_insuf.call_count == 1
mock_insuf.reset_mock() mock_insuf.reset_mock()
trade.stoploss_order_id = 'stoploss_orderid'
freqtrade.create_stoploss_order(trade, 200) freqtrade.create_stoploss_order(trade, 200)
# No change to stoploss-orderid # No change to stoploss-orderid
assert trade.stoploss_order_id == 'stoploss_orderid' assert trade.has_open_sl_orders is False
assert mock_insuf.call_count == 1 assert mock_insuf.call_count == 1
@ -1668,7 +1668,7 @@ def test_handle_stoploss_on_exchange_trailing(
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short trade.is_short = is_short
trade.is_open = True trade.is_open = True
trade.stoploss_order_id = '100' assert trade.has_open_sl_orders is False
trade.stoploss_last_update = dt_now() - timedelta(minutes=20) trade.stoploss_last_update = dt_now() - timedelta(minutes=20)
trade.orders.append( trade.orders.append(
Order( Order(
@ -1678,27 +1678,35 @@ def test_handle_stoploss_on_exchange_trailing(
ft_amount=trade.amount, ft_amount=trade.amount,
ft_price=trade.stop_loss, ft_price=trade.stop_loss,
order_id='100', order_id='100',
order_date=dt_now() - timedelta(minutes=20),
) )
) )
stoploss_order_hanging = MagicMock(return_value={ stoploss_order_hanging = {
'id': '100', 'id': '100',
'status': 'open', 'status': 'open',
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
'price': hang_price, 'price': hang_price,
'average': 2, 'average': 2,
'fee': {},
'amount': 0,
'info': { 'info': {
'stopPrice': stop_price[0] 'stopPrice': stop_price[0]
} }
}) }
stoploss_order_cancel = deepcopy(stoploss_order_hanging)
stoploss_order_cancel['status'] = 'canceled'
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) 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% # stoploss initially at 5%
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id == '13434334' assert len(trade.open_sl_orders) == 1
assert trade.open_sl_orders[-1].order_id == '13434334'
# price jumped 2x # price jumped 2x
mocker.patch( mocker.patch(
@ -1710,14 +1718,17 @@ def test_handle_stoploss_on_exchange_trailing(
}) })
) )
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock(return_value={
'id': '13434334', 'status': 'canceled', 'fee': {}, 'amount': trade.amount})
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) 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}.cancel_stoploss_order', cancel_order_mock)
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) mocker.patch(f'{EXMS}.create_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
assert freqtrade.handle_stoploss_on_exchange(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() cancel_order_mock.assert_not_called()
stoploss_order_mock.assert_not_called() stoploss_order_mock.assert_not_called()
@ -1748,14 +1759,21 @@ def test_handle_stoploss_on_exchange_trailing(
'last': bid[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 freqtrade.handle_trade(trade) is True
assert trade.stoploss_order_id is None assert trade.is_open is False
assert trade.has_open_sl_orders is False
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_handle_stoploss_on_exchange_trailing_error( def test_handle_stoploss_on_exchange_trailing_error(
mocker, default_conf_usdt, fee, caplog, limit_order, is_short mocker, default_conf_usdt, fee, caplog, limit_order, is_short, time_machine
) -> None: ) -> None:
time_machine.move_to(dt_now() - timedelta(minutes=601))
enter_order = limit_order[entry_side(is_short)] enter_order = limit_order[entry_side(is_short)]
exit_order = limit_order[exit_side(is_short)] exit_order = limit_order[exit_side(is_short)]
# When trailing stoploss is set # When trailing stoploss is set
@ -1774,9 +1792,6 @@ def test_handle_stoploss_on_exchange_trailing_error(
{'id': exit_order['id']}, {'id': exit_order['id']},
]), ]),
get_fee=fee, get_fee=fee,
)
mocker.patch.multiple(
EXMS,
create_stoploss=stoploss, create_stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True), stoploss_adjust=MagicMock(return_value=True),
) )
@ -1798,10 +1813,7 @@ def test_handle_stoploss_on_exchange_trailing_error(
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short trade.is_short = is_short
trade.is_open = True trade.is_open = True
trade.stoploss_order_id = "abcd"
trade.stop_loss = 0.2 trade.stop_loss = 0.2
trade.stoploss_last_update = (dt_now() - timedelta(minutes=601)).replace(tzinfo=None)
trade.is_short = is_short
stoploss_order_hanging = { stoploss_order_hanging = {
'id': "abcd", 'id': "abcd",
@ -1813,23 +1825,37 @@ def test_handle_stoploss_on_exchange_trailing_error(
'stopPrice': '0.1' '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', mocker.patch(f'{EXMS}.cancel_stoploss_order',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
mocker.patch(f'{EXMS}.fetch_stoploss_order', mocker.patch(f'{EXMS}.fetch_stoploss_order',
return_value=stoploss_order_hanging) return_value=stoploss_order_hanging)
time_machine.shift(timedelta(minutes=50))
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) 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) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog)
# Still try to create order # Still try to create order
assert stoploss.call_count == 1 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 # Fail creating stoploss order
trade.stoploss_last_update = dt_now() - timedelta(minutes=601)
caplog.clear() caplog.clear()
cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order') cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order')
mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) 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) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1 assert cancel_mock.call_count == 2
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog)
@ -1850,7 +1876,6 @@ def test_stoploss_on_exchange_price_rounding(
price_to_precision=price_mock, price_to_precision=price_mock,
) )
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
open_trade_usdt.stoploss_order_id = '13434334'
open_trade_usdt.stop_loss = 222.55 open_trade_usdt.stop_loss = 222.55
freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {}) freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {})
@ -1881,6 +1906,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
exit_order, exit_order,
]), ]),
get_fee=fee, get_fee=fee,
is_cancel_order_result_suitable=MagicMock(return_value=True),
) )
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,
@ -1911,8 +1937,6 @@ def test_handle_stoploss_on_exchange_custom_stop(
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trade.is_short = is_short trade.is_short = is_short
trade.is_open = True trade.is_open = True
trade.stoploss_order_id = '100'
trade.stoploss_last_update = dt_now() - timedelta(minutes=601)
trade.orders.append( trade.orders.append(
Order( Order(
ft_order_side='stoploss', ft_order_side='stoploss',
@ -1920,11 +1944,12 @@ def test_handle_stoploss_on_exchange_custom_stop(
ft_is_open=True, ft_is_open=True,
ft_amount=trade.amount, ft_amount=trade.amount,
ft_price=trade.stop_loss, ft_price=trade.stop_loss,
order_date=dt_now() - timedelta(minutes=601),
order_id='100', order_id='100',
) )
) )
Trade.commit()
stoploss_order_hanging = MagicMock(return_value={ slo = {
'id': '100', 'id': '100',
'status': 'open', 'status': 'open',
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
@ -1933,9 +1958,17 @@ def test_handle_stoploss_on_exchange_custom_stop(
'info': { 'info': {
'stopPrice': '2.0805' 'stopPrice': '2.0805'
} }
}) }
slo_canceled = deepcopy(slo)
slo_canceled.update({'status': 'canceled'})
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) 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_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
@ -1954,7 +1987,6 @@ def test_handle_stoploss_on_exchange_custom_stop(
stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
trade.stoploss_order_id = '100'
# 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
@ -1968,10 +2000,12 @@ def test_handle_stoploss_on_exchange_custom_stop(
# setting stoploss_on_exchange_interval to 0 seconds # setting stoploss_on_exchange_interval to 0 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 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 assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_called_once_with('100', 'ETH/USDT') cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT')
# Long uses modified ask - offset, short modified bid + offset # Long uses modified ask - offset, short modified bid + offset
stoploss_order_mock.assert_called_once_with( stoploss_order_mock.assert_called_once_with(
amount=pytest.approx(trade.amount), amount=pytest.approx(trade.amount),
@ -2048,7 +2082,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trade.is_open = True trade.is_open = True
trade.stoploss_order_id = '100'
trade.stoploss_last_update = dt_now() trade.stoploss_last_update = dt_now()
trade.orders.append( trade.orders.append(
Order( Order(
@ -4054,7 +4088,17 @@ def test_execute_trade_exit_sloe_cancel_exception(
PairLock.session = MagicMock() PairLock.session = MagicMock()
freqtrade.config['dry_run'] = False freqtrade.config['dry_run'] = False
trade.stoploss_order_id = "abcd" 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, freqtrade.execute_trade_exit(trade=trade, limit=1234,
exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
@ -4157,16 +4201,15 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
trade = Trade.session.scalars(select(Trade)).first() trade = Trade.session.scalars(select(Trade)).first()
trades = [trade] trades = [trade]
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
freqtrade.exit_positions(trades) freqtrade.exit_positions(trades)
assert trade assert trade
assert trade.stoploss_order_id == '123' assert trade.has_open_sl_orders is True
assert not trade.has_open_orders assert not trade.has_open_orders
# Assuming stoploss on exchange is hit # Assuming stoploss on exchange is hit
# stoploss_order_id should become None # trade should be sold at the price of stoploss, with exit_reaeon STOPLOSS_ON_EXCHANGE
# and trade should be sold at the price of stoploss
stoploss_executed = MagicMock(return_value={ stoploss_executed = MagicMock(return_value={
"id": "123", "id": "123",
"timestamp": 1542707426845, "timestamp": 1542707426845,
@ -4188,7 +4231,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed)
freqtrade.exit_positions(trades) freqtrade.exit_positions(trades)
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
assert trade.is_open is False assert trade.is_open is False
assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value
assert rpc_mock.call_count == 4 assert rpc_mock.call_count == 4
@ -5693,7 +5736,6 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
def reset_open_orders(trade): def reset_open_orders(trade):
trade.stoploss_order_id = None
trade.is_short = is_short trade.is_short = is_short
create_mock_trades(fee, is_short=is_short) create_mock_trades(fee, is_short=is_short)
@ -5705,7 +5747,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
trade = trades[1] trade = trades[1]
reset_open_orders(trade) reset_open_orders(trade)
assert not trade.has_open_orders assert not trade.has_open_orders
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
freqtrade.handle_insufficient_funds(trade) freqtrade.handle_insufficient_funds(trade)
order = trade.orders[0] order = trade.orders[0]
@ -5715,7 +5757,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
assert mock_uts.call_count == 0 assert mock_uts.call_count == 0
# No change to orderid - as update_trade_state is mocked # No change to orderid - as update_trade_state is mocked
assert not trade.has_open_orders assert not trade.has_open_orders
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
caplog.clear() caplog.clear()
mock_fo.reset_mock() mock_fo.reset_mock()
@ -5726,7 +5768,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
# This part in not relevant anymore # This part in not relevant anymore
# assert not trade.has_open_orders # assert not trade.has_open_orders
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
freqtrade.handle_insufficient_funds(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_4(is_short=is_short) order = mock_order_4(is_short=is_short)
@ -5734,8 +5776,8 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
assert mock_fo.call_count == 1 assert mock_fo.call_count == 1
assert mock_uts.call_count == 1 assert mock_uts.call_count == 1
# Found open buy order # Found open buy order
assert trade.has_open_orders assert trade.has_open_orders is True
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
caplog.clear() caplog.clear()
mock_fo.reset_mock() mock_fo.reset_mock()
@ -5744,16 +5786,16 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
trade = trades[4] trade = trades[4]
reset_open_orders(trade) reset_open_orders(trade)
assert not trade.has_open_orders assert not trade.has_open_orders
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders
freqtrade.handle_insufficient_funds(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_5_stoploss(is_short=is_short) order = mock_order_5_stoploss(is_short=is_short)
assert log_has_re(r"Trying to refind Order\(.*", caplog) assert log_has_re(r"Trying to refind Order\(.*", caplog)
assert mock_fo.call_count == 1 assert mock_fo.call_count == 1
assert mock_uts.call_count == 2 assert mock_uts.call_count == 2
# stoploss_order_id is "refound" and added to the trade # stoploss order is "refound" and added to the trade
assert not trade.has_open_orders assert not trade.has_open_orders
assert trade.stoploss_order_id is not None assert trade.has_open_sl_orders is True
caplog.clear() caplog.clear()
mock_fo.reset_mock() mock_fo.reset_mock()
@ -5764,7 +5806,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
reset_open_orders(trade) reset_open_orders(trade)
# This part in not relevant anymore # This part in not relevant anymore
# assert not trade.has_open_orders # assert not trade.has_open_orders
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
freqtrade.handle_insufficient_funds(trade) freqtrade.handle_insufficient_funds(trade)
order = mock_order_6_sell(is_short=is_short) order = mock_order_6_sell(is_short=is_short)
@ -5773,7 +5815,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
assert mock_uts.call_count == 1 assert mock_uts.call_count == 1
# sell-orderid is "refound" and added to the trade # sell-orderid is "refound" and added to the trade
assert trade.open_orders_ids[0] == order['id'] assert trade.open_orders_ids[0] == order['id']
assert trade.stoploss_order_id is None assert trade.has_open_sl_orders is False
caplog.clear() caplog.clear()

View File

@ -49,7 +49,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
stoploss_order_closed['filled'] = stoploss_order_closed['amount'] stoploss_order_closed['filled'] = stoploss_order_closed['amount']
# Sell first trade based on stoploss, keep 2nd and 3rd trade open # Sell first trade based on stoploss, keep 2nd and 3rd trade open
stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open] stop_orders = [stoploss_order_closed, stoploss_order_open.copy(), stoploss_order_open.copy()]
stoploss_order_mock = MagicMock( stoploss_order_mock = MagicMock(
side_effect=stop_orders) side_effect=stop_orders)
# Sell 3rd trade (not called for the first trade) # Sell 3rd trade (not called for the first trade)
@ -100,9 +100,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
stop_order = stop_orders[idx] stop_order = stop_orders[idx]
stop_order['id'] = f"stop{idx}" stop_order['id'] = f"stop{idx}"
oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss') oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss')
oobj.ft_is_open = True
trade.orders.append(oobj) trade.orders.append(oobj)
trade.stoploss_order_id = f"stop{idx}" assert len(trade.open_sl_orders) == 1
n = freqtrade.exit_positions(trades) n = freqtrade.exit_positions(trades)
assert n == 2 assert n == 2
@ -113,6 +114,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
# Only order for 3rd trade needs to be cancelled # Only order for 3rd trade needs to be cancelled
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert stoploss_order_mock.call_count == 3
# Wallets must be updated between stoploss cancellation and selling, and will be updated again # Wallets must be updated between stoploss cancellation and selling, and will be updated again
# during update_trade_state # during update_trade_state
assert wallets_mock.call_count == 4 assert wallets_mock.call_count == 4