diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c6b8f0e62..ee104325b 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -39,6 +39,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] # Don't modify sequence of DEFAULT_TRADES_COLUMNS # it has wide consequences for stored trades files DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost'] +TRADING_MODES = ['spot', 'margin', 'futures'] +COLLATERAL_TYPES = ['cross', 'isolated'] LAST_BT_RESULT_FN = '.last_result.json' FTHYPT_FILEVERSION = 'fthypt_fileversion' @@ -146,6 +148,8 @@ CONF_SCHEMA = { 'sell_profit_offset': {'type': 'number'}, 'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'ignore_buying_expired_candle_after': {'type': 'number'}, + 'trading_mode': {'type': 'string', 'enum': TRADING_MODES}, + 'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES}, 'bot_name': {'type': 'string'}, 'unfilledtimeout': { 'type': 'object', @@ -193,7 +197,7 @@ CONF_SCHEMA = { 'required': ['price_side'] }, 'custom_price_max_distance_ratio': { - 'type': 'number', 'minimum': 0.0 + 'type': 'number', 'minimum': 0.0 }, 'order_types': { 'type': 'object', diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 4e3f693e5..663b37b83 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -5,15 +5,21 @@ class RPCMessageType(Enum): STATUS = 'status' WARNING = 'warning' STARTUP = 'startup' + BUY = 'buy' BUY_FILL = 'buy_fill' BUY_CANCEL = 'buy_cancel' + SELL = 'sell' SELL_FILL = 'sell_fill' SELL_CANCEL = 'sell_cancel' PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' + SHORT = 'short' + SHORT_FILL = 'short_fill' + SHORT_CANCEL = 'short_cancel' + def __repr__(self): return self.value diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 10bf0a753..f711bc258 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -804,8 +804,14 @@ class Exchange: rate_for_order = self.price_to_precision(pair, rate) if needs_price else None self._lev_prep(pair, leverage) - order = self._api.create_order(pair, ordertype, side, - amount, rate_for_order, params) + order = self._api.create_order( + pair, + ordertype, + side, + amount, + rate_for_order, + params + ) self._log_exchange_response('create_order', order) return order diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ddb4b148f..bb7e06e8a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,7 +7,7 @@ import traceback from datetime import datetime, time, timezone from math import isclose from threading import Lock -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import arrow from schedule import Scheduler @@ -17,7 +17,8 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, State, TradingMode +from freqtrade.enums import (Collateral, RPCMessageType, SellType, SignalDirection, State, + TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -101,14 +102,19 @@ class FreqtradeBot(LoggingMixin): initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED - # Protect sell-logic from forcesell and vice versa + # Protect exit-logic from forcesell and vice versa self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + self.trading_mode: TradingMode = TradingMode.SPOT + self.collateral_type: Optional[Collateral] = None + if 'trading_mode' in self.config: self.trading_mode = TradingMode(self.config['trading_mode']) - else: - self.trading_mode = TradingMode.SPOT + + if 'collateral_type' in self.config: + self.collateral_type = Collateral(self.config['collateral_type']) + self._schedule = Scheduler() if self.trading_mode == TradingMode.FUTURES: @@ -194,7 +200,7 @@ class FreqtradeBot(LoggingMixin): # Protect from collisions with forceexit. # Without this, freqtrade my try to recreate stoploss_on_exchange orders - # while selling is in process, since telegram messages arrive in an different thread. + # while exiting is in process, since telegram messages arrive in an different thread. with self._exit_lock: trades = Trade.get_open_trades() # First process current opened trades (positions) @@ -305,21 +311,26 @@ class FreqtradeBot(LoggingMixin): trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() for trade in trades: - - if not trade.is_open and not trade.fee_updated('sell'): + if not trade.is_open and not trade.fee_updated(trade.exit_side): # Get sell fee - order = trade.select_order('sell', False) + order = trade.select_order(trade.exit_side, False) if order: - logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.exit_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) self.update_trade_state(trade, order.order_id, stoploss_order=order.ft_order_side == 'stoploss') trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() for trade in trades: - if trade.is_open and not trade.fee_updated('buy'): - order = trade.select_order('buy', False) + if trade.is_open and not trade.fee_updated(trade.enter_side): + order = trade.select_order(trade.enter_side, False) if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) self.update_trade_state(trade, order.order_id) def handle_insufficient_funds(self, trade: Trade): @@ -327,8 +338,8 @@ class FreqtradeBot(LoggingMixin): Determine if we ever opened a exiting order for this trade. If not, try update entering fees - otherwise "refind" the open order we obviously lost. """ - sell_order = trade.select_order('sell', None) - if sell_order: + exit_order = trade.select_order(trade.exit_side, None) + if exit_order: self.refind_lost_order(trade) else: self.reupdate_enter_order_fees(trade) @@ -338,10 +349,11 @@ class FreqtradeBot(LoggingMixin): Get buy order from database, and try to reupdate. Handles trades where the initial fee-update did not work. """ - logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', False) + logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}") + order = trade.select_order(trade.enter_side, False) if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id) def refind_lost_order(self, trade): @@ -357,7 +369,7 @@ class FreqtradeBot(LoggingMixin): if not order.ft_is_open: logger.debug(f"Order {order} is no longer open.") continue - if order.ft_order_side == 'buy': + if order.ft_order_side == trade.enter_side: # Skip buy side - this is handled by reupdate_enter_order_fees continue try: @@ -367,7 +379,7 @@ class FreqtradeBot(LoggingMixin): if fo and fo['status'] == 'open': # Assume this as the open stoploss order trade.stoploss_order_id = order.order_id - elif order.ft_order_side == 'sell': + elif order.ft_order_side == trade.exit_side: if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id @@ -456,7 +468,9 @@ class FreqtradeBot(LoggingMixin): # running get_signal on historical data fetched (signal, enter_tag) = self.strategy.get_entry_signal( - pair, self.strategy.timeframe, analyzed_df + pair, + self.strategy.timeframe, + analyzed_df ) if signal: @@ -465,19 +479,31 @@ class FreqtradeBot(LoggingMixin): bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): - # TODO-lev: Does the below need to be adjusted for shorts? - if self._check_depth_of_market_buy(pair, bid_check_dom): - # TODO-lev: pass in "enter" as side. - - return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) + if self._check_depth_of_market(pair, bid_check_dom, side=signal): + return self.execute_entry( + pair, + stake_amount, + enter_tag=enter_tag, + is_short=(signal == SignalDirection.SHORT) + ) else: return False - return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) + return self.execute_entry( + pair, + stake_amount, + enter_tag=enter_tag, + is_short=(signal == SignalDirection.SHORT) + ) else: return False - def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: + def _check_depth_of_market( + self, + pair: str, + conf: Dict, + side: SignalDirection + ) -> bool: """ Checks depth of market before executing a buy """ @@ -487,9 +513,17 @@ class FreqtradeBot(LoggingMixin): order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) order_book_bids = order_book_data_frame['b_size'].sum() order_book_asks = order_book_data_frame['a_size'].sum() - bids_ask_delta = order_book_bids / order_book_asks + + enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks + exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids + bids_ask_delta = enter_side / exit_side + + bids = f"Bids: {order_book_bids}" + asks = f"Asks: {order_book_asks}" + delta = f"Delta: {bids_ask_delta}" + logger.info( - f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, " + f"{bids}, {asks}, {delta}, Direction: {side.value}" f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " f"Immediate Ask Quantity: {order_book['asks'][0][1]}." @@ -501,21 +535,65 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False - def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool: + def leverage_prep( + self, + pair: str, + open_rate: float, + amount: float, + leverage: float, + is_short: bool + ) -> Tuple[float, Optional[float]]: + + interest_rate = 0.0 + isolated_liq = None + + # TODO-lev: Uncomment once liq and interest merged in + # if TradingMode == TradingMode.MARGIN: + # interest_rate = self.exchange.get_interest_rate( + # pair=pair, + # open_rate=open_rate, + # is_short=is_short + # ) + + # if self.collateral_type == Collateral.ISOLATED: + + # isolated_liq = liquidation_price( + # exchange_name=self.exchange.name, + # trading_mode=self.trading_mode, + # open_rate=open_rate, + # amount=amount, + # leverage=leverage, + # is_short=is_short + # ) + + return interest_rate, isolated_liq + + def execute_entry( + self, + pair: str, + stake_amount: float, + price: Optional[float] = None, + forcebuy: bool = False, + leverage: float = 1.0, + is_short: bool = False, + enter_tag: Optional[str] = None + ) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY :param stake_amount: amount of stake-currency for the pair + :param leverage: amount of leverage applied to this trade :return: True if a buy order is created, false if it fails. """ time_in_force = self.strategy.order_time_in_force['buy'] + [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] + if price: enter_limit_requested = price else: # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") + proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side) custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=proposed_enter_rate)( pair=pair, current_time=datetime.now(timezone.utc), @@ -524,10 +602,14 @@ class FreqtradeBot(LoggingMixin): enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) if not enter_limit_requested: - raise PricingError('Could not determine buy price.') + raise PricingError(f'Could not determine {side} price.') - min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, - self.strategy.stoploss) + min_stake_amount = self.exchange.get_min_pair_stake_amount( + pair, + enter_limit_requested, + self.strategy.stoploss, + leverage=leverage + ) if not self.edge: max_stake_amount = self.wallets.get_available_stake_amount() @@ -543,10 +625,12 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: return False - logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") + logger.info( + f"{name} signal found: about create a new trade for {pair} with stake_amount: " + f"{stake_amount} ..." + ) - amount = stake_amount / enter_limit_requested + amount = (stake_amount / enter_limit_requested) * leverage order_type = self.strategy.order_types['buy'] if forcebuy: # Forcebuy can define a different ordertype @@ -558,15 +642,21 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force, current_time=datetime.now(timezone.utc), - side='long' + side='short' if is_short else 'long' ): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", - amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') + order = self.exchange.create_order( + pair=pair, + ordertype=order_type, + side=side, + amount=amount, + rate=enter_limit_requested, + time_in_force=time_in_force, + leverage=leverage + ) + order_obj = Order.parse_from_ccxt_object(order, pair, side) order_id = order['id'] order_status = order.get('status', None) @@ -579,17 +669,17 @@ class FreqtradeBot(LoggingMixin): # return false if the order is not filled if float(order['filled']) == 0: - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' + logger.warning('%s %s order with time in force %s for %s is %s by %s.' ' zero amount is fulfilled.', - order_tif, order_type, pair, order_status, self.exchange.name) + name, order_tif, order_type, pair, order_status, self.exchange.name) return False else: # the order is partially fulfilled # in case of IOC orders we can check immediately # if the order is fulfilled fully or partially - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' + logger.warning('%s %s order with time in force %s for %s is %s by %s.' ' %s amount fulfilled out of %s (%s remaining which is canceled).', - order_tif, order_type, pair, order_status, self.exchange.name, + name, order_tif, order_type, pair, order_status, self.exchange.name, order['filled'], order['amount'], order['remaining'] ) stake_amount = order['cost'] @@ -602,6 +692,14 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + interest_rate, isolated_liq = self.leverage_prep( + leverage=leverage, + pair=pair, + amount=amount, + open_rate=enter_limit_filled_price, + is_short=is_short + ) + # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') open_date = datetime.now(timezone.utc) @@ -627,6 +725,10 @@ class FreqtradeBot(LoggingMixin): # TODO-lev: compatibility layer for buy_tag (!) buy_tag=enter_tag, timeframe=timeframe_to_minutes(self.config['timeframe']), + leverage=leverage, + is_short=is_short, + interest_rate=interest_rate, + isolated_liq=isolated_liq, trading_mode=self.trading_mode, funding_fees=funding_fees ) @@ -652,7 +754,7 @@ class FreqtradeBot(LoggingMixin): """ msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY, + 'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY, 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -673,11 +775,11 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a entry order cancel occurred. """ - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") - + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side) + msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL, + 'type': msg_type, 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -696,9 +798,10 @@ class FreqtradeBot(LoggingMixin): self.rpc.send_msg(msg) def _notify_enter_fill(self, trade: Trade) -> None: + msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL, + 'type': msg_type, 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -752,6 +855,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) (enter, exit_) = (False, False) + exit_signal_type = "exit_short" if trade.is_short else "exit_long" # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal if (self.config.get('use_sell_signal', True) or @@ -762,15 +866,16 @@ class FreqtradeBot(LoggingMixin): (enter, exit_) = self.strategy.get_exit_signal( trade.pair, self.strategy.timeframe, - analyzed_df, is_short=trade.is_short + analyzed_df, + is_short=trade.is_short ) - # TODO-lev: side should depend on trade side. - exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") + logger.debug('checking exit') + exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side) if self._check_and_execute_exit(trade, exit_rate, enter, exit_): return True - logger.debug('Found no sell signal for %s.', trade) + logger.debug(f'Found no {exit_signal_type} signal for %s.', trade) return False def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: @@ -855,7 +960,10 @@ class FreqtradeBot(LoggingMixin): # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - stop_price = trade.open_rate * (1 + stoploss) + if trade.is_short: + stop_price = trade.open_rate * (1 - stoploss) + else: + stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price): trade.stoploss_last_update = datetime.utcnow() @@ -880,11 +988,11 @@ class FreqtradeBot(LoggingMixin): # 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, side=trade.exit_side) + self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) return False - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: + def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: """ Check to see if stoploss on exchange should be updated in case of trailing stoploss on exchange @@ -892,7 +1000,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - if self.exchange.stoploss_adjust(trade.stop_loss, order, side): + if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side): # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: @@ -918,7 +1026,11 @@ class FreqtradeBot(LoggingMixin): Check and execute trade exit """ should_exit: SellCheckTuple = self.strategy.should_exit( - trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, + trade, + exit_rate, + datetime.now(timezone.utc), + enter=enter, + exit_=exit_, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) @@ -959,24 +1071,23 @@ class FreqtradeBot(LoggingMixin): continue fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) + is_entering = order['side'] == trade.enter_side + not_closed = order['status'] == 'open' or fully_cancelled + side = trade.enter_side if is_entering else trade.exit_side + timed_out = self._check_timed_out(side, order) + time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout' - if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('buy', order) - or strategy_safe_wrapper(self.strategy.check_buy_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) - - elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('sell', order) - or strategy_safe_wrapper(self.strategy.check_sell_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) + if not_closed and (fully_cancelled or timed_out or ( + strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)( + pair=trade.pair, + trade=trade, + order=order + ) + )): + if is_entering: + self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) + else: + self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) def cancel_all_open_orders(self) -> None: """ @@ -991,10 +1102,10 @@ class FreqtradeBot(LoggingMixin): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue - if order['side'] == 'buy': + if order['side'] == trade.enter_side: self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - elif order['side'] == 'sell': + elif order['side'] == trade.exit_side: self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) Trade.commit() @@ -1016,7 +1127,7 @@ class FreqtradeBot(LoggingMixin): if filled_val > 0 and filled_stake < minstake: logger.warning( f"Order {trade.open_order_id} for {trade.pair} not cancelled, " - f"as the filled amount of {filled_val} would result in an unsellable trade.") + f"as the filled amount of {filled_val} would result in an unexitable trade.") return False corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) @@ -1031,12 +1142,16 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('Buy order %s for %s.', reason, trade) + side = trade.enter_side.capitalize() + logger.info('%s order %s for %s.', side, reason, trade) # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): - logger.info('Buy order fully cancelled. Removing %s from database.', trade) + logger.info( + '%s order fully cancelled. Removing %s from database.', + side, trade + ) # if trade is not partially completed, just delete the trade trade.delete() was_trade_fully_canceled = True @@ -1054,11 +1169,11 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None - logger.info('Partial buy order timeout for %s.', trade) + logger.info('Partial %s order timeout for %s.', trade.enter_side, trade) reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() - self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], + self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side], reason=reason) return was_trade_fully_canceled @@ -1076,12 +1191,13 @@ class FreqtradeBot(LoggingMixin): trade.amount) trade.update_order(co) except InvalidOrderException: - logger.exception(f"Could not cancel sell order {trade.open_order_id}") + logger.exception( + f"Could not cancel {trade.exit_side} order {trade.open_order_id}") return 'error cancelling order' - logger.info('Sell order %s for %s.', reason, trade) + logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('Sell order %s for %s.', reason, trade) + logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) trade.update_order(order) trade.close_rate = None @@ -1098,7 +1214,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() self._notify_exit_cancel( trade, - order_type=self.strategy.order_types['sell'], + order_type=self.strategy.order_types[trade.exit_side], reason=reason ) return reason @@ -1129,7 +1245,12 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: + def execute_trade_exit( + self, + trade: Trade, + limit: float, + sell_reason: SellCheckTuple, # TODO-lev update to exit_reason + ) -> bool: """ Executes a trade exit for the given trade and limit :param trade: Trade instance @@ -1137,13 +1258,13 @@ class FreqtradeBot(LoggingMixin): :param sell_reason: Reason the sell was triggered :return: True if it succeeds (supported) False (not supported) """ - sell_type = 'sell' # TODO-lev: Update to exit + exit_type = 'sell' # TODO-lev: Update to exit if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - sell_type = 'stoploss' + exit_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price - if self.config['dry_run'] and sell_type == 'stoploss' \ + if self.config['dry_run'] and exit_type == 'stoploss' \ and self.strategy.order_types['stoploss_on_exchange']: limit = trade.stop_loss @@ -1167,7 +1288,7 @@ class FreqtradeBot(LoggingMixin): except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - order_type = self.strategy.order_types[sell_type] + order_type = self.strategy.order_types[exit_type] if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") @@ -1177,7 +1298,7 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get("forcesell", order_type) amount = self._safe_exit_amount(trade.pair, trade.amount) - time_in_force = self.strategy.order_time_in_force['sell'] + time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, @@ -1191,7 +1312,7 @@ class FreqtradeBot(LoggingMixin): order = self.exchange.create_order( pair=trade.pair, ordertype=order_type, - side="sell", + side=trade.exit_side, amount=amount, rate=limit, time_in_force=time_in_force @@ -1202,7 +1323,7 @@ class FreqtradeBot(LoggingMixin): self.handle_insufficient_funds(trade) return False - order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') + order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side) trade.orders.append(order_obj) trade.open_order_id = order['id'] @@ -1230,7 +1351,7 @@ class FreqtradeBot(LoggingMixin): profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. current_rate = self.exchange.get_rate( - trade.pair, refresh=False, side="sell") if not fill else None + trade.pair, refresh=False, side=trade.exit_side) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" @@ -1275,7 +1396,7 @@ class FreqtradeBot(LoggingMixin): profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side) profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" @@ -1390,7 +1511,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: # Eat into dust if we own more than base currency - # TODO-lev: won't be in "base"(quote) currency for shorts + # TODO-lev: won't be in base currency for shorts logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index bbb390e75..5496628f4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -506,7 +506,6 @@ class LocalTrade(): lower_stop = new_loss < self.stop_loss # stop losses only walk up, never down!, - # TODO-lev # ? But adding more to a leveraged trade would create a lower liquidation price, # ? decreasing the minimum stoploss if (higher_stop and not self.is_short) or (lower_stop and self.is_short): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a22a0b6b8..94541218c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -840,28 +840,32 @@ class IStrategy(ABC, HyperStrategyMixin): else: logger.warning("CustomStoploss function did not return valid stoploss") - if self.trailing_stop and trade.stop_loss < (low or current_rate): + sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short) + sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short) + if self.trailing_stop and (sl_lower_long or sl_higher_short): # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. - # TODO-lev: Check this function - high / low usage must be inversed for short trades! - high_profit = current_profit if not high else trade.calc_profit_ratio(high) + bound = low if trade.is_short else high + bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound) # Don't update stoploss if trailing_only_offset_is_reached is true. - if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): + if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset): # Specific handling for trailing_stop_positive - if self.trailing_stop_positive is not None and high_profit > sl_offset: + if self.trailing_stop_positive is not None and bound_profit > sl_offset: stop_loss_value = self.trailing_stop_positive logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} " f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") - trade.adjust_stop_loss(high or current_rate, stop_loss_value) + trade.adjust_stop_loss(bound or current_rate, stop_loss_value) + sl_higher_short = (trade.stop_loss >= (low or current_rate) and not trade.is_short) + sl_lower_long = ((trade.stop_loss <= (high or current_rate) and trade.is_short)) # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. - if ((trade.stop_loss >= (low or current_rate)) and + if ((sl_higher_short or sl_lower_long) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS @@ -870,12 +874,18 @@ class IStrategy(ABC, HyperStrategyMixin): if trade.initial_stop_loss != trade.stop_loss: sell_type = SellType.TRAILING_STOP_LOSS logger.debug( - f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, " + f"{trade.pair} - HIT STOP: current price at " + f"{((high if trade.is_short else low) or current_rate):.6f}, " f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") + new_stoploss = ( + trade.stop_loss + trade.initial_stop_loss + if trade.is_short else + trade.stop_loss - trade.initial_stop_loss + ) logger.debug(f"{trade.pair} - Trailing stop saved " - f"{trade.stop_loss - trade.initial_stop_loss:.6f}") + f"{new_stoploss:.6f}") return SellCheckTuple(sell_type=sell_type) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 80fa7cdae..13df9c2a8 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -58,6 +58,8 @@ class SampleStrategy(IStrategy): # Hyperoptable parameters buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) + short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) + exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) # Optimal timeframe for the strategy. timeframe = '5m' @@ -354,6 +356,16 @@ class SampleStrategy(IStrategy): ), 'enter_long'] = 1 + dataframe.loc[ + ( + # Signal: RSI crosses above 70 + (qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) & + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'enter_short'] = 1 + return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -371,5 +383,18 @@ class SampleStrategy(IStrategy): (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['volume'] > 0) # Make sure Volume is not 0 ), + 'exit_long'] = 1 + + dataframe.loc[ + ( + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) & + # Guard: tema below BB middle + (dataframe['tema'] <= dataframe['bb_middleband']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'exit_short'] = 1 + return dataframe diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 4e07e299c..d8218c82a 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -903,7 +903,7 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist', return_value=True - ) + ) def fake_iterator(*args, **kwargs): yield from [saved_hyperopt_results] @@ -1309,9 +1309,10 @@ def test_start_list_data(testdatadir, capsys): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: Short trades? def test_show_trades(mocker, fee, capsys, caplog): mocker.patch("freqtrade.persistence.init_db") - create_mock_trades(fee) + create_mock_trades(fee, False) args = [ "show-trades", "--db-url", diff --git a/tests/conftest.py b/tests/conftest.py index 7eec8d171..2c6297d57 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -209,8 +209,14 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, - enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None: +def patch_get_signal( + freqtrade: FreqtradeBot, + enter_long=True, + exit_long=False, + enter_short=False, + exit_short=False, + enter_tag: Optional[str] = None +) -> None: """ :param mocker: mocker to patch IStrategy class :param value: which value IStrategy.get_signal() must return @@ -241,7 +247,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -def create_mock_trades(fee, use_db: bool = True): +def create_mock_trades(fee, is_short: bool, use_db: bool = True): """ Create some fake trades ... """ @@ -252,22 +258,22 @@ def create_mock_trades(fee, use_db: bool = True): LocalTrade.add_bt_trade(trade) # Simulate dry_run entries - trade = mock_trade_1(fee) + trade = mock_trade_1(fee, is_short) add_trade(trade) - trade = mock_trade_2(fee) + trade = mock_trade_2(fee, is_short) add_trade(trade) - trade = mock_trade_3(fee) + trade = mock_trade_3(fee, is_short) add_trade(trade) - trade = mock_trade_4(fee) + trade = mock_trade_4(fee, is_short) add_trade(trade) - trade = mock_trade_5(fee) + trade = mock_trade_5(fee, is_short) add_trade(trade) - trade = mock_trade_6(fee) + trade = mock_trade_6(fee, is_short) add_trade(trade) if use_db: @@ -286,22 +292,22 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): LocalTrade.add_bt_trade(trade) # Simulate dry_run entries - trade = mock_trade_1(fee) + trade = mock_trade_1(fee, False) add_trade(trade) - trade = mock_trade_2(fee) + trade = mock_trade_2(fee, False) add_trade(trade) - trade = mock_trade_3(fee) + trade = mock_trade_3(fee, False) add_trade(trade) - trade = mock_trade_4(fee) + trade = mock_trade_4(fee, False) add_trade(trade) - trade = mock_trade_5(fee) + trade = mock_trade_5(fee, False) add_trade(trade) - trade = mock_trade_6(fee) + trade = mock_trade_6(fee, False) add_trade(trade) trade = short_trade(fee) @@ -324,7 +330,7 @@ def create_mock_trades_usdt(fee, use_db: bool = True): else: LocalTrade.add_bt_trade(trade) - # Simulate dry_run entries + # Simulate dry_run entries trade = mock_trade_usdt_1(fee) add_trade(trade) @@ -2297,6 +2303,7 @@ def limit_sell_order_usdt_open(): 'timestamp': arrow.utcnow().int_timestamp, 'price': 2.20, 'amount': 30.0, + 'cost': 66.0, 'filled': 0.0, 'remaining': 30.0, 'status': 'open' @@ -2342,3 +2349,27 @@ def market_sell_order_usdt(): 'remaining': 0.0, 'status': 'closed' } + + +@pytest.fixture(scope='function') +def limit_order(limit_buy_order_usdt, limit_sell_order_usdt): + return { + 'buy': limit_buy_order_usdt, + 'sell': limit_sell_order_usdt + } + + +@pytest.fixture(scope='function') +def market_order(market_buy_order_usdt, market_sell_order_usdt): + return { + 'buy': market_buy_order_usdt, + 'sell': market_sell_order_usdt + } + + +@pytest.fixture(scope='function') +def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open): + return { + 'buy': limit_buy_order_usdt_open, + 'sell': limit_sell_order_usdt_open + } diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index cf3c970f6..fe88064a3 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -6,12 +6,24 @@ from freqtrade.persistence.models import Order, Trade MOCK_TRADE_COUNT = 6 -def mock_order_1(): +def enter_side(is_short: bool): + return "sell" if is_short else "buy" + + +def exit_side(is_short: bool): + return "buy" if is_short else "sell" + + +def direc(is_short: bool): + return "short" if is_short else "long" + + +def mock_order_1(is_short: bool): return { - 'id': '1234', + 'id': f'1234_{direc(is_short)}', 'symbol': 'ETH/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -20,7 +32,7 @@ def mock_order_1(): } -def mock_trade_1(fee): +def mock_trade_1(fee, is_short: bool): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -32,21 +44,22 @@ def mock_trade_1(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, exchange='binance', - open_order_id='dry_run_buy_12345', + open_order_id=f'dry_run_buy_{direc(is_short)}_12345', strategy='StrategyTestV3', timeframe=5, + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_1(is_short), 'ETH/BTC', enter_side(is_short)) trade.orders.append(o) return trade -def mock_order_2(): +def mock_order_2(is_short: bool): return { - 'id': '1235', + 'id': f'1235_{direc(is_short)}', 'symbol': 'ETC/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -55,12 +68,12 @@ def mock_order_2(): } -def mock_order_2_sell(): +def mock_order_2_sell(is_short: bool): return { - 'id': '12366', + 'id': f'12366_{direc(is_short)}', 'symbol': 'ETC/BTC', 'status': 'closed', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'limit', 'price': 0.128, 'amount': 123.0, @@ -69,7 +82,7 @@ def mock_order_2_sell(): } -def mock_trade_2(fee): +def mock_trade_2(fee, is_short: bool): """ Closed trade... """ @@ -82,30 +95,31 @@ def mock_trade_2(fee): fee_close=fee.return_value, open_rate=0.123, close_rate=0.128, - close_profit=0.005, - close_profit_abs=0.000584127, + close_profit=-0.005 if is_short else 0.005, + close_profit_abs=-0.005584127 if is_short else 0.000584127, exchange='binance', is_open=False, - open_order_id='dry_run_sell_12345', + open_order_id=f'dry_run_sell_{direc(is_short)}_12345', strategy='StrategyTestV3', timeframe=5, sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_2(is_short), 'ETC/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETC/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_2_sell(is_short), 'ETC/BTC', exit_side(is_short)) trade.orders.append(o) return trade -def mock_order_3(): +def mock_order_3(is_short: bool): return { - 'id': '41231a12a', + 'id': f'41231a12a_{direc(is_short)}', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.05, 'amount': 123.0, @@ -114,12 +128,12 @@ def mock_order_3(): } -def mock_order_3_sell(): +def mock_order_3_sell(is_short: bool): return { - 'id': '41231a666a', + 'id': f'41231a666a_{direc(is_short)}', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'stop_loss_limit', 'price': 0.06, 'average': 0.06, @@ -129,7 +143,7 @@ def mock_order_3_sell(): } -def mock_trade_3(fee): +def mock_trade_3(fee, is_short: bool): """ Closed trade """ @@ -142,8 +156,8 @@ def mock_trade_3(fee): fee_close=fee.return_value, open_rate=0.05, close_rate=0.06, - close_profit=0.01, - close_profit_abs=0.000155, + close_profit=-0.01 if is_short else 0.01, + close_profit_abs=-0.001155 if is_short else 0.000155, exchange='binance', is_open=False, strategy='StrategyTestV3', @@ -151,20 +165,21 @@ def mock_trade_3(fee): sell_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc), + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_3(is_short), 'XRP/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'XRP/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_3_sell(is_short), 'XRP/BTC', exit_side(is_short)) trade.orders.append(o) return trade -def mock_order_4(): +def mock_order_4(is_short: bool): return { - 'id': 'prod_buy_12345', + 'id': f'prod_buy_{direc(is_short)}_12345', 'symbol': 'ETC/BTC', 'status': 'open', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -173,7 +188,7 @@ def mock_order_4(): } -def mock_trade_4(fee): +def mock_trade_4(fee, is_short: bool): """ Simulate prod entry """ @@ -188,21 +203,22 @@ def mock_trade_4(fee): is_open=True, open_rate=0.123, exchange='binance', - open_order_id='prod_buy_12345', + open_order_id=f'prod_buy_{direc(is_short)}_12345', strategy='StrategyTestV3', timeframe=5, + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', enter_side(is_short)) trade.orders.append(o) return trade -def mock_order_5(): +def mock_order_5(is_short: bool): return { - 'id': 'prod_buy_3455', + 'id': f'prod_buy_{direc(is_short)}_3455', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -211,12 +227,12 @@ def mock_order_5(): } -def mock_order_5_stoploss(): +def mock_order_5_stoploss(is_short: bool): return { - 'id': 'prod_stoploss_3455', + 'id': f'prod_stoploss_{direc(is_short)}_3455', 'symbol': 'XRP/BTC', 'status': 'open', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'stop_loss_limit', 'price': 0.123, 'amount': 123.0, @@ -225,7 +241,7 @@ def mock_order_5_stoploss(): } -def mock_trade_5(fee): +def mock_trade_5(fee, is_short: bool): """ Simulate prod entry with stoploss """ @@ -241,22 +257,23 @@ def mock_trade_5(fee): open_rate=0.123, exchange='binance', strategy='SampleStrategy', - stoploss_order_id='prod_stoploss_3455', + stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455', timeframe=5, + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'XRP/BTC', 'stoploss') + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(is_short), 'XRP/BTC', 'stoploss') trade.orders.append(o) return trade -def mock_order_6(): +def mock_order_6(is_short: bool): return { - 'id': 'prod_buy_6', + 'id': f'prod_buy_{direc(is_short)}_6', 'symbol': 'LTC/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.15, 'amount': 2.0, @@ -265,23 +282,23 @@ def mock_order_6(): } -def mock_order_6_sell(): +def mock_order_6_sell(is_short: bool): return { - 'id': 'prod_sell_6', + 'id': f'prod_sell_{direc(is_short)}_6', 'symbol': 'LTC/BTC', 'status': 'open', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'limit', - 'price': 0.20, + 'price': 0.15 if is_short else 0.20, 'amount': 2.0, 'filled': 0.0, 'remaining': 2.0, } -def mock_trade_6(fee): +def mock_trade_6(fee, is_short: bool): """ - Simulate prod entry with open sell order + Simulate prod entry with open exit order """ trade = Trade( pair='LTC/BTC', @@ -295,12 +312,12 @@ def mock_trade_6(fee): open_rate=0.15, exchange='binance', strategy='SampleStrategy', - open_order_id="prod_sell_6", + open_order_id=f"prod_sell_{direc(is_short)}_6", timeframe=5, ) - o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_6(is_short), 'LTC/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_6_sell(is_short), 'LTC/BTC', exit_side(is_short)) trade.orders.append(o) return trade diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index e7b8c5b2f..94cea62eb 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -111,9 +111,10 @@ def test_load_backtest_data_multi(testdatadir): @pytest.mark.usefixtures("init_persistence") -def test_load_trades_from_db(default_conf, fee, mocker): +@pytest.mark.parametrize('is_short', [False, True]) +def test_load_trades_from_db(default_conf, fee, is_short, mocker): - create_mock_trades(fee) + create_mock_trades(fee, is_short) # remove init so it does not init again init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock()) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index f0033b2d7..641d2f263 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -164,6 +164,8 @@ def test_get_balances_prod(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "get_balances", "fetch_balance") +# TODO-lev: All these stoploss tests with shorts + @pytest.mark.parametrize('ordertype', ['market', 'limit']) @pytest.mark.parametrize('side,adjustedprice', [ diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index c6246dccb..93eebde82 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -679,7 +679,7 @@ def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: - create_mock_trades(fee) + create_mock_trades(fee, False) pm.refresh_pairlist() assert pm.whitelist == ['XRP/BTC'] assert log_has_re(r'Removing pair .* since .* is below .*', caplog) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 2f8fb9b85..0d349abf3 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -289,7 +289,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency) -def test_rpc_trade_history(mocker, default_conf, markets, fee): +@pytest.mark.parametrize('is_short', [True, False]) +def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -297,7 +298,7 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee): ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - create_mock_trades(fee) + create_mock_trades(fee, is_short) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() trades = rpc._rpc_trade_history(2) @@ -314,7 +315,8 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee): assert trades['trades'][0]['pair'] == 'XRP/BTC' -def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): +@pytest.mark.parametrize('is_short', [True, False]) +def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) stoploss_mock = MagicMock() cancel_mock = MagicMock() @@ -327,7 +329,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot.strategy.order_types['stoploss_on_exchange'] = True - create_mock_trades(fee) + create_mock_trades(fee, is_short) rpc = RPC(freqtradebot) with pytest.raises(RPCException, match='invalid argument'): rpc._rpc_delete('200') diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 18a84a19e..c9ffb4f34 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -458,7 +458,8 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): assert 'starting_capital_ratio' in response -def test_api_count(botclient, mocker, ticker, fee, markets): +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_count(botclient, mocker, ticker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -475,7 +476,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json()["max"] == 1 # Create some test data - create_mock_trades(fee) + create_mock_trades(fee, is_short) rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json()["current"] == 4 @@ -556,7 +557,8 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date()) -def test_api_trades(botclient, mocker, fee, markets): +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_trades(botclient, mocker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -569,7 +571,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 0 assert rc.json()['total_trades'] == 0 - create_mock_trades(fee) + create_mock_trades(fee, is_short) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trades") @@ -584,6 +586,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['total_trades'] == 2 +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_api_trade_single(botclient, mocker, fee, ticker, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -596,7 +599,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets): assert_response(rc, 404) assert rc.json()['detail'] == 'Trade not found.' - create_mock_trades(fee) + create_mock_trades(fee, False) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trade/3") @@ -604,6 +607,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets): assert rc.json()['trade_id'] == 3 +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_api_delete_trade(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -619,7 +623,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): # Error - trade won't exist yet. assert_response(rc, 502) - create_mock_trades(fee) + create_mock_trades(fee, False) ftbot.strategy.order_types['stoploss_on_exchange'] = True trades = Trade.query.all() @@ -695,6 +699,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_api_profit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -710,7 +715,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): assert_response(rc, 200) assert rc.json()['trade_count'] == 0 - create_mock_trades(fee) + create_mock_trades(fee, False) # Simulate fulfilled LIMIT_BUY order for trade rc = client_get(client, f"{BASE_URI}/profit") @@ -746,7 +751,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") -def test_api_stats(botclient, mocker, ticker, fee, markets,): +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) +def test_api_stats(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -762,7 +768,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,): assert 'durations' in rc.json() assert 'sell_reasons' in rc.json() - create_mock_trades(fee) + create_mock_trades(fee, False) rc = client_get(client, f"{BASE_URI}/stats") assert_response(rc, 200) @@ -820,6 +826,10 @@ def test_api_performance(botclient, fee): {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}] +# TODO-lev: @pytest.mark.parametrize('is_short,side', [ +# (True, "short"), +# (False, "long") +# ]) def test_api_status(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -835,7 +845,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) assert rc.json() == [] - create_mock_trades(fee) + create_mock_trades(fee, False) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) @@ -888,7 +898,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'is_open': True, 'max_rate': ANY, 'min_rate': ANY, - 'open_order_id': 'dry_run_buy_12345', + 'open_order_id': 'dry_run_buy_long_12345', 'open_rate_requested': ANY, 'open_trade_value': 15.1668225, 'sell_reason': None, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 791797ceb..c81fda101 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -33,6 +33,7 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ + def __init__(self, rpc: RPC, config) -> None: super().__init__(rpc, config) self.state = {'called': False} @@ -479,8 +480,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] +@pytest.mark.parametrize('is_short', [True, False]) def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + limit_buy_order, limit_sell_order, mocker, is_short) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -496,7 +498,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Create some test data - create_mock_trades(fee) + create_mock_trades(fee, is_short) telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -997,9 +999,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg = ('
current max total stake\n--------- ----- -------------\n' ' 1 {} {}').format( - default_conf['max_open_trades'], - default_conf['stake_amount'] - ) + default_conf['max_open_trades'], + default_conf['stake_amount'] + ) assert msg in msg_mock.call_args_list[0][0][0] @@ -1159,6 +1161,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Winrate' not in msg_mock.call_args_list[0][0][0] +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_telegram_trades(mocker, update, default_conf, fee): telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1177,7 +1180,7 @@ def test_telegram_trades(mocker, update, default_conf, fee): assert "
" not in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - create_mock_trades(fee) + create_mock_trades(fee, False) context = MagicMock() context.args = [5] @@ -1191,6 +1194,7 @@ def test_telegram_trades(mocker, update, default_conf, fee): msg_mock.call_args_list[0][0][0])) +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_telegram_delete_trade(mocker, update, default_conf, fee): telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1201,7 +1205,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): assert "Trade-id not set." in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - create_mock_trades(fee) + create_mock_trades(fee, False) context = MagicMock() context.args = [1] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a9334c616..e735ab181 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -47,8 +47,8 @@ def test_returns_latest_signal(ohlcv_history): mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'enter_long'] = 1 - assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history - ) == (SignalDirection.LONG, None) + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, None) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) mocked_history.loc[1, 'exit_long'] = 0 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b77c42d15..6d784d9d1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -11,7 +11,7 @@ import arrow import pytest from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import RPCMessageType, RunMode, SellType, State, TradingMode +from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) @@ -23,9 +23,9 @@ from freqtrade.worker import Worker from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, - mock_order_3, mock_order_3_sell, mock_order_4, - mock_order_5_stoploss, mock_order_6_sell) +from tests.conftest_trades import (MOCK_TRADE_COUNT, enter_side, exit_side, mock_order_1, + mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, + mock_order_4, mock_order_5_stoploss, mock_order_6_sell) def patch_RPCManager(mocker) -> MagicMock: @@ -41,6 +41,7 @@ def patch_RPCManager(mocker) -> MagicMock: # Unit tests + def test_freqtradebot_state(mocker, default_conf_usdt, markets) -> None: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -195,12 +196,10 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: @pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [ - # Override stoploss - (0.79, False), - # Override strategy stoploss - (0.85, True) + (0.79, False), # Override stoploss + (0.85, True), # Override strategy stoploss ]) -def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker, +def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, buy_price_mult, ignore_strat_sl, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -210,14 +209,14 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker, # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 # Thus, if price falls 21%, stoploss should be triggered # - # mocking the ticker_usdt: price is falling ... - buy_price = limit_buy_order_usdt['price'] + # mocking the ticker: price is falling ... + enter_price = limit_order['buy']['price'] mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * buy_price_mult, - 'ask': buy_price * buy_price_mult, - 'last': buy_price * buy_price_mult, + 'bid': enter_price * buy_price_mult, + 'ask': enter_price * buy_price_mult, + 'last': enter_price * buy_price_mult, }), get_fee=fee, ) @@ -230,7 +229,8 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker, freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + caplog.clear() + trade.update(limit_order['buy']) ############################################# # stoploss shoud be hit @@ -271,7 +271,12 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) - assert Trade.total_open_trades_stakes() == 120.0 -def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, fee, mocker) -> None: +@pytest.mark.parametrize("is_short,open_rate", [ + (False, 2.0), + (True, 2.2) +]) +def test_create_trade(default_conf_usdt, ticker_usdt, limit_order, + fee, mocker, is_short, open_rate) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -284,10 +289,11 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, fee, # Save state of current whitelist whitelist = deepcopy(default_conf_usdt['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.create_trade('ETH/USDT') trade = Trade.query.first() + trade.is_short = is_short assert trade is not None assert trade.stake_amount == 60.0 assert trade.is_open @@ -295,9 +301,9 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, fee, assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order_usdt) + trade.update(limit_order[enter_side(is_short)]) - assert trade.open_rate == 2.0 + assert trade.open_rate == open_rate assert trade.amount == 30.0 assert whitelist == default_conf_usdt['exchange']['pair_whitelist'] @@ -319,6 +325,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke freqtrade.create_trade('ETH/USDT') +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ (5.0, True, True, 99), (0.00005, True, False, 99), @@ -326,27 +333,27 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke (UNLIMITED_STAKE_AMOUNT, False, True, 0), ]) def test_create_trade_minimal_amount( - default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, fee, mocker, - stake_amount, create, amount_enough, max_open_trades, caplog + default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, + stake_amount, create, amount_enough, max_open_trades, caplog, is_short ) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_usdt_open) + enter_mock = MagicMock(return_value=limit_order_open[enter_side(is_short)]) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, - create_order=buy_mock, + create_order=enter_mock, get_fee=fee, ) default_conf_usdt['max_open_trades'] = max_open_trades freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.config['stake_amount'] = stake_amount - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) if create: assert freqtrade.create_trade('ETH/USDT') if amount_enough: - rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] + rate, amount = enter_mock.call_args[1]['rate'], enter_mock.call_args[1]['amount'] assert rate * amount <= default_conf_usdt['stake_amount'] else: assert log_has_re( @@ -416,7 +423,8 @@ def test_enter_positions_global_pairlock(default_conf_usdt, ticker_usdt, limit_b assert log_has_re(message, caplog) -def test_handle_protections(mocker, default_conf_usdt, fee): +@pytest.mark.parametrize('is_short', [False, True]) +def test_handle_protections(mocker, default_conf_usdt, fee, is_short): default_conf_usdt['protections'] = [ {"method": "CooldownPeriod", "stop_duration": 60}, { @@ -431,7 +439,7 @@ def test_handle_protections(mocker, default_conf_usdt, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade.protections._protection_handlers[1].global_stop = MagicMock( return_value=(True, arrow.utcnow().shift(hours=1).datetime, "asdf")) - create_mock_trades(fee) + create_mock_trades(fee, is_short) freqtrade.handle_protections('ETC/BTC') send_msg_mock = freqtrade.rpc.send_msg assert send_msg_mock.call_count == 2 @@ -450,7 +458,8 @@ def test_create_trade_no_signal(default_conf_usdt, fee, mocker) -> None: ) default_conf_usdt['stake_amount'] = 10 freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_long=False) + # patch_get_signal(freqtrade, enter_long=False) + patch_get_signal(freqtrade, enter_long=False, exit_long=False) Trade.query = MagicMock() Trade.query.filter = MagicMock() @@ -516,19 +525,24 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, assert len(trades) == 4 -def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, - limit_buy_order_usdt_open, fee, mocker, caplog) -> None: +@pytest.mark.parametrize('is_short, open_rate', [ + (False, 2.0), + (True, 2.02) +]) +def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, limit_order_open, + is_short, open_rate, fee, mocker, caplog + ) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, - create_order=MagicMock(return_value=limit_buy_order_usdt_open), - fetch_order=MagicMock(return_value=limit_buy_order_usdt), + create_order=MagicMock(return_value=limit_order_open[enter_side(is_short)]), + fetch_order=MagicMock(return_value=limit_order[enter_side(is_short)]), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades @@ -543,11 +557,12 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_buy_order_ assert trade.is_open assert trade.open_date is not None assert trade.exchange == 'binance' - assert trade.open_rate == 2.0 - assert trade.amount == 30.0 + assert trade.open_rate == open_rate # TODO-lev: I think? That's what the ticker ask price is + assert isclose(trade.amount, 60 / open_rate) assert log_has( - 'Buy signal found: about create a new trade for ETH/USDT with stake_amount: 60.0 ...', + f'{"Short" if is_short else "Long"} signal found: about create a new trade for ETH/USDT ' + 'with stake_amount: 60.0 ...', caplog ) @@ -695,148 +710,160 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) assert ("ETH/USDT", default_conf_usdt["timeframe"]) in refresh_mock.call_args[0][0] -def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, - limit_buy_order_usdt_open) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, + limit_order_open, is_short) -> None: + + open_order = limit_order_open[enter_side(is_short)] + order = limit_order[enter_side(is_short)] + patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) stake_amount = 2 bid = 0.11 - buy_rate_mock = MagicMock(return_value=bid) - buy_mm = MagicMock(return_value=limit_buy_order_usdt_open) + enter_rate_mock = MagicMock(return_value=bid) + enter_mm = MagicMock(return_value=open_order) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_rate=buy_rate_mock, + get_rate=enter_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, 'last': 1.9 }), - create_order=buy_mm, + create_order=enter_mm, get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) pair = 'ETH/USDT' - assert not freqtrade.execute_entry(pair, stake_amount) - assert buy_rate_mock.call_count == 1 - assert buy_mm.call_count == 0 + assert not freqtrade.execute_entry(pair, stake_amount, is_short=is_short) + assert enter_rate_mock.call_count == 1 + assert enter_mm.call_count == 0 assert freqtrade.strategy.confirm_trade_entry.call_count == 1 - buy_rate_mock.reset_mock() + enter_rate_mock.reset_mock() - limit_buy_order_usdt_open['id'] = '22' + open_order['id'] = '22' freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_entry(pair, stake_amount) - assert buy_rate_mock.call_count == 1 - assert buy_mm.call_count == 1 - call_args = buy_mm.call_args_list[0][1] + assert enter_rate_mock.call_count == 1 + assert enter_mm.call_count == 1 + call_args = enter_mm.call_args_list[0][1] assert call_args['pair'] == pair assert call_args['rate'] == bid assert call_args['amount'] == round(stake_amount / bid, 8) - buy_rate_mock.reset_mock() + enter_rate_mock.reset_mock() # Should create an open trade with an open order id # As the order is not fulfilled yet trade = Trade.query.first() + trade.is_short = is_short assert trade assert trade.is_open is True assert trade.open_order_id == '22' # Test calling with price - limit_buy_order_usdt_open['id'] = '33' + open_order['id'] = '33' fix_price = 0.06 - assert freqtrade.execute_entry(pair, stake_amount, fix_price) + assert freqtrade.execute_entry(pair, stake_amount, fix_price, is_short=is_short) # Make sure get_rate wasn't called again - assert buy_rate_mock.call_count == 0 + assert enter_rate_mock.call_count == 0 - assert buy_mm.call_count == 2 - call_args = buy_mm.call_args_list[1][1] + assert enter_mm.call_count == 2 + call_args = enter_mm.call_args_list[1][1] assert call_args['pair'] == pair assert call_args['rate'] == fix_price assert call_args['amount'] == round(stake_amount / fix_price, 8) # In case of closed order - limit_buy_order_usdt['status'] = 'closed' - limit_buy_order_usdt['price'] = 10 - limit_buy_order_usdt['cost'] = 100 - limit_buy_order_usdt['id'] = '444' + order['status'] = 'closed' + order['price'] = 10 + order['cost'] = 100 + order['id'] = '444' mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order_usdt)) - assert freqtrade.execute_entry(pair, stake_amount) + MagicMock(return_value=order)) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[2] + trade.is_short = is_short assert trade assert trade.open_order_id is None assert trade.open_rate == 10 assert trade.stake_amount == 100 # In case of rejected or expired order and partially filled - limit_buy_order_usdt['status'] = 'expired' - limit_buy_order_usdt['amount'] = 30.0 - limit_buy_order_usdt['filled'] = 20.0 - limit_buy_order_usdt['remaining'] = 10.00 - limit_buy_order_usdt['price'] = 0.5 - limit_buy_order_usdt['cost'] = 15.0 - limit_buy_order_usdt['id'] = '555' + order['status'] = 'expired' + order['amount'] = 30.0 + order['filled'] = 20.0 + order['remaining'] = 10.00 + order['price'] = 0.5 + order['cost'] = 15.0 + order['id'] = '555' mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order_usdt)) + MagicMock(return_value=order)) assert freqtrade.execute_entry(pair, stake_amount) trade = Trade.query.all()[3] + trade.is_short = is_short assert trade assert trade.open_order_id == '555' assert trade.open_rate == 0.5 assert trade.stake_amount == 15.0 # Test with custom stake - limit_buy_order_usdt['status'] = 'open' - limit_buy_order_usdt['id'] = '556' + order['status'] = 'open' + order['id'] = '556' freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0 - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[4] + trade.is_short = is_short assert trade assert trade.stake_amount == 150 # Exception case - limit_buy_order_usdt['id'] = '557' + order['id'] = '557' freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[5] + trade.is_short = is_short assert trade assert trade.stake_amount == 2.0 # In case of the order is rejected and not filled at all - limit_buy_order_usdt['status'] = 'rejected' - limit_buy_order_usdt['amount'] = 30.0 - limit_buy_order_usdt['filled'] = 0.0 - limit_buy_order_usdt['remaining'] = 30.0 - limit_buy_order_usdt['price'] = 0.5 - limit_buy_order_usdt['cost'] = 0.0 - limit_buy_order_usdt['id'] = '66' + order['status'] = 'rejected' + order['amount'] = 30.0 + order['filled'] = 0.0 + order['remaining'] = 30.0 + order['price'] = 0.5 + order['cost'] = 0.0 + order['id'] = '66' mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order_usdt)) + MagicMock(return_value=order)) assert not freqtrade.execute_entry(pair, stake_amount) # Fail to get price... mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) - with pytest.raises(PricingError, match="Could not determine buy price."): - freqtrade.execute_entry(pair, stake_amount) + with pytest.raises(PricingError, match=f"Could not determine {enter_side(is_short)} price."): + freqtrade.execute_entry(pair, stake_amount, is_short=is_short) # In case of custom entry price mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.50) - limit_buy_order_usdt['status'] = 'open' - limit_buy_order_usdt['id'] = '5566' + order['status'] = 'open' + order['id'] = '5566' freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508 - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[6] + trade.is_short = is_short assert trade assert trade.open_rate_requested == 0.508 # In case of custom entry price set to None - limit_buy_order_usdt['status'] = 'open' - limit_buy_order_usdt['id'] = '5567' + + order['status'] = 'open' + order['id'] = '5567' freqtrade.strategy.custom_entry_price = lambda **kwargs: None mocker.patch.multiple( @@ -844,22 +871,25 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, get_rate=MagicMock(return_value=10), ) - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[7] + trade.is_short = is_short assert trade assert trade.open_rate_requested == 10 # In case of custom entry price not float type - limit_buy_order_usdt['status'] = 'open' - limit_buy_order_usdt['id'] = '5568' + order['status'] = 'open' + order['id'] = '5568' freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[8] + trade.is_short = is_short assert trade assert trade.open_rate_requested == 10 -def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_order_usdt) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order, is_short) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -868,7 +898,7 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_o 'ask': 2.2, 'last': 1.9 }), - create_order=MagicMock(return_value=limit_buy_order_usdt), + create_order=MagicMock(return_value=limit_order[enter_side(is_short)]), get_rate=MagicMock(return_value=0.11), get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, @@ -877,13 +907,14 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_o pair = 'ETH/USDT' freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) + # TODO-lev: KeyError happens on short, why? assert freqtrade.execute_entry(pair, stake_amount) - limit_buy_order_usdt['id'] = '222' + limit_order[enter_side(is_short)]['id'] = '222' freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) assert freqtrade.execute_entry(pair, stake_amount) - limit_buy_order_usdt['id'] = '2223' + limit_order[enter_side(is_short)]['id'] = '2223' freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_entry(pair, stake_amount) @@ -891,14 +922,16 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_o assert not freqtrade.execute_entry(pair, stake_amount) -def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_buy_order_usdt) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) + order = limit_order[enter_side(is_short)] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', - return_value=limit_buy_order_usdt['amount']) + return_value=order['amount']) stoploss = MagicMock(return_value={'id': 13434334}) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) @@ -907,6 +940,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_buy_order_usd freqtrade.strategy.order_types['stoploss_on_exchange'] = True trade = MagicMock() + trade.is_short = is_short trade.open_order_id = None trade.stoploss_order_id = None trade.is_open = True @@ -918,9 +952,12 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_buy_order_usd assert trade.is_open is True -def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, - limit_buy_order_usdt, limit_sell_order_usdt) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short, + limit_order) -> None: stoploss = MagicMock(return_value={'id': 13434334}) + enter_order = limit_order[enter_side(is_short)] + exit_order = limit_order[exit_side(is_short)] patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -931,8 +968,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order_usdt['id']}, - {'id': limit_sell_order_usdt['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -941,15 +978,17 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, stoploss=stoploss ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + 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 trade = MagicMock() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = None + trade.is_short = is_short assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 @@ -988,6 +1027,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, caplog.clear() freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 @@ -999,7 +1039,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, 'type': 'stop_loss_limit', 'price': 3, 'average': 2, - 'amount': limit_buy_order_usdt['amount'], + 'amount': enter_order['amount'], }) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True @@ -1038,9 +1078,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, assert stoploss.call_count == 0 -def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, - limit_buy_order_usdt, limit_sell_order_usdt) -> None: +@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[enter_side(is_short)] + exit_order = limit_order[exit_side(is_short)] patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1051,8 +1094,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order_usdt['id']}, - {'id': limit_sell_order_usdt['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1062,10 +1105,11 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 @@ -1077,13 +1121,17 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, assert trade.is_open is True -def test_create_stoploss_order_invalid_order(mocker, default_conf_usdt, caplog, fee, - limit_buy_order_usdt_open, limit_sell_order_usdt): +@pytest.mark.parametrize("is_short", [False, True]) +def test_create_stoploss_order_invalid_order( + mocker, default_conf_usdt, caplog, fee, is_short, limit_order, limit_order_open +): + open_order = limit_order_open[enter_side(is_short)] + order = limit_order[exit_side(is_short)] rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) create_order_mock = MagicMock(side_effect=[ - limit_buy_order_usdt_open, - {'id': limit_sell_order_usdt['id']} + open_order, + {'id': order['id']} ]) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1101,11 +1149,12 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf_usdt, caplog, stoploss=MagicMock(side_effect=InvalidOrderException()), ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + 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.query.first() + trade.is_short = is_short caplog.clear() freqtrade.create_stoploss_order(trade, 200) assert trade.stoploss_order_id is None @@ -1125,9 +1174,12 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf_usdt, caplog, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' -def test_create_stoploss_order_insufficient_funds(mocker, default_conf_usdt, caplog, fee, - limit_buy_order_usdt_open, limit_sell_order_usdt): - sell_mock = MagicMock(return_value={'id': limit_sell_order_usdt['id']}) +@pytest.mark.parametrize("is_short", [False, True]) +def test_create_stoploss_order_insufficient_funds( + mocker, default_conf_usdt, caplog, fee, limit_order_open, + 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') @@ -1139,8 +1191,8 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf_usdt, cap 'last': 1.9 }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, - sell_mock, + limit_order[enter_side(is_short)], + exit_order, ]), get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), @@ -1149,11 +1201,12 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf_usdt, cap 'freqtrade.exchange.Binance', stoploss=MagicMock(side_effect=InsufficientFundsError()), ) - patch_get_signal(freqtrade) + 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.query.first() + trade.is_short = is_short caplog.clear() freqtrade.create_stoploss_order(trade, 200) # stoploss_orderid was empty before @@ -1168,11 +1221,18 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf_usdt, cap assert mock_insuf.call_count == 1 +@pytest.mark.parametrize("is_short,bid,ask,stop_price,amt,hang_price", [ + (False, [4.38, 4.16], [4.4, 4.17], ['2.0805', 4.4 * 0.95], 27.39726027, 3), + (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.09 * 1.05], 27.27272727, 1.5), +]) @pytest.mark.usefixtures("init_persistence") -def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, - limit_buy_order_usdt, limit_sell_order_usdt) -> None: +def test_handle_stoploss_on_exchange_trailing( + mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, amt, hang_price +) -> None: # TODO-lev: test for short # When trailing stoploss is set + enter_order = limit_order[enter_side(is_short)] + exit_order = limit_order[exit_side(is_short)] stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -1180,11 +1240,11 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, fetch_ticker=MagicMock(return_value={ 'bid': 2.19, 'ask': 2.2, - 'last': 2.19 + 'last': 2.19, }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order_usdt['id']}, - {'id': limit_sell_order_usdt['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1206,15 +1266,16 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, freqtrade.strategy.order_types['stoploss_on_exchange'] = True # setting stoploss - freqtrade.strategy.stoploss = -0.05 + 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) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 @@ -1223,10 +1284,10 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, 'id': 100, 'status': 'open', 'type': 'stop_loss_limit', - 'price': 3, + 'price': hang_price, 'average': 2, 'info': { - 'stopPrice': '2.0805' + 'stopPrice': stop_price[0] } }) @@ -1240,9 +1301,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, mocker.patch( 'freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': 4.38, - 'ask': 4.4, - 'last': 4.38 + 'bid': bid[0], + 'ask': ask[0], + 'last': bid[0], }) ) @@ -1258,7 +1319,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, stoploss_order_mock.assert_not_called() assert freqtrade.handle_trade(trade) is False - assert trade.stop_loss == 4.4 * 0.95 + assert trade.stop_loss == stop_price[1] # setting stoploss_on_exchange_interval to 0 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 @@ -1267,11 +1328,11 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') stoploss_order_mock.assert_called_once_with( - amount=27.39726027, + amount=amt, pair='ETH/USDT', order_types=freqtrade.strategy.order_types, - stop_price=4.4 * 0.95, - side="sell", + stop_price=stop_price[1], + side=exit_side(is_short), leverage=1.0 ) @@ -1279,18 +1340,20 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, mocker.patch( 'freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': 4.16, - 'ask': 4.17, - 'last': 4.16 + 'bid': bid[1], + 'ask': ask[1], + 'last': bid[1], }) ) assert freqtrade.handle_trade(trade) is True +@pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange_trailing_error( - mocker, default_conf_usdt, fee, caplog, limit_buy_order_usdt, limit_sell_order_usdt + mocker, default_conf_usdt, fee, caplog, limit_order, is_short ) -> None: - # TODO-lev: test for short + enter_order = limit_order[enter_side(is_short)] + exit_order = limit_order[exit_side(is_short)] # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) @@ -1303,8 +1366,8 @@ def test_handle_stoploss_on_exchange_trailing_error( 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order_usdt['id']}, - {'id': limit_sell_order_usdt['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1322,18 +1385,20 @@ def test_handle_stoploss_on_exchange_trailing_error( freqtrade.strategy.order_types['stoploss_on_exchange'] = True # setting stoploss - freqtrade.strategy.stoploss = -0.05 + 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) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = "abcd" trade.stop_loss = 0.2 trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None) + trade.is_short = is_short stoploss_order_hanging = { 'id': "abcd", @@ -1349,7 +1414,7 @@ def test_handle_stoploss_on_exchange_trailing_error( side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', return_value=stoploss_order_hanging) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") + 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 @@ -1359,14 +1424,17 @@ def test_handle_stoploss_on_exchange_trailing_error( caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) -@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange_custom_stop( - mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_sell_order_usdt) -> None: + mocker, default_conf_usdt, fee, is_short, limit_order +) -> None: + enter_order = limit_order[enter_side(is_short)] + exit_order = limit_order[exit_side(is_short)] # When trailing stoploss is set # TODO-lev: test for short stoploss = MagicMock(return_value={'id': 13434334}) @@ -1379,8 +1447,8 @@ def test_handle_stoploss_on_exchange_custom_stop( 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order_usdt['id']}, - {'id': limit_sell_order_usdt['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1407,10 +1475,11 @@ def test_handle_stoploss_on_exchange_custom_stop( # setting stoploss_on_exchange_interval to 60 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 @@ -1435,9 +1504,9 @@ def test_handle_stoploss_on_exchange_custom_stop( mocker.patch( 'freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': 4.38, - 'ask': 4.4, - 'last': 4.38 + '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, }) ) @@ -1453,8 +1522,8 @@ def test_handle_stoploss_on_exchange_custom_stop( stoploss_order_mock.assert_not_called() assert freqtrade.handle_trade(trade) is False - assert trade.stop_loss == 4.4 * 0.96 - assert trade.stop_loss_pct == -0.04 + 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 @@ -1462,12 +1531,13 @@ def test_handle_stoploss_on_exchange_custom_stop( assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') + # Long uses modified ask - offset, short modified bid + offset stoploss_order_mock.assert_called_once_with( - amount=31.57894736, + amount=trade.amount, pair='ETH/USDT', order_types=freqtrade.strategy.order_types, - stop_price=4.4 * 0.96, - side="sell", + stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04, + side=exit_side(is_short), leverage=1.0 ) @@ -1483,9 +1553,12 @@ def test_handle_stoploss_on_exchange_custom_stop( assert freqtrade.handle_trade(trade) is True -def test_tsl_on_exchange_compatible_with_edge( - mocker, edge_conf, fee, limit_buy_order_usdt, limit_sell_order_usdt) -> None: - # TODO-lev: test for short +def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, + limit_order) -> None: + + enter_order = limit_order['buy'] + exit_order = limit_order['sell'] + # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -1502,8 +1575,8 @@ def test_tsl_on_exchange_compatible_with_edge( 'last': 2.19 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order_usdt['id']}, - {'id': limit_sell_order_usdt['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, stoploss=stoploss, @@ -1594,7 +1667,7 @@ def test_tsl_on_exchange_compatible_with_edge( pair='NEO/BTC', order_types=freqtrade.strategy.order_types, stop_price=4.4 * 0.99, - side="sell", + side='sell', leverage=1.0 ) @@ -1622,11 +1695,47 @@ def test_enter_positions(mocker, default_conf_usdt, return_value, side_effect, assert mock_ct.call_count == len(default_conf_usdt['exchange']['pair_whitelist']) -def test_exit_positions_exception(mocker, default_conf_usdt, limit_buy_order_usdt, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_exit_positions( + mocker, default_conf_usdt, limit_order, is_short, caplog +) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt) + + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + return_value=limit_order[enter_side(is_short)]) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', + return_value=limit_order[enter_side(is_short)]['amount']) trade = MagicMock() + trade.is_short = is_short + trade.open_order_id = '123' + trade.open_fee = 0.001 + trades = [trade] + n = freqtrade.exit_positions(trades) + assert n == 0 + # Test amount not modified by fee-logic + assert not log_has( + 'Applying fee to amount for Trade {} from 30.0 to 90.81'.format(trade), caplog + ) + + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) + # test amount modified by fee-logic + n = freqtrade.exit_positions(trades) + assert n == 0 + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_exit_positions_exception( + mocker, default_conf_usdt, limit_order, caplog, is_short +) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + order = limit_order[enter_side(is_short)] + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) + + trade = MagicMock() + trade.is_short = is_short trade.open_order_id = None trade.open_fee = 0.001 trade.pair = 'ETH/USDT' @@ -1637,19 +1746,24 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_buy_order_usd 'freqtrade.freqtradebot.FreqtradeBot.handle_trade', side_effect=DependencyException() ) + caplog.clear() n = freqtrade.exit_positions(trades) assert n == 0 assert log_has('Unable to exit trade ETH/USDT: ', caplog) -def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_trade_state( + mocker, default_conf_usdt, limit_order, is_short, caplog +) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + order = limit_order[enter_side(is_short)] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', - return_value=limit_buy_order_usdt['amount']) + return_value=order['amount']) trade = Trade( open_order_id=123, @@ -1659,6 +1773,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap open_date=arrow.utcnow().datetime, amount=11, exchange="binance", + is_short=is_short ) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) @@ -1669,7 +1784,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap assert not log_has_re(r'Applying fee to .*', caplog) caplog.clear() assert trade.open_order_id is None - assert trade.amount == limit_buy_order_usdt['amount'] + assert trade.amount == order['amount'] trade.open_order_id = '123' mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) @@ -1687,12 +1802,16 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap assert log_has_re('Found open order for.*', caplog) +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize('initial_amount,has_rounding_fee', [ (30.0 + 1e-14, True), (8.0, False) ]) -def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, limit_buy_order_usdt, - fee, mocker, initial_amount, has_rounding_fee, caplog): +def test_update_trade_state_withorderdict( + default_conf_usdt, trades_for_order, limit_order, fee, mocker, initial_amount, + has_rounding_fee, is_short, caplog +): + order = limit_order[enter_side(is_short)] trades_for_order[0]['amount'] = initial_amount mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! @@ -1711,22 +1830,26 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l fee_close=fee.return_value, open_order_id="123456", is_open=True, + is_short=is_short ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order_usdt) + freqtrade.update_trade_state(trade, '123456', order) assert trade.amount != amount - assert trade.amount == limit_buy_order_usdt['amount'] + assert trade.amount == order['amount'] if has_rounding_fee: assert log_has_re(r'Applying fee on amount for .*', caplog) -def test_update_trade_state_exception(mocker, default_conf_usdt, - limit_buy_order_usdt, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit_order, + caplog) -> None: + order = limit_order[enter_side(is_short)] freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) trade = MagicMock() trade.open_order_id = '123' trade.open_fee = 0.001 + trade.is_short = is_short # Test raise of OperationalException exception mocker.patch( @@ -1753,8 +1876,12 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog) -def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell_order_usdt_open, - limit_sell_order_usdt, mocker): +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_trade_state_sell( + default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker, +): + open_order = limit_order_open[exit_side(is_short)] + l_order = limit_order[exit_side(is_short)] mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1762,8 +1889,8 @@ def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) patch_exchange(mocker) - amount = limit_sell_order_usdt["amount"] freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + amount = l_order["amount"] wallet_mock.reset_mock() trade = Trade( pair='LTC/ETH', @@ -1775,12 +1902,14 @@ def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell open_date=arrow.utcnow().datetime, open_order_id="123456", is_open=True, + interest_rate=0.0005, + is_short=is_short ) - order = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'LTC/ETH', 'sell') + order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', (enter_side(is_short))) trade.orders.append(order) assert order.status == 'open' - freqtrade.update_trade_state(trade, trade.open_order_id, limit_sell_order_usdt) - assert trade.amount == limit_sell_order_usdt['amount'] + freqtrade.update_trade_state(trade, trade.open_order_id, l_order) + assert trade.amount == l_order['amount'] # Wallet needs to be updated after closing a limit-sell order to reenable buying assert wallet_mock.call_count == 1 assert not trade.is_open @@ -1788,78 +1917,96 @@ def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell assert order.status == 'closed' -def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_usdt_open, - limit_sell_order_usdt, fee, mocker) -> None: +@pytest.mark.parametrize('is_short,close_profit', [ + (False, 0.09451372), + (True, 0.08635224), +]) +def test_handle_trade( + default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short, close_profit +) -> None: + open_order = limit_order_open[exit_side(is_short)] + enter_order = limit_order[enter_side(is_short)] + exit_order = limit_order[exit_side(is_short)] patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, + 'bid': 2.19, 'ask': 2.2, - 'last': 1.9 + 'last': 2.19 }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt, - limit_sell_order_usdt_open, + enter_order, + open_order, ]), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade time.sleep(0.01) # Race condition fix - trade.update(limit_buy_order_usdt) + trade.update(enter_order) assert trade.is_open is True freqtrade.wallets.update() - patch_get_signal(freqtrade, enter_long=False, exit_long=True) + patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short) assert freqtrade.handle_trade(trade) is True - assert trade.open_order_id == limit_sell_order_usdt['id'] + assert trade.open_order_id == exit_order['id'] - # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order_usdt) + # Simulate fulfilled LIMIT order for trade + trade.update(exit_order) - assert trade.close_rate == 2.2 - assert trade.close_profit == 0.09451372 + assert trade.close_rate == 2.0 if is_short else 2.2 + assert trade.close_profit == close_profit assert trade.calc_profit() == 5.685 assert trade.close_date is not None -def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, - fee, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_handle_overlapping_signals( + default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, is_short +) -> None: + open_order = limit_order_open[exit_side(is_short)] patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + open_order, {'id': 1234553382}, ]), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_long=True, exit_long=True) + if is_short: + patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True) + else: + patch_get_signal(freqtrade, enter_long=True, exit_long=True) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() # Buy and Sell triggering, so doing nothing ... trades = Trade.query.all() + nb_trades = len(trades) assert nb_trades == 0 # Buy is triggering, so buying ... - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() trades = Trade.query.all() + for trade in trades: + trade.is_short = is_short nb_trades = len(trades) assert nb_trades == 1 assert trades[0].is_open is True @@ -1868,26 +2015,42 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or patch_get_signal(freqtrade, enter_long=False) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() + for trade in trades: + trade.is_short = is_short nb_trades = len(trades) assert nb_trades == 1 assert trades[0].is_open is True # Buy and Sell are triggering, so doing nothing ... - patch_get_signal(freqtrade, enter_long=True, exit_long=True) + if is_short: + patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True) + else: + patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() + for trade in trades: + trade.is_short = is_short nb_trades = len(trades) assert nb_trades == 1 assert trades[0].is_open is True # Sell is triggering, guess what : we are Selling! - patch_get_signal(freqtrade, enter_long=False, exit_long=True) + if is_short: + patch_get_signal(freqtrade, enter_long=False, exit_short=True) + else: + patch_get_signal(freqtrade, enter_long=False, exit_long=True) trades = Trade.query.all() + for trade in trades: + trade.is_short = is_short assert freqtrade.handle_trade(trades[0]) is True -def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, - fee, mocker, caplog) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog, + is_short) -> None: + + open_order = limit_order_open[enter_side(is_short)] + caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) @@ -1895,19 +2058,20 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_o 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + open_order, {'id': 1234553382}, ]), get_fee=fee, ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True # FIX: sniffing logs, suggest handle_trade should not execute_trade_exit @@ -1915,14 +2079,22 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_o # we might just want to check if we are in a sell condition without # executing # if ROI is reached we must sell + # TODO-lev: Change the next line for shorts + caplog.clear() patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/USDT - Required profit reached. sell_type=SellType.ROI", caplog) -def test_handle_trade_use_sell_signal(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, - limit_sell_order_usdt_open, fee, mocker, caplog) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_handle_trade_use_sell_signal( + default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog, is_short +) -> None: + + enter_open_order = limit_order_open[exit_side(is_short)] + exit_open_order = limit_order_open[enter_side(is_short)] + # use_sell_signal is True buy default caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) @@ -1930,50 +2102,61 @@ def test_handle_trade_use_sell_signal(default_conf_usdt, ticker_usdt, limit_buy_ 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, - limit_sell_order_usdt_open, + enter_open_order, + exit_open_order, ]), get_fee=fee, ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True patch_get_signal(freqtrade, enter_long=False, exit_long=False) assert not freqtrade.handle_trade(trade) - patch_get_signal(freqtrade, enter_long=False, exit_long=True) + if is_short: + patch_get_signal(freqtrade, enter_long=False, exit_short=True) + else: + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/USDT - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) -def test_close_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, - limit_buy_order_usdt_open, limit_sell_order_usdt, fee, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_close_trade( + default_conf_usdt, ticker_usdt, limit_order_open, + limit_order, fee, mocker, is_short +) -> None: + open_order = limit_order_open[exit_side(is_short)] + enter_order = limit_order[exit_side(is_short)] + exit_order = limit_order[enter_side(is_short)] patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, - create_order=MagicMock(return_value=limit_buy_order_usdt_open), + create_order=MagicMock(return_value=open_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) # Create trade and sell it freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade - trade.update(limit_buy_order_usdt) - trade.update(limit_sell_order_usdt) + trade.update(enter_order) + trade.update(exit_order) assert trade.is_open is False with pytest.raises(DependencyException, match=r'.*closed trade.*'): @@ -1993,27 +2176,33 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog): assert ftbot.strategy.analyze.call_count == 1 -def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, limit_buy_order_old, - open_trade, fee, mocker) -> None: - default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30} +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_buy_usercustom( + default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, + limit_sell_order_old, fee, mocker, is_short +) -> None: + + old_order = limit_sell_order_old if is_short else limit_buy_order_old + default_conf_usdt["unfilledtimeout"] = {"buy": 30, + "sell": 1400} if is_short else {"buy": 1400, "sell": 30} rpc_mock = patch_RPCManager(mocker) - cancel_order_mock = MagicMock(return_value=limit_buy_order_old) - cancel_buy_order = deepcopy(limit_buy_order_old) - cancel_buy_order['status'] = 'canceled' - cancel_order_wr_mock = MagicMock(return_value=cancel_buy_order) + cancel_order_mock = MagicMock(return_value=old_order) + cancel_enter_order = deepcopy(old_order) + cancel_enter_order['status'] = 'canceled' + cancel_order_wr_mock = MagicMock(return_value=cancel_enter_order) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, - fetch_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_wr_mock, cancel_order=cancel_order_mock, get_fee=fee ) freqtrade = FreqtradeBot(default_conf_usdt) - + open_trade.is_short = is_short Trade.query.session.add(open_trade) # Ensure default is to return empty (so not mocked yet) @@ -2021,24 +2210,34 @@ def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, li assert cancel_order_mock.call_count == 0 # Return false - trade remains open - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + if is_short: + freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) + else: + freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 1 - assert freqtrade.strategy.check_buy_timeout.call_count == 1 + if is_short: + assert freqtrade.strategy.check_sell_timeout.call_count == 1 + # Raise Keyerror ... (no impact on trade) + freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError) + else: + assert freqtrade.strategy.check_buy_timeout.call_count == 1 + freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError) - # Raise Keyerror ... (no impact on trade) - freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError) freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 1 - assert freqtrade.strategy.check_buy_timeout.call_count == 1 - - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) + if is_short: + assert freqtrade.strategy.check_sell_timeout.call_count == 1 + freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True) + else: + assert freqtrade.strategy.check_buy_timeout.call_count == 1 + freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) # Trade should be closed since the function returns true freqtrade.check_handle_timedout() assert cancel_order_wr_mock.call_count == 1 @@ -2046,28 +2245,39 @@ def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, li trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 0 - assert freqtrade.strategy.check_buy_timeout.call_count == 1 + if is_short: + assert freqtrade.strategy.check_sell_timeout.call_count == 1 + else: + assert freqtrade.strategy.check_buy_timeout.call_count == 1 -def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, - fee, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_buy( + default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, + limit_sell_order_old, fee, mocker, is_short +) -> None: + old_order = limit_sell_order_old if is_short else limit_buy_order_old rpc_mock = patch_RPCManager(mocker) - limit_buy_cancel = deepcopy(limit_buy_order_old) + limit_buy_cancel = deepcopy(old_order) limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, - fetch_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_mock, get_fee=fee ) freqtrade = FreqtradeBot(default_conf_usdt) + open_trade.is_short = is_short Trade.query.session.add(open_trade) - freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) + if is_short: + freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) + else: + freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) # check it does cancel buy orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 @@ -2076,25 +2286,32 @@ def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_ord nb_trades = len(trades) assert nb_trades == 0 # Custom user buy-timeout is never called - assert freqtrade.strategy.check_buy_timeout.call_count == 0 + if is_short: + assert freqtrade.strategy.check_sell_timeout.call_count == 0 + else: + assert freqtrade.strategy.check_buy_timeout.call_count == 0 -def test_check_handle_cancelled_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, - fee, mocker, caplog) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_cancelled_buy( + default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, + limit_sell_order_old, fee, mocker, caplog, is_short +) -> None: """ Handle Buy order cancelled on exchange""" + old_order = limit_sell_order_old if is_short else limit_buy_order_old rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) - limit_buy_order_old.update({"status": "canceled", 'filled': 0.0}) + old_order.update({"status": "canceled", 'filled': 0.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, - fetch_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=old_order), cancel_order=cancel_order_mock, get_fee=fee ) freqtrade = FreqtradeBot(default_conf_usdt) - + open_trade.is_short = is_short Trade.query.session.add(open_trade) # check it does cancel buy orders over the time limit @@ -2104,11 +2321,15 @@ def test_check_handle_cancelled_buy(default_conf_usdt, ticker_usdt, limit_buy_or trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 0 - assert log_has_re("Buy order cancelled on exchange for Trade.*", caplog) + assert log_has_re( + f"{'Sell' if is_short else 'Buy'} order cancelled on exchange for Trade.*", caplog) -def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt, - open_trade, fee, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_buy_exception( + default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, + is_short, fee, mocker +) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) @@ -2133,8 +2354,11 @@ def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt, assert nb_trades == 1 -def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old, - mocker, open_trade) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_sell_usercustom( + default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, + is_short, open_trade_usdt +) -> None: default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440} rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() @@ -2147,12 +2371,12 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l ) freqtrade = FreqtradeBot(default_conf_usdt) - open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime - open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime - open_trade.close_profit_abs = 0.001 - open_trade.is_open = False + open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime + open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade_usdt.close_profit_abs = 0.001 + open_trade_usdt.is_open = False - Trade.query.session.add(open_trade) + Trade.query.session.add(open_trade_usdt) # Ensure default is false freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 @@ -2162,7 +2386,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 0 - assert open_trade.is_open is False + assert open_trade_usdt.is_open is False assert freqtrade.strategy.check_sell_timeout.call_count == 1 freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError) @@ -2170,7 +2394,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 0 - assert open_trade.is_open is False + assert open_trade_usdt.is_open is False assert freqtrade.strategy.check_sell_timeout.call_count == 1 # Return True - sells! @@ -2178,12 +2402,15 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 - assert open_trade.is_open is True + assert open_trade_usdt.is_open is True assert freqtrade.strategy.check_sell_timeout.call_count == 1 -def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, - open_trade) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_sell( + default_conf_usdt, ticker_usdt, limit_sell_order_old, + mocker, is_short, open_trade_usdt +) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) @@ -2195,25 +2422,28 @@ def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_o ) freqtrade = FreqtradeBot(default_conf_usdt) - open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime - open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime - open_trade.close_profit_abs = 0.001 - open_trade.is_open = False + open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime + open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade_usdt.close_profit_abs = 0.001 + open_trade_usdt.is_open = False - Trade.query.session.add(open_trade) + Trade.query.session.add(open_trade_usdt) freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 - assert open_trade.is_open is True + assert open_trade_usdt.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 -def test_check_handle_cancelled_sell(default_conf_usdt, ticker_usdt, limit_sell_order_old, - open_trade, mocker, caplog) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_cancelled_sell( + default_conf_usdt, ticker_usdt, limit_sell_order_old, open_trade_usdt, + is_short, mocker, caplog +) -> None: """ Handle sell order cancelled on exchange""" rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() @@ -2227,22 +2457,25 @@ def test_check_handle_cancelled_sell(default_conf_usdt, ticker_usdt, limit_sell_ ) freqtrade = FreqtradeBot(default_conf_usdt) - open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime - open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime - open_trade.is_open = False + open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime + open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime + open_trade_usdt.is_open = False - Trade.query.session.add(open_trade) + Trade.query.session.add(open_trade_usdt) # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 - assert open_trade.is_open is True + assert open_trade_usdt.is_open is True assert log_has_re("Sell order cancelled on exchange for Trade.*", caplog) -def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy_order_old_partial, - open_trade, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_partial( + default_conf_usdt, ticker_usdt, limit_buy_order_old_partial, is_short, + open_trade, mocker +) -> None: rpc_mock = patch_RPCManager(mocker) limit_buy_canceled = deepcopy(limit_buy_order_old_partial) limit_buy_canceled['status'] = 'canceled' @@ -2270,9 +2503,12 @@ def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount -def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_trade, caplog, fee, - limit_buy_order_old_partial, trades_for_order, - limit_buy_order_old_partial_canceled, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_partial_fee( + default_conf_usdt, ticker_usdt, open_trade, caplog, fee, is_short, + limit_buy_order_old_partial, trades_for_order, + limit_buy_order_old_partial_canceled, mocker +) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0)) @@ -2309,9 +2545,12 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_ assert pytest.approx(trades[0].fee_open) == 0.001 -def test_check_handle_timedout_partial_except(default_conf_usdt, ticker_usdt, open_trade, caplog, - fee, limit_buy_order_old_partial, trades_for_order, - limit_buy_order_old_partial_canceled, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_partial_except( + default_conf_usdt, ticker_usdt, open_trade, caplog, fee, is_short, + limit_buy_order_old_partial, trades_for_order, + limit_buy_order_old_partial_canceled, mocker +) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) patch_exchange(mocker) @@ -2370,6 +2609,7 @@ def test_check_handle_timedout_exception(default_conf_usdt, ticker_usdt, open_tr Trade.query.session.add(open_trade_usdt) + caplog.clear() freqtrade.check_handle_timedout() assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ADA/USDT, amount=30.00000000, " r"is_short=False, leverage=1.0, " @@ -2379,10 +2619,13 @@ def test_check_handle_timedout_exception(default_conf_usdt, ticker_usdt, open_tr caplog) -def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_usdt) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, + is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - cancel_buy_order = deepcopy(limit_buy_order_usdt) + l_order = limit_order[enter_side(is_short)] + cancel_buy_order = deepcopy(limit_order[enter_side(is_short)]) cancel_buy_order['status'] = 'canceled' del cancel_buy_order['filled'] @@ -2395,36 +2638,39 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_ trade = MagicMock() trade.pair = 'LTC/USDT' trade.open_rate = 200 - limit_buy_order_usdt['filled'] = 0.0 - limit_buy_order_usdt['status'] = 'open' + trade.is_short = False + trade.enter_side = "buy" + l_order['filled'] = 0.0 + l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) + assert freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() caplog.clear() - limit_buy_order_usdt['filled'] = 0.01 - assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) + l_order['filled'] = 0.01 + assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 0 - assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog) + assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unexitable.*", caplog) caplog.clear() cancel_order_mock.reset_mock() - limit_buy_order_usdt['filled'] = 2 - assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) + l_order['filled'] = 2 + assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 # Order remained open for some reason (cancel failed) cancel_buy_order['status'] = 'open' cancel_order_mock = MagicMock(return_value=cancel_buy_order) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) - assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) + assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) -@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], - indirect=['limit_buy_order_canceled_empty']) -def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, +@ pytest.mark.parametrize("is_short", [False, True]) +@ pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], + indirect=['limit_buy_order_canceled_empty']) +def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_short, limit_buy_order_canceled_empty) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2437,22 +2683,25 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, reason = CANCEL_REASON['TIMEOUT'] trade = MagicMock() trade.pair = 'LTC/ETH' + trade.enter_side = "buy" assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert cancel_order_mock.call_count == 0 assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog) assert nofiy_mock.call_count == 1 -@pytest.mark.parametrize('cancelorder', [ +@ pytest.mark.parametrize("is_short", [False, True]) +@ pytest.mark.parametrize('cancelorder', [ {}, {'remaining': None}, 'String Return value', 123 ]) -def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_buy_order_usdt, +def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order, is_short, cancelorder) -> None: patch_RPCManager(mocker) patch_exchange(mocker) + l_order = limit_order[enter_side(is_short)] cancel_order_mock = MagicMock(return_value=cancelorder) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2464,16 +2713,18 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_buy_o trade = MagicMock() trade.pair = 'LTC/USDT' + trade.enter_side = "buy" trade.open_rate = 200 - limit_buy_order_usdt['filled'] = 0.0 - limit_buy_order_usdt['status'] = 'open' + trade.enter_side = "buy" + l_order['filled'] = 0.0 + l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) + assert freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() - limit_buy_order_usdt['filled'] = 1.0 - assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) + l_order['filled'] = 1.0 + assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 @@ -2538,8 +2789,12 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' -def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker - ) -> None: +@ pytest.mark.parametrize("is_short, open_rate, amt", [ + (False, 2.0, 30.0), + (True, 2.02, 29.70297029), +]) +def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker, + ticker_usdt_sell_down, is_short, open_rate, amt) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2550,7 +2805,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False) # Create some test data @@ -2558,25 +2813,31 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ rpc_mock.reset_mock() trade = Trade.query.first() + assert trade.is_short == is_short assert trade assert freqtrade.strategy.confirm_trade_exit.call_count == 0 # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_usdt_sell_up + fetch_ticker=ticker_usdt_sell_down if is_short else ticker_usdt_sell_up ) # Prevented sell ... - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.ROI)) + freqtrade.execute_trade_exit( + trade=trade, + limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), + sell_reason=SellCheckTuple(sell_type=SellType.ROI) + ) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 # Repatch with true freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) - - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.ROI)) + freqtrade.execute_trade_exit( + trade=trade, + limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), + sell_reason=SellCheckTuple(sell_type=SellType.ROI) + ) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2587,13 +2848,13 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'exchange': 'Binance', 'pair': 'ETH/USDT', 'gain': 'profit', - 'limit': 2.2, - 'amount': 30.0, + 'limit': 2.0 if is_short else 2.2, + 'amount': amt, 'order_type': 'limit', - 'open_rate': 2.0, - 'current_rate': 2.3, - 'profit_amount': 5.685, - 'profit_ratio': 0.09451372, + 'open_rate': open_rate, + 'current_rate': 2.01 if is_short else 2.3, + 'profit_amount': 0.29554455 if is_short else 5.685, + 'profit_ratio': 0.00493809 if is_short else 0.09451372, 'stake_currency': 'USDT', 'fiat_currency': 'USD', 'sell_reason': SellType.ROI.value, @@ -2603,8 +2864,9 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ } == last_msg +@ pytest.mark.parametrize("is_short", [False, True]) def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_down, - mocker) -> None: + ticker_usdt_sell_up, mocker, is_short) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2615,22 +2877,23 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) # Create some test data freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_usdt_sell_down + fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down ) - - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + freqtrade.execute_trade_exit( + trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down)()['bid'], + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2640,13 +2903,13 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'exchange': 'Binance', 'pair': 'ETH/USDT', 'gain': 'loss', - 'limit': 2.01, - 'amount': 30.0, + 'limit': 2.2 if is_short else 2.01, + 'amount': 29.70297029 if is_short else 30.0, 'order_type': 'limit', - 'open_rate': 2.0, - 'current_rate': 2.0, - 'profit_amount': -0.00075, - 'profit_ratio': -1.247e-05, + 'open_rate': 2.02 if is_short else 2.0, + 'current_rate': 2.2 if is_short else 2.0, + 'profit_amount': -5.65990099 if is_short else -0.00075, + 'profit_ratio': -0.0945681 if is_short else -1.247e-05, 'stake_currency': 'USDT', 'fiat_currency': 'USD', 'sell_reason': SellType.STOP_LOSS.value, @@ -2656,8 +2919,14 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd } == last_msg -def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fee, - ticker_usdt_sell_up, mocker) -> None: +@pytest.mark.parametrize( + "is_short,amount,open_rate,current_rate,limit,profit_amount,profit_ratio,profit_or_loss", [ + (False, 30, 2.0, 2.3, 2.25, 7.18125, 0.11938903, 'profit'), + (True, 29.70297029, 2.02, 2.2, 2.25, -7.14876237, -0.11944465, 'loss'), # TODO-lev + ]) +def test_execute_trade_exit_custom_exit_price( + default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, is_short, amount, open_rate, + current_rate, limit, profit_amount, profit_ratio, profit_or_loss, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2670,7 +2939,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe config['custom_price_max_distance_ratio'] = 0.1 patch_whitelist(mocker, config) freqtrade = FreqtradeBot(config) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False) # Create some test data @@ -2678,6 +2947,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe rpc_mock.reset_mock() trade = Trade.query.first() + trade.is_short = is_short assert trade assert freqtrade.strategy.confirm_trade_exit.call_count == 0 @@ -2691,7 +2961,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe # Set a custom exit price freqtrade.strategy.custom_exit_price = lambda **kwargs: 2.25 - + # TODO-lev: side="buy" freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL)) @@ -2706,14 +2976,14 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/USDT', - 'gain': 'profit', - 'limit': 2.25, - 'amount': 30.0, + 'gain': profit_or_loss, + 'limit': limit, + 'amount': amount, 'order_type': 'limit', - 'open_rate': 2.0, - 'current_rate': 2.3, - 'profit_amount': 7.18125, - 'profit_ratio': 0.11938903, + 'open_rate': open_rate, + 'current_rate': current_rate, + 'profit_amount': profit_amount, + 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', 'fiat_currency': 'USD', 'sell_reason': SellType.SELL_SIGNAL.value, @@ -2723,8 +2993,10 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe } == last_msg +@ pytest.mark.parametrize("is_short", [False, True]) def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( - default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_down, mocker) -> None: + 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( @@ -2735,27 +3007,29 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) # Create some test data freqtrade.enter_positions() trade = Trade.query.first() + assert trade.is_short == is_short assert trade # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_ticker=ticker_usdt_sell_down + 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 * 0.99 - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) + trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99 + freqtrade.execute_trade_exit( + trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down())['bid'], + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2766,13 +3040,13 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'exchange': 'Binance', 'pair': 'ETH/USDT', 'gain': 'loss', - 'limit': 1.98, - 'amount': 30.0, + 'limit': 2.02 if is_short else 1.98, + 'amount': 29.70297029 if is_short else 30.0, 'order_type': 'limit', - 'open_rate': 2.0, - 'current_rate': 2.0, - 'profit_amount': -0.8985, - 'profit_ratio': -0.01493766, + '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', 'fiat_currency': 'USD', 'sell_reason': SellType.STOP_LOSS.value, @@ -2810,14 +3084,16 @@ def test_execute_trade_exit_sloe_cancel_exception( freqtrade.config['dry_run'] = False trade.stoploss_order_id = "abcd" + # TODO-lev: side="buy" freqtrade.execute_trade_exit(trade=trade, limit=1234, sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert create_order_mock.call_count == 2 assert log_has('Could not cancel stoploss order abcd', caplog) -def test_execute_trade_exit_with_stoploss_on_exchange(default_conf_usdt, ticker_usdt, fee, - ticker_usdt_sell_up, mocker) -> None: +@ 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) @@ -2843,12 +3119,13 @@ def test_execute_trade_exit_with_stoploss_on_exchange(default_conf_usdt, ticker_ freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types['stoploss_on_exchange'] = True - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) # Create some test data freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade trades = [trade] @@ -2861,10 +3138,12 @@ def test_execute_trade_exit_with_stoploss_on_exchange(default_conf_usdt, ticker_ fetch_ticker=ticker_usdt_sell_up ) + # TODO-lev: side="buy" freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() + trade.is_short = is_short assert trade assert cancel_order.call_count == 1 assert rpc_mock.call_count == 3 @@ -2942,8 +3221,33 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL -def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, - ticker_usdt_sell_up, mocker) -> None: +@pytest.mark.parametrize( + "is_short,amount,open_rate,current_rate,limit,profit_amount,profit_ratio,profit_or_loss", [ + (False, 30, 2.0, 2.3, 2.2, 5.685, 0.09451372, 'profit'), + # TODO-lev: Should the current rate be 2.2 for shorts? + (True, 29.70297029, 2.02, 2.2, 2.3, -8.63762376, -0.1443212, 'loss'), + ]) +def test_execute_trade_exit_market_order( + default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount, open_rate, + limit, profit_amount, profit_ratio, profit_or_loss, ticker_usdt_sell_up, mocker +) -> None: + """ + amount + long: 60 / 2.0 = 30 + short: 60 / 2.02 = 29.70297029 + open_value + long: (30 * 2.0) + (30 * 2.0 * 0.0025) = 60.15 + short: (29.702970297029704 * 2.02) - (29.702970297029704 * 2.02 * 0.0025) = 59.85 + close_value + long: (30 * 2.2) - (30 * 2.2 * 0.0025) = 65.835 + short: (29.702970297029704 * 2.3) + (29.702970297029704 * 2.3 * 0.0025) = 68.48762376237624 + profit + long: 65.835 - 60.15 = 5.684999999999995 + short: 59.85 - 68.48762376237624 = -8.637623762376244 + profit_ratio + long: (65.835/60.15) - 1 = 0.0945137157107232 + short: 1 - (68.48762376237624/59.85) = -0.1443211990371971 + """ rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2954,12 +3258,13 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, ) patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) # Create some test data freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade # Increase the price and sell it @@ -2969,11 +3274,15 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, ) freqtrade.config['order_types']['sell'] = 'market' - freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_type=SellType.ROI)) + # TODO-lev: side="buy" + freqtrade.execute_trade_exit( + trade=trade, + limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], + sell_reason=SellCheckTuple(sell_type=SellType.ROI) + ) assert not trade.is_open - assert trade.close_profit == 0.09451372 + assert trade.close_profit == profit_ratio assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2982,14 +3291,14 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/USDT', - 'gain': 'profit', - 'limit': 2.2, - 'amount': 30.0, + 'gain': profit_or_loss, + 'limit': limit, + 'amount': round(amount, 9), 'order_type': 'market', - 'open_rate': 2.0, - 'current_rate': 2.3, - 'profit_amount': 5.685, - 'profit_ratio': 0.09451372, + 'open_rate': open_rate, + 'current_rate': current_rate, + 'profit_amount': profit_amount, + 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', 'fiat_currency': 'USD', 'sell_reason': SellType.ROI.value, @@ -3000,7 +3309,8 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, } == last_msg -def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_usdt, fee, +@ pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_usdt, fee, is_short, ticker_usdt_sell_up, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') @@ -3013,12 +3323,13 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u InsufficientFundsError(), ]), ) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) # Create some test data freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade # Increase the price and sell it @@ -3028,24 +3339,29 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u ) sell_reason = SellCheckTuple(sell_type=SellType.ROI) + # TODO-lev: side="buy" assert not freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], sell_reason=sell_reason) assert mock_insuf.call_count == 1 -@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [ +@ pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type,is_short', [ # Enable profit - (True, 1.9, 2.2, False, True, SellType.SELL_SIGNAL.value), - # Disable profit - (False, 2.9, 3.2, True, False, SellType.SELL_SIGNAL.value), - # Enable loss - # * Shouldn't this be SellType.STOP_LOSS.value - (True, 0.19, 0.22, False, False, None), + (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, False), + (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, True), + # # Disable profit + (False, 3.19, 3.2, True, False, SellType.SELL_SIGNAL.value, False), + (False, 3.19, 3.2, True, False, SellType.SELL_SIGNAL.value, True), + # # Enable loss + # # * Shouldn't this be SellType.STOP_LOSS.value + (True, 0.21, 0.22, False, False, None, False), + (True, 2.41, 2.42, False, False, None, True), # Disable loss - (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value), + (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value, False), + (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value, True), ]) def test_sell_profit_only( - default_conf_usdt, limit_buy_order_usdt, limit_buy_order_usdt_open, + default_conf_usdt, limit_order, limit_order_open, is_short, fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3057,7 +3373,7 @@ def test_sell_profit_only( 'last': bid }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + limit_order_open[enter_side(is_short)], {'id': 1234553382}, ]), get_fee=fee, @@ -3068,7 +3384,7 @@ def test_sell_profit_only( 'sell_profit_offset': 0.1, }) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) if sell_type == SellType.SELL_SIGNAL.value: freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) else: @@ -3077,9 +3393,10 @@ def test_sell_profit_only( freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + trade.is_short = is_short + trade.update(limit_order[enter_side(is_short)]) freqtrade.wallets.update() - patch_get_signal(freqtrade, enter_long=False, exit_long=True) + patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short) assert freqtrade.handle_trade(trade) is handle_first if handle_second: @@ -3087,7 +3404,7 @@ def test_sell_profit_only( assert freqtrade.handle_trade(trade) is True -def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_buy_order_usdt_open, +def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_open, fee, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3099,7 +3416,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_ 'last': 0.00002172 }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + limit_order_open['buy'], {'id': 1234553382}, ]), get_fee=fee, @@ -3113,7 +3430,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_ trade = Trade.query.first() amnt = trade.amount - trade.update(limit_buy_order_usdt) + trade.update(limit_order['buy']) patch_get_signal(freqtrade, enter_long=False, exit_long=True) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) @@ -3122,7 +3439,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_ assert trade.amount != amnt -@pytest.mark.parametrize('amount_wallet,has_err', [ +@ pytest.mark.parametrize('amount_wallet,has_err', [ (95.29, False), (91.29, True) ]) @@ -3159,8 +3476,9 @@ def test__safe_exit_amount(default_conf_usdt, fee, caplog, mocker, amount_wallet assert wallet_update.call_count == 1 -def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_down, mocker, - caplog) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, + ticker_usdt_sell_down, mocker, caplog, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3169,12 +3487,13 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_down get_fee=fee, ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) # Create some test data freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade # Decrease the price and sell it @@ -3183,6 +3502,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_down fetch_ticker=ticker_usdt_sell_down ) + # TODO-lev: side="buy" freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'], sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade.close(ticker_usdt_sell_down()['bid']) @@ -3195,8 +3515,9 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_down assert log_has_re(f"Pair {trade.pair} is still locked.*", caplog) -def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt, - limit_buy_order_usdt_open, fee, mocker) -> None: +@ pytest.mark.parametrize("is_short", [False, True]) +def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_open, is_short, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3207,7 +3528,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt, 'last': 2.19 }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + limit_order_open[enter_side(is_short)], {'id': 1234553382}, ]), get_fee=fee, @@ -3215,25 +3536,37 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt, default_conf_usdt['ignore_roi_if_buy_signal'] = True freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + trade.is_short = is_short + trade.update(limit_order[enter_side(is_short)]) freqtrade.wallets.update() - patch_get_signal(freqtrade, enter_long=True, exit_long=True) + if is_short: + patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True) + else: + patch_get_signal(freqtrade, enter_long=True, exit_long=True) + assert freqtrade.handle_trade(trade) is False # Test if buy-signal is absent (should sell due to roi = true) - patch_get_signal(freqtrade, enter_long=False, exit_long=True) + if is_short: + patch_get_signal(freqtrade, enter_long=False, exit_short=True) + else: + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.ROI.value -def test_trailing_stop_loss(default_conf_usdt, limit_buy_order_usdt_open, - fee, caplog, mocker) -> None: +@ pytest.mark.parametrize("is_short,val1,val2", [ + (False, 1.5, 1.1), + (True, 0.5, 0.9) +]) +def test_trailing_stop_loss(default_conf_usdt, limit_order_open, + is_short, val1, val2, fee, caplog, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3244,7 +3577,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_buy_order_usdt_open, 'last': 2.0 }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + limit_order_open[enter_side(is_short)], {'id': 1234553382}, ]), get_fee=fee, @@ -3252,19 +3585,20 @@ def test_trailing_stop_loss(default_conf_usdt, limit_buy_order_usdt_open, default_conf_usdt['trailing_stop'] = True patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() trade = Trade.query.first() + assert trade.is_short == is_short assert freqtrade.handle_trade(trade) is False - # Raise ticker_usdt above buy price + # Raise praise into profits mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': 2.0 * 1.5, - 'ask': 2.0 * 1.5, - 'last': 2.0 * 1.5 + 'bid': 2.0 * val1, + 'ask': 2.0 * val1, + 'last': 2.0 * val1 })) # Stoploss should be adjusted @@ -3273,40 +3607,46 @@ def test_trailing_stop_loss(default_conf_usdt, limit_buy_order_usdt_open, # Price fell mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': 2.0 * 1.1, - 'ask': 2.0 * 1.1, - 'last': 2.0 * 1.1 + 'bid': 2.0 * val2, + 'ask': 2.0 * val2, + 'last': 2.0 * val2 })) caplog.set_level(logging.DEBUG) # Sell as trailing-stop is reached assert freqtrade.handle_trade(trade) is True - assert log_has("ETH/USDT - HIT STOP: current price at 2.200000, stoploss is 2.700000, " - "initial stoploss was at 1.800000, trade opened at 2.000000", caplog) + stop_multi = 1.1 if is_short else 0.9 + assert log_has(f"ETH/USDT - HIT STOP: current price at {(2.0 * val2):6f}, " + f"stoploss is {(2.0 * val1 * stop_multi):6f}, " + f"initial stoploss was at {(2.0 * stop_multi):6f}, trade opened at 2.000000", + caplog) assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -@pytest.mark.parametrize('offset,trail_if_reached,second_sl', [ - (0, False, 2.0394), - (0.011, False, 2.0394), - (0.055, True, 1.8), +@ pytest.mark.parametrize('offset,trail_if_reached,second_sl,is_short', [ + (0, False, 2.0394, False), + (0.011, False, 2.0394, False), + (0.055, True, 1.8, False), + (0, False, 2.1614, True), + (0.011, False, 2.1614, True), + (0.055, True, 2.42, True), ]) def test_trailing_stop_loss_positive( - default_conf_usdt, limit_buy_order_usdt, limit_buy_order_usdt_open, - offset, fee, caplog, mocker, trail_if_reached, second_sl + default_conf_usdt, limit_order, limit_order_open, + offset, fee, caplog, mocker, trail_if_reached, second_sl, is_short ) -> None: - buy_price = limit_buy_order_usdt['price'] + enter_price = limit_order[enter_side(is_short)]['price'] patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price - 0.01, - 'ask': buy_price - 0.01, - 'last': buy_price - 0.01 + 'bid': enter_price - (-0.01 if is_short else 0.01), + 'ask': enter_price - (-0.01 if is_short else 0.01), + 'last': enter_price - (-0.01 if is_short else 0.01), }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + limit_order_open[enter_side(is_short)], {'id': 1234553382}, ]), get_fee=fee, @@ -3319,12 +3659,13 @@ def test_trailing_stop_loss_positive( patch_whitelist(mocker, default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + trade.is_short = is_short + trade.update(limit_order[enter_side(is_short)]) caplog.set_level(logging.DEBUG) # stop-loss not reached assert freqtrade.handle_trade(trade) is False @@ -3333,34 +3674,36 @@ def test_trailing_stop_loss_positive( mocker.patch( 'freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': buy_price + 0.06, - 'ask': buy_price + 0.06, - 'last': buy_price + 0.06 + 'bid': enter_price + (-0.06 if is_short else 0.06), + 'ask': enter_price + (-0.06 if is_short else 0.06), + 'last': enter_price + (-0.06 if is_short else 0.06), }) ) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - caplog_text = f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 0.0249%" + caplog_text = (f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: " + f"{'0.0249' if not is_short else '0.0224'}%") if trail_if_reached: assert not log_has(caplog_text, caplog) assert not log_has("ETH/USDT - Adjusting stoploss...", caplog) else: assert log_has(caplog_text, caplog) assert log_has("ETH/USDT - Adjusting stoploss...", caplog) - assert trade.stop_loss == second_sl + assert pytest.approx(trade.stop_loss) == second_sl caplog.clear() mocker.patch( 'freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': buy_price + 0.125, - 'ask': buy_price + 0.125, - 'last': buy_price + 0.125, + 'bid': enter_price + (-0.135 if is_short else 0.125), + 'ask': enter_price + (-0.135 if is_short else 0.125), + 'last': enter_price + (-0.135 if is_short else 0.125), }) ) assert freqtrade.handle_trade(trade) is False assert log_has( - f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 0.0572%", + f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: " + f"{'0.0572' if not is_short else '0.0567'}%", caplog ) assert log_has("ETH/USDT - Adjusting stoploss...", caplog) @@ -3368,22 +3711,25 @@ def test_trailing_stop_loss_positive( mocker.patch( 'freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ - 'bid': buy_price + 0.02, - 'ask': buy_price + 0.02, - 'last': buy_price + 0.02 + 'bid': enter_price + (-0.02 if is_short else 0.02), + 'ask': enter_price + (-0.02 if is_short else 0.02), + 'last': enter_price + (-0.02 if is_short else 0.02), }) ) # Lower price again (but still positive) assert freqtrade.handle_trade(trade) is True assert log_has( - f"ETH/USDT - HIT STOP: current price at {buy_price + 0.02:.6f}, " + f"ETH/USDT - HIT STOP: current price at {enter_price + (-0.02 if is_short else 0.02):.6f}, " f"stoploss is {trade.stop_loss:.6f}, " - f"initial stoploss was at 1.800000, trade opened at 2.000000", caplog) + f"initial stoploss was at {'2.42' if is_short else '1.80'}0000, " + f"trade opened at {2.2 if is_short else 2.0}00000", + caplog) assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt, - limit_buy_order_usdt_open, fee, mocker) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_open, + is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3394,7 +3740,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd 'last': 2.0 }), create_order=MagicMock(side_effect=[ - limit_buy_order_usdt_open, + limit_order_open[enter_side(is_short)], {'id': 1234553382}, {'id': 1234553383} ]), @@ -3405,19 +3751,21 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd 'ignore_roi_if_buy_signal': False } freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + trade.is_short = is_short + trade.update(limit_order[enter_side(is_short)]) # Sell due to min_roi_reached - patch_get_signal(freqtrade, enter_long=True, exit_long=True) + patch_get_signal(freqtrade, enter_long=not is_short, exit_long=not is_short, + enter_short=is_short, exit_short=is_short) assert freqtrade.handle_trade(trade) is True # Test if buy-signal is absent - patch_get_signal(freqtrade, enter_long=False, exit_long=True) + patch_get_signal(freqtrade, enter_long=False, exit_long=not is_short, exit_short=is_short) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.ROI.value @@ -3497,7 +3845,7 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock ) -@pytest.mark.parametrize( +@ pytest.mark.parametrize( 'fee_par,fee_reduction_amount,use_ticker_usdt_rate,expected_log', [ # basic, amount does not change ({'cost': 0.008, 'currency': 'ETH'}, 0, False, None), @@ -3550,7 +3898,7 @@ def test_get_real_amount( assert log_has(expected_log, caplog) -@pytest.mark.parametrize( +@ pytest.mark.parametrize( 'fee_cost, fee_currency, fee_reduction_amount, expected_fee, expected_log_amount', [ # basic, amount is reduced by fee (None, None, 0.001, 0.001, 7.992), @@ -3681,7 +4029,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord ) -def test_get_real_amount_open_trade(default_conf_usdt, fee, mocker): +def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker): amount = 12345 trade = Trade( pair='LTC/ETH', @@ -3702,7 +4050,7 @@ def test_get_real_amount_open_trade(default_conf_usdt, fee, mocker): assert freqtrade.get_real_amount(trade, order) == amount -@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [ +@ pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [ (8.0, 0.0, 10, 8), (8.0, 0.0, 0, 8), (8.0, 0.1, 0, 7.9), @@ -3731,13 +4079,17 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker, assert walletmock.call_count == 1 -@pytest.mark.parametrize("delta, is_high_delta", [ +@ pytest.mark.parametrize("delta, is_high_delta", [ (0.1, False), (100, True), ]) +@ pytest.mark.parametrize('is_short, open_rate', [ + (False, 2.0), + (True, 2.02), +]) def test_order_book_depth_of_market( - default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, limit_buy_order_usdt, - fee, mocker, order_book_l2, delta, is_high_delta + default_conf_usdt, ticker_usdt, limit_order, limit_order_open, + fee, mocker, order_book_l2, delta, is_high_delta, is_short, open_rate ): default_conf_usdt['bid_strategy']['check_depth_of_market']['enabled'] = True default_conf_usdt['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = delta @@ -3747,20 +4099,21 @@ def test_order_book_depth_of_market( mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, - create_order=MagicMock(return_value=limit_buy_order_usdt_open), + create_order=MagicMock(return_value=limit_order_open[enter_side(is_short)]), get_fee=fee, ) # Save state of current whitelist whitelist = deepcopy(default_conf_usdt['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() trade = Trade.query.first() if is_high_delta: assert trade is None else: + trade.is_short = is_short assert trade is not None assert trade.stake_amount == 60.0 assert trade.is_open @@ -3770,13 +4123,13 @@ def test_order_book_depth_of_market( assert len(Trade.query.all()) == 1 # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order_usdt) + trade.update(limit_order_open[enter_side(is_short)]) - assert trade.open_rate == 2.0 + assert trade.open_rate == open_rate # TODO-lev: double check assert whitelist == default_conf_usdt['exchange']['pair_whitelist'] -@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ +@ pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ (False, 0.045, 0.046, 2, None), (True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]}) ]) @@ -3809,7 +4162,7 @@ def test_order_book_bid_strategy1(mocker, default_conf_usdt, order_book_l2, exce assert ticker_usdt_mock.call_count == 0 -def test_check_depth_of_market_buy(default_conf_usdt, mocker, order_book_l2) -> None: +def test_check_depth_of_market(default_conf_usdt, mocker, order_book_l2) -> None: """ test check depth of market """ @@ -3826,11 +4179,12 @@ def test_check_depth_of_market_buy(default_conf_usdt, mocker, order_book_l2) -> freqtrade = FreqtradeBot(default_conf_usdt) conf = default_conf_usdt['bid_strategy']['check_depth_of_market'] - assert freqtrade._check_depth_of_market_buy('ETH/USDT', conf) is False + assert freqtrade._check_depth_of_market('ETH/BTC', conf, side=SignalDirection.LONG) is False +@ pytest.mark.parametrize('is_short', [False, True]) def test_order_book_ask_strategy( - default_conf_usdt, limit_buy_order_usdt_open, limit_buy_order_usdt, fee, + default_conf_usdt, limit_buy_order_usdt_open, limit_buy_order_usdt, fee, is_short, limit_sell_order_usdt_open, mocker, order_book_l2, caplog) -> None: """ test order book ask strategy @@ -3868,7 +4222,10 @@ def test_order_book_ask_strategy( freqtrade.wallets.update() assert trade.is_open is True - patch_get_signal(freqtrade, enter_long=False, exit_long=True) + if is_short: + patch_get_signal(freqtrade, enter_long=False, exit_short=True) + else: + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0] @@ -3906,7 +4263,7 @@ def test_startup_trade_reinit(default_conf_usdt, edge_conf, mocker): assert reinit_mock.call_count == 0 -@pytest.mark.usefixtures("init_persistence") +@ pytest.mark.usefixtures("init_persistence") def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_buy_order_usdt_open, caplog): default_conf_usdt['dry_run'] = True @@ -3939,38 +4296,46 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_ caplog) -@pytest.mark.usefixtures("init_persistence") -def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_buy_order_usdt, - limit_sell_order_usdt): +@ pytest.mark.usefixtures("init_persistence") +@ pytest.mark.parametrize("is_short,buy_calls,sell_calls", [ + (False, 1, 2), + (True, 2, 1), +]) +def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, limit_order_open, + is_short, buy_calls, sell_calls): default_conf_usdt['cancel_open_orders_on_exit'] = True - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - side_effect=[ - ExchangeError(), - limit_sell_order_usdt, - limit_buy_order_usdt, - limit_sell_order_usdt - ]) + mocker.patch( + 'freqtrade.exchange.Exchange.fetch_order', + side_effect=[ + ExchangeError(), + limit_order[exit_side(is_short)], + limit_order_open[enter_side(is_short)], + limit_order_open[exit_side(is_short)], + ] + ) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit') freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.query.all() assert len(trades) == MOCK_TRADE_COUNT freqtrade.cancel_all_open_orders() - assert buy_mock.call_count == 1 - assert sell_mock.call_count == 2 + assert buy_mock.call_count == buy_calls + assert sell_mock.call_count == sell_calls -@pytest.mark.usefixtures("init_persistence") -def test_check_for_open_trades(mocker, default_conf_usdt, fee): +@ pytest.mark.usefixtures("init_persistence") +@ pytest.mark.parametrize("is_short", [False, True]) +def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade.check_for_open_trades() assert freqtrade.rpc.send_msg.call_count == 0 - create_mock_trades(fee) + create_mock_trades(fee, is_short) trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True freqtrade.check_for_open_trades() @@ -3978,10 +4343,11 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee): assert 'Handle these trades manually' in freqtrade.rpc.send_msg.call_args[0][0]['status'] -@pytest.mark.usefixtures("init_persistence") -def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog): +@ pytest.mark.parametrize("is_short", [False, True]) +@ pytest.mark.usefixtures("init_persistence") +def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) freqtrade.startup_update_open_orders() assert not log_has_re(r"Error updating Order .*", caplog) @@ -3994,7 +4360,7 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog): caplog.clear() assert len(Order.get_open_orders()) == 3 - matching_buy_order = mock_order_4() + matching_buy_order = mock_order_4(is_short=is_short) matching_buy_order.update({ 'status': 'closed', }) @@ -4004,8 +4370,9 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog): assert len(Order.get_open_orders()) == 2 -@pytest.mark.usefixtures("init_persistence") -def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, fee): +@ pytest.mark.usefixtures("init_persistence") +@ pytest.mark.parametrize("is_short", [False, True]) +def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, fee, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) def patch_with_fee(order): @@ -4015,19 +4382,20 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, f mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=[ - patch_with_fee(mock_order_2_sell()), - patch_with_fee(mock_order_3_sell()), - patch_with_fee(mock_order_1()), - patch_with_fee(mock_order_2()), - patch_with_fee(mock_order_3()), - patch_with_fee(mock_order_4()), + patch_with_fee(mock_order_2_sell(is_short=is_short)), + patch_with_fee(mock_order_3_sell(is_short=is_short)), + patch_with_fee(mock_order_1(is_short=is_short)), + patch_with_fee(mock_order_2(is_short=is_short)), + patch_with_fee(mock_order_3(is_short=is_short)), + patch_with_fee(mock_order_4(is_short=is_short)), ] ) - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.get_trades().all() assert len(trades) == MOCK_TRADE_COUNT for trade in trades: + trade.is_short = is_short assert trade.fee_open_cost is None assert trade.fee_open_currency is None assert trade.fee_close_cost is None @@ -4054,7 +4422,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, f for trade in trades: if trade.is_open: # Exclude Trade 4 - as the order is still open. - if trade.select_order('buy', False): + if trade.select_order(enter_side(is_short), False): assert trade.fee_open_cost is not None assert trade.fee_open_currency is not None else: @@ -4066,20 +4434,29 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf_usdt, f assert trade.fee_close_currency is not None -@pytest.mark.usefixtures("init_persistence") -def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog): +@ pytest.mark.usefixtures("init_persistence") +@ pytest.mark.parametrize("is_short", [False, True]) +def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.get_trades().all() freqtrade.reupdate_enter_order_fees(trades[0]) - assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert log_has_re( + f"Trying to reupdate {enter_side(is_short)} " + r"fees for .*", + caplog + ) assert mock_uts.call_count == 1 assert mock_uts.call_args_list[0][0][0] == trades[0] - assert mock_uts.call_args_list[0][0][1] == mock_order_1()['id'] - assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + assert mock_uts.call_args_list[0][0][1] == mock_order_1(is_short=is_short)['id'] + assert log_has_re( + f"Updating {enter_side(is_short)}-fee on trade " + r".* for order .*\.", + caplog + ) mock_uts.reset_mock() caplog.clear() @@ -4094,21 +4471,24 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog): amount=30, open_rate=2.0, exchange='binance', + is_short=is_short ) Trade.query.session.add(trade) freqtrade.reupdate_enter_order_fees(trade) - assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert log_has_re(f"Trying to reupdate {enter_side(is_short)} fees for " + r".*", caplog) assert mock_uts.call_count == 0 - assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + assert not log_has_re(f"Updating {enter_side(is_short)}-fee on trade " + r".* for order .*\.", caplog) -@pytest.mark.usefixtures("init_persistence") +@ pytest.mark.usefixtures("init_persistence") def test_handle_insufficient_funds(mocker, default_conf_usdt, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees') - create_mock_trades(fee) + create_mock_trades(fee, is_short=False) trades = Trade.get_trades().all() # Trade 0 has only a open buy order, no closed order @@ -4141,8 +4521,9 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee): assert mock_bof.call_count == 1 -@pytest.mark.usefixtures("init_persistence") -def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): +@ pytest.mark.usefixtures("init_persistence") +@ pytest.mark.parametrize("is_short", [False, True]) +def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog, is_short): caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') @@ -4153,8 +4534,9 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): def reset_open_orders(trade): trade.open_order_id = None trade.stoploss_order_id = None + trade.is_short = is_short - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.get_trades().all() caplog.clear() @@ -4166,7 +4548,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_1() + order = mock_order_1(is_short=is_short) assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 @@ -4184,7 +4566,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_4() + order = mock_order_4(is_short=is_short) assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 @@ -4202,7 +4584,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_5_stoploss() + order = mock_order_5_stoploss(is_short=is_short) assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 @@ -4221,7 +4603,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_6_sell() + order = mock_order_6_sell(is_short=is_short) assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 @@ -4234,7 +4616,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): # Test error case mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=ExchangeError()) - order = mock_order_5_stoploss() + order = mock_order_5_stoploss(is_short=is_short) freqtrade.refind_lost_order(trades[4]) assert log_has(f"Error updating {order['id']}.", caplog) @@ -4280,20 +4662,25 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: assert valid_price_at_min_alwd < proposed_price +def test_leverage_prep(): + # TODO-lev + return + + @pytest.mark.parametrize('trading_mode,calls,t1,t2', [ - (TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - (TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - (TradingMode.FUTURES, 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"), - (TradingMode.FUTURES, 32, "2021-09-01 00:00:00", "2021-09-01 08:00:01"), - (TradingMode.FUTURES, 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"), - (TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"), + ('spot', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ('margin', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ('futures', 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"), + ('futures', 32, "2021-09-01 00:00:00", "2021-09-01 08:00:01"), + ('futures', 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"), + ('futures', 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"), + ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), + ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"), + ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"), + ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"), + ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"), + ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"), + ('futures', 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"), ]) def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine, t1, t2): diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 7128fcd89..6f9bd6555 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1514,11 +1514,12 @@ def test_adjust_min_max_rates(fee): @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize('use_db', [True, False]) -def test_get_open(fee, use_db): +@pytest.mark.parametrize('is_short', [True, False]) +def test_get_open(fee, is_short, use_db): Trade.use_db = use_db Trade.reset_trades() - create_mock_trades(fee, use_db) + create_mock_trades(fee, is_short, use_db) assert len(Trade.get_open_trades()) == 4 Trade.use_db = True @@ -1874,14 +1875,15 @@ def test_fee_updated(fee): @pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize('is_short', [True, False]) @pytest.mark.parametrize('use_db', [True, False]) -def test_total_open_trades_stakes(fee, use_db): +def test_total_open_trades_stakes(fee, is_short, use_db): Trade.use_db = use_db Trade.reset_trades() res = Trade.total_open_trades_stakes() assert res == 0 - create_mock_trades(fee, use_db) + create_mock_trades(fee, is_short, use_db) res = Trade.total_open_trades_stakes() assert res == 0.004 @@ -1889,6 +1891,7 @@ def test_total_open_trades_stakes(fee, use_db): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) @pytest.mark.parametrize('use_db', [True, False]) def test_get_total_closed_profit(fee, use_db): @@ -1896,7 +1899,7 @@ def test_get_total_closed_profit(fee, use_db): Trade.reset_trades() res = Trade.get_total_closed_profit() assert res == 0 - create_mock_trades(fee, use_db) + create_mock_trades(fee, False, use_db) res = Trade.get_total_closed_profit() assert res == 0.000739127 @@ -1904,11 +1907,12 @@ def test_get_total_closed_profit(fee, use_db): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) @pytest.mark.parametrize('use_db', [True, False]) def test_get_trades_proxy(fee, use_db): Trade.use_db = use_db Trade.reset_trades() - create_mock_trades(fee, use_db) + create_mock_trades(fee, False, use_db) trades = Trade.get_trades_proxy() assert len(trades) == 6 @@ -1937,9 +1941,10 @@ def test_get_trades_backtest(): @pytest.mark.usefixtures("init_persistence") +# @pytest.mark.parametrize('is_short', [True, False]) def test_get_overall_performance(fee): - create_mock_trades(fee) + create_mock_trades(fee, False) res = Trade.get_overall_performance() assert len(res) == 2 @@ -1949,12 +1954,13 @@ def test_get_overall_performance(fee): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_get_best_pair(fee): res = Trade.get_best_pair() assert res is None - create_mock_trades(fee) + create_mock_trades(fee, False) res = Trade.get_best_pair() assert len(res) == 2 assert res[0] == 'XRP/BTC' @@ -2036,8 +2042,9 @@ def test_update_order_from_ccxt(caplog): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_select_order(fee): - create_mock_trades(fee) + create_mock_trades(fee, False) trades = Trade.get_trades().all()