diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 9aee56ef4..9f4ef8277 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -24,7 +24,7 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and * Fetch open trades from persistence. * Calculate current list of tradable pairs. -* Download ohlcv data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs) +* Download OHLCV data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs) This step is only executed once per Candle to avoid unnecessary network traffic. * Call `bot_loop_start()` strategy callback. * Analyze strategy per pair. diff --git a/docs/exchanges.md b/docs/exchanges.md index e539aaf34..38183b0bc 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -210,6 +210,9 @@ OKX requires a passphrase for each api key, you will therefore need to add this ## Gate.io +!!! Tip "Stoploss on Exchange" + Gate.io supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.. + Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. diff --git a/docs/stoploss.md b/docs/stoploss.md index 631357e52..2d95813ac 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -24,7 +24,7 @@ These modes can be configured with these values: ``` !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now. Do not set too low/tight stoploss value if using stop loss on exchange! If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. diff --git a/docs/utils.md b/docs/utils.md index c6e795e60..a28a0f456 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -517,20 +517,25 @@ Requires a configuration with specified `pairlists` attribute. Can be used to generate static pairlists to be used during backtesting / hyperopt. ``` -usage: freqtrade test-pairlist [-h] [-c PATH] +usage: freqtrade test-pairlist [-h] [-v] [-c PATH] [--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] - [-1] [--print-json] + [-1] [--print-json] [--exchange EXCHANGE] optional arguments: -h, --help show this help message and exit + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. --quote QUOTE_CURRENCY [QUOTE_CURRENCY ...] Specify quote currency(-ies). Space-separated list. -1, --one-column Print output in one column. --print-json Print list of pairs or market symbols in JSON format. + --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no + config is provided. + ``` ### Examples diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 474759d95..7d4624bd1 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -52,7 +52,7 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one "trading_mode"] ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column", - "list_pairs_print_json"] + "list_pairs_print_json", "exchange"] ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 952ce6d04..772cc4293 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -226,9 +226,11 @@ class Edge: """ final = [] for pair, info in self._cached_pairs.items(): - if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ - info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \ - pair in pairs: + if ( + info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) + and info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) + and pair in pairs + ): final.append(pair) if self._final_pairs != final: @@ -253,8 +255,8 @@ class Edge: """ final = [] for pair, info in self._cached_pairs.items(): - if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ - info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): + if (info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and + info.winrate > float(self.edge_config.get('minimum_winrate', 0.60))): final.append({ 'Pair': pair, 'Winrate': info.winrate, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9e6a19de9..656557e78 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1124,11 +1124,11 @@ class Exchange: raise OperationalException(e) from e @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) - def fetch_order(self, order_id: str, pair: str) -> Dict: + def fetch_order(self, order_id: str, pair: str, params={}) -> Dict: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) try: - order = self._api.fetch_order(order_id, pair) + order = self._api.fetch_order(order_id, pair, params=params) self._log_exchange_response('fetch_order', order) order = self._order_contracts_to_amount(order) return order @@ -1172,7 +1172,7 @@ class Exchange: and order.get('filled') == 0.0) @retrier - def cancel_order(self, order_id: str, pair: str) -> Dict: + def cancel_order(self, order_id: str, pair: str, params={}) -> Dict: if self._config['dry_run']: try: order = self.fetch_dry_run_order(order_id) @@ -1183,7 +1183,7 @@ class Exchange: return {} try: - order = self._api.cancel_order(order_id, pair) + order = self._api.cancel_order(order_id, pair, params=params) self._log_exchange_response('cancel_order', order) order = self._order_contracts_to_amount(order) return order diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 8a55b93c4..50ff0c872 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -23,10 +23,10 @@ class Gateio(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, "ohlcv_volume_currency": "quote", + "stoploss_order_types": {"limit": "limit"}, + "stoploss_on_exchange": True, } - _headers = {'X-Gate-Channel-Id': 'freqtrade'} - _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, MarginMode.CROSS), @@ -41,3 +41,25 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') + + def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict: + return self.fetch_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) + + def cancel_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict: + return self.cancel_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) + + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return ((side == "sell" and stop_loss > float(order['stopPrice'])) or + (side == "buy" and stop_loss < float(order['stopPrice']))) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 634de99f0..6c5d098dc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -359,77 +359,118 @@ class Backtesting: """ # Special handling if high or low hit STOP_LOSS or ROI if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - if trade.stop_loss > sell_row[HIGH_IDX]: - # our stoploss was already higher than candle high, - # possibly due to a cancelled trade exit. - # sell at open price. - return sell_row[OPEN_IDX] - - # Special case: trailing triggers within same candle as trade opened. Assume most - # pessimistic price movement, which is moving just enough to arm stoploss and - # immediately going down to stop price. - if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0: - if ( - not self.strategy.use_custom_stoploss and self.strategy.trailing_stop - and self.strategy.trailing_only_offset_is_reached - and self.strategy.trailing_stop_positive_offset is not None - and self.strategy.trailing_stop_positive - ): - # Worst case: price reaches stop_positive_offset and dives down. - stop_rate = (sell_row[OPEN_IDX] * - (1 + abs(self.strategy.trailing_stop_positive_offset) - - abs(self.strategy.trailing_stop_positive))) - else: - # Worst case: price ticks tiny bit above open and dives down. - stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct)) - assert stop_rate < sell_row[HIGH_IDX] - # Limit lower-end to candle low to avoid sells below the low. - # This still remains "worst case" - but "worst realistic case". - return max(sell_row[LOW_IDX], stop_rate) - - # Set close_rate to stoploss - return trade.stop_loss + return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur) elif sell.sell_type == (SellType.ROI): - roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) - if roi is not None and roi_entry is not None: - if roi == -1 and roi_entry % self.timeframe_min == 0: - # When forceselling with ROI=-1, the roi time will always be equal to trade_dur. - # If that entry is a multiple of the timeframe (so on candle open) - # - we'll use open instead of close - return sell_row[OPEN_IDX] + return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur) + else: + return sell_row[OPEN_IDX] - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - close_rate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) - - if (trade_dur > 0 and trade_dur == roi_entry - and roi_entry % self.timeframe_min == 0 - and sell_row[OPEN_IDX] > close_rate): - # new ROI entry came into effect. - # use Open rate if open_rate > calculated sell rate - return sell_row[OPEN_IDX] - - if ( - trade_dur == 0 - # Red candle (for longs), TODO: green candle (for shorts) - and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle - and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate - and close_rate > sell_row[CLOSE_IDX] - ): - # ROI on opening candles with custom pricing can only - # trigger if the entry was at Open or lower. - # details: https: // github.com/freqtrade/freqtrade/issues/6261 - # If open_rate is < open, only allow sells below the close on red candles. - raise ValueError("Opening candle ROI on red candles.") - # Use the maximum between close_rate and low as we - # cannot sell outside of a candle. - # Applies when a new ROI setting comes in place and the whole candle is above that. - return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) - - else: - # This should not be reached... + def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, + trade_dur: int) -> float: + # our stoploss was already lower than candle high, + # possibly due to a cancelled trade exit. + # sell at open price. + is_short = trade.is_short or False + leverage = trade.leverage or 1.0 + side_1 = -1 if is_short else 1 + if is_short: + if trade.stop_loss < sell_row[LOW_IDX]: return sell_row[OPEN_IDX] else: + if trade.stop_loss > sell_row[HIGH_IDX]: + return sell_row[OPEN_IDX] + + # Special case: trailing triggers within same candle as trade opened. Assume most + # pessimistic price movement, which is moving just enough to arm stoploss and + # immediately going down to stop price. + if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0: + if ( + not self.strategy.use_custom_stoploss and self.strategy.trailing_stop + and self.strategy.trailing_only_offset_is_reached + and self.strategy.trailing_stop_positive_offset is not None + and self.strategy.trailing_stop_positive + ): + # Worst case: price reaches stop_positive_offset and dives down. + stop_rate = (sell_row[OPEN_IDX] * + (1 + side_1 * abs(self.strategy.trailing_stop_positive_offset) - + side_1 * abs(self.strategy.trailing_stop_positive / leverage))) + else: + # Worst case: price ticks tiny bit above open and dives down. + stop_rate = sell_row[OPEN_IDX] * (1 - + side_1 * abs(trade.stop_loss_pct / leverage)) + if is_short: + assert stop_rate > sell_row[LOW_IDX] + else: + assert stop_rate < sell_row[HIGH_IDX] + + # Limit lower-end to candle low to avoid sells below the low. + # This still remains "worst case" - but "worst realistic case". + if is_short: + return min(sell_row[HIGH_IDX], stop_rate) + else: + return max(sell_row[LOW_IDX], stop_rate) + + # Set close_rate to stoploss + return trade.stop_loss + + def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, + trade_dur: int) -> float: + is_short = trade.is_short or False + leverage = trade.leverage or 1.0 + side_1 = -1 if is_short else 1 + roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) + if roi is not None and roi_entry is not None: + if roi == -1 and roi_entry % self.timeframe_min == 0: + # When forceselling with ROI=-1, the roi time will always be equal to trade_dur. + # If that entry is a multiple of the timeframe (so on candle open) + # - we'll use open instead of close + return sell_row[OPEN_IDX] + + # - (Expected abs profit - open_rate - open_fee) / (fee_close -1) + roi_rate = trade.open_rate * roi / leverage + open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open) + close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1) + if is_short: + is_new_roi = sell_row[OPEN_IDX] < close_rate + else: + is_new_roi = sell_row[OPEN_IDX] > close_rate + if (trade_dur > 0 and trade_dur == roi_entry + and roi_entry % self.timeframe_min == 0 + and is_new_roi): + # new ROI entry came into effect. + # use Open rate if open_rate > calculated sell rate + return sell_row[OPEN_IDX] + + if (trade_dur == 0 and ( + ( + is_short + # Red candle (for longs) + and sell_row[OPEN_IDX] < sell_row[CLOSE_IDX] # Red candle + and trade.open_rate > sell_row[OPEN_IDX] # trade-open above open_rate + and close_rate < sell_row[CLOSE_IDX] # closes below close + ) + or + ( + not is_short + # green candle (for shorts) + and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # green candle + and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate + and close_rate > sell_row[CLOSE_IDX] # closes above close + ) + )): + # ROI on opening candles with custom pricing can only + # trigger if the entry was at Open or lower wick. + # details: https: // github.com/freqtrade/freqtrade/issues/6261 + # If open_rate is < open, only allow sells below the close on red candles. + raise ValueError("Opening candle ROI on red candles.") + + # Use the maximum between close_rate and low as we + # cannot sell outside of a candle. + # Applies when a new ROI setting comes in place and the whole candle is above that. + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) + + else: + # This should not be reached... return sell_row[OPEN_IDX] def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple @@ -501,7 +542,10 @@ class Backtesting: proposed_rate=closerate, current_profit=current_profit) # We can't place orders lower than current low. # freqtrade does not support this in live, and the order would fill immediately - closerate = max(closerate, sell_row[LOW_IDX]) + if trade.is_short: + closerate = min(closerate, sell_row[HIGH_IDX]) + else: + closerate = max(closerate, sell_row[LOW_IDX]) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['exit'] @@ -534,8 +578,8 @@ class Backtesting: ft_pair=trade.pair, order_id=str(self.order_id_counter), symbol=trade.pair, - ft_order_side="sell", - side="sell", + ft_order_side=trade.exit_side, + side=trade.exit_side, order_type=order_type, status="open", price=closerate, @@ -607,7 +651,10 @@ class Backtesting: proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate # We can't place orders higher than current high (otherwise it'd be a stop limit buy) # which freqtrade does not support in live. - propose_rate = min(propose_rate, row[HIGH_IDX]) + if direction == "short": + propose_rate = max(propose_rate, row[LOW_IDX]) + else: + propose_rate = min(propose_rate, row[HIGH_IDX]) min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 max_stake_amount = self.exchange.get_max_pair_stake_amount(pair, propose_rate) @@ -712,8 +759,8 @@ class Backtesting: ft_pair=trade.pair, order_id=str(self.order_id_counter), symbol=trade.pair, - ft_order_side="buy", - side="buy", + ft_order_side=trade.enter_side, + side=trade.enter_side, order_type=order_type, status="open", order_date=current_time, @@ -795,17 +842,17 @@ class Backtesting: timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) if timedout: - if order.side == 'buy': + if order.side == trade.enter_side: self.timedout_entry_orders += 1 if trade.nr_of_successful_entries == 0: - # Remove trade due to buy timeout expiration. + # Remove trade due to entry timeout expiration. return True else: # Close additional buy order del trade.orders[trade.orders.index(order)] - if order.side == 'sell': + if order.side == trade.exit_side: self.timedout_exit_orders += 1 - # Close sell order and retry selling on next signal. + # Close exit order and retry exiting on next signal. del trade.orders[trade.orders.index(order)] return False @@ -901,8 +948,8 @@ class Backtesting: open_trades[pair].append(trade) for trade in list(open_trades[pair]): - # 2. Process buy orders. - order = trade.select_order('buy', is_open=True) + # 2. Process entry orders. + order = trade.select_order(trade.enter_side, is_open=True) if order and self._get_order_filled(order.price, row): order.close_bt_order(current_time) trade.open_order_id = None @@ -914,7 +961,7 @@ class Backtesting: self._get_sell_trade_entry(trade, row) # Place sell order if necessary # 4. Process sell orders. - order = trade.select_order('sell', is_open=True) + order = trade.select_order(trade.exit_side, is_open=True) if order and self._get_order_filled(order.price, row): trade.open_order_id = None trade.close_date = current_time diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index ad0c0f0be..d1f88d2a5 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -4,6 +4,7 @@ Spread pair list filter import logging from typing import Any, Dict +from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList @@ -20,6 +21,12 @@ class SpreadFilter(IPairList): self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005) self._enabled = self._max_spread_ratio != 0 + if not self._exchange.exchange_has('fetchTickers'): + raise OperationalException( + 'Exchange does not support fetchTickers, therefore SpreadFilter cannot be used.' + 'Please edit your config and restart the bot.' + ) + @property def needstickers(self) -> bool: """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 392d77320..88455e86e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -592,7 +592,7 @@ class RPC: else: try: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate = tickers.get(pair, {}).get('bid', None) + rate = tickers.get(pair, {}).get('last', None) if rate: if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 485cf62b7..00e55a330 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -869,7 +869,7 @@ class IStrategy(ABC, HyperStrategyMixin): force_stoploss=force_stoploss, low=low, high=high) # Set current rate to high for backtesting sell - current_rate = high or rate + current_rate = (low if trade.is_short else high) or rate current_profit = trade.calc_profit_ratio(current_rate) # if enter signal and ignore_roi is set, we don't need to evaluate min_roi. @@ -961,9 +961,9 @@ class IStrategy(ABC, HyperStrategyMixin): else: logger.warning("CustomStoploss function did not return valid stoploss") - 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): + sl_lower_short = (trade.stop_loss < (low or current_rate) and not trade.is_short) + sl_higher_long = (trade.stop_loss > (high or current_rate) and trade.is_short) + if self.trailing_stop and (sl_lower_short or sl_higher_long): # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset @@ -981,12 +981,12 @@ class IStrategy(ABC, HyperStrategyMixin): 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)) + sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short) + sl_lower_short = (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 ((sl_higher_short or sl_lower_long) and + if ((sl_higher_long or sl_lower_short) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS diff --git a/requirements-dev.txt b/requirements-dev.txt index 5c94ac7c8..c2f3eae8a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,8 +6,8 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.6.0 -mypy==0.931 -pytest==7.0.1 +mypy==0.940 +pytest==7.1.0 pytest-asyncio==0.18.2 pytest-cov==3.0.0 pytest-mock==3.7.0 @@ -17,12 +17,12 @@ isort==5.10.1 time-machine==2.6.0 # Convert jupyter notebooks to markdown documents -nbconvert==6.4.2 +nbconvert==6.4.4 # mypy types -types-cachetools==4.2.10 +types-cachetools==5.0.0 types-filelock==3.2.5 -types-requests==2.27.11 +types-requests==2.27.12 types-tabulate==0.8.5 # Extensions to datetime library diff --git a/requirements.txt b/requirements.txt index b523defb2..1011dfb9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -numpy==1.22.2 +numpy==1.22.3 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.75.12 +ccxt==1.76.5 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 @@ -32,7 +32,7 @@ sdnotify==0.3.2 # API Server fastapi==0.75.0 -uvicorn==0.17.5 +uvicorn==0.17.6 pyjwt==2.3.0 aiofiles==0.8.0 psutil==5.9.0 diff --git a/setup.py b/setup.py index d14139cda..eb2921e73 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ setup( ], install_requires=[ # from requirements.txt - 'ccxt>=1.74.17', + 'ccxt>=1.76.5', 'SQLAlchemy', 'python-telegram-bot>=13.4', 'arrow>=0.17.0', diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5c0b55efe..634ab20b9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2720,37 +2720,36 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123}) mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', return_value={'for': 123}) + mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', return_value={'for': 123}) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', - return_value={'fee': {}, 'status': 'canceled', 'amount': 1234}) - mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', - return_value={'fee': {}, 'status': 'canceled', 'amount': 1234}) + res = {'fee': {}, 'status': 'canceled', 'amount': 1234} + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value=res) + mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', return_value=res) + mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value=res) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) - assert co == {'fee': {}, 'status': 'canceled', 'amount': 1234} + assert co == res - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', - return_value='canceled') - mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', - return_value='canceled') + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value='canceled') + mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', return_value='canceled') + mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value='canceled') # Fall back to fetch_stoploss_order co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co == {'for': 123} - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', - side_effect=InvalidOrderException("")) - mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', - side_effect=InvalidOrderException("")) - + exc = InvalidOrderException("") + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.Ftx.fetch_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', side_effect=exc) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co['amount'] == 555 assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}} with pytest.raises(InvalidOrderException): - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', - side_effect=InvalidOrderException("Did not find order")) - mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', - side_effect=InvalidOrderException("Did not find order")) + exc = InvalidOrderException("Did not find order") + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.Ftx.cancel_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', side_effect=exc) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 3ecce96aa..faac0c8af 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -1,8 +1,11 @@ +from unittest.mock import MagicMock + import pytest from freqtrade.exceptions import OperationalException from freqtrade.exchange import Gateio from freqtrade.resolvers.exchange_resolver import ExchangeResolver +from tests.conftest import get_patched_exchange def test_validate_order_types_gateio(default_conf, mocker): @@ -26,3 +29,43 @@ def test_validate_order_types_gateio(default_conf, mocker): with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): ExchangeResolver.load_exchange('gateio', default_conf, True) + + +def test_fetch_stoploss_order_gateio(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + + fetch_order_mock = MagicMock() + exchange.fetch_order = fetch_order_mock + + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert fetch_order_mock.call_count == 1 + assert fetch_order_mock.call_args_list[0][1]['order_id'] == '1234' + assert fetch_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC' + assert fetch_order_mock.call_args_list[0][1]['params'] == {'stop': True} + + +def test_cancel_stoploss_order_gateio(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + + cancel_order_mock = MagicMock() + exchange.cancel_order = cancel_order_mock + + exchange.cancel_stoploss_order('1234', 'ETH/BTC') + assert cancel_order_mock.call_count == 1 + assert cancel_order_mock.call_args_list[0][1]['order_id'] == '1234' + assert cancel_order_mock.call_args_list[0][1]['pair'] == 'ETH/BTC' + assert cancel_order_mock.call_args_list[0][1]['params'] == {'stop': True} + + +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_gateio(mocker, default_conf, sl1, sl2, sl3, side): + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + order = { + 'price': 1500, + 'stopPrice': 1500, + } + assert exchange.stoploss_adjust(sl1, order, side) + assert not exchange.stoploss_adjust(sl2, order, side) diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 29b7fd210..43ad6ecd8 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -19,6 +19,7 @@ class BTrade(NamedTuple): open_tick: int close_tick: int enter_tag: Optional[str] = None + is_short: bool = False class BTContainer(NamedTuple): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index ea95a500f..7ede1adc3 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -15,7 +15,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, # Test 0: Sell with signal sell in candle 3 # Test with Stop-loss at 1% tc0 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], # exit with stoploss hit @@ -29,7 +29,7 @@ tc0 = BTContainer(data=[ # Test 1: Stop-Loss Triggered 1% loss # Test with Stop-loss at 1% tc1 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4600, 4600, 6172, 0, 0], # exit with stoploss hit @@ -44,7 +44,7 @@ tc1 = BTContainer(data=[ # Test 2: Minus 4% Low, minus 1% close # Test with Stop-Loss at 3% tc2 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4962, 4975, 6172, 0, 0], @@ -63,7 +63,7 @@ tc2 = BTContainer(data=[ # Trade-A: Stop-Loss Triggered 2% Loss # Trade-B: Stop-Loss Triggered 2% Loss tc3 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4800, 4975, 6172, 0, 0], # exit with stoploss hit @@ -81,7 +81,7 @@ tc3 = BTContainer(data=[ # Test with Stop-loss at 2% ROI 6% # Stop-Loss Triggered 2% Loss tc4 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5750, 4850, 5750, 6172, 0, 0], # Exit with stoploss hit @@ -95,7 +95,7 @@ tc4 = BTContainer(data=[ # Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain # stop-loss: 1%, ROI: 3% tc5 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4980, 4987, 6172, 1, 0], [1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5025, 4975, 4987, 6172, 0, 0], @@ -109,7 +109,7 @@ tc5 = BTContainer(data=[ # Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss # stop-loss: 2% ROI: 5% tc6 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5300, 4850, 5050, 6172, 0, 0], # Exit with stoploss @@ -123,7 +123,7 @@ tc6 = BTContainer(data=[ # Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain # stop-loss: 2% ROI: 3% tc7 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0], @@ -138,7 +138,7 @@ tc7 = BTContainer(data=[ # Test 8: trailing_stop should raise so candle 3 causes a stoploss. # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 2 tc8 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5050, 4950, 5000, 6172, 0, 0], [2, 5000, 5250, 4750, 4850, 6172, 0, 0], @@ -152,7 +152,7 @@ tc8 = BTContainer(data=[ # Test 9: trailing_stop should raise - high and low in same candle. # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 3 tc9 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5050, 4950, 5000, 6172, 0, 0], [2, 5000, 5050, 4950, 5000, 6172, 0, 0], @@ -166,7 +166,7 @@ tc9 = BTContainer(data=[ # without applying trailing_stop_positive since stoploss_offset is at 10%. # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc10 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], @@ -182,7 +182,7 @@ tc10 = BTContainer(data=[ # applying a positive trailing stop of 3% since stop_positive_offset is reached. # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc11 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], @@ -198,7 +198,7 @@ tc11 = BTContainer(data=[ # applying a positive trailing stop of 3% since stop_positive_offset is reached. # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc12 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 4650, 5100, 6172, 0, 0], @@ -213,7 +213,7 @@ tc12 = BTContainer(data=[ # Test 13: Buy and sell ROI on same candle # stop-loss: 10% (should not apply), ROI: 1% tc13 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 4850, 5100, 6172, 0, 0], @@ -226,7 +226,7 @@ tc13 = BTContainer(data=[ # Test 14 - Buy and Stoploss on same candle # stop-loss: 5%, ROI: 10% (should not apply) tc14 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4600, 5100, 6172, 0, 0], [2, 5100, 5251, 4850, 5100, 6172, 0, 0], @@ -240,7 +240,7 @@ tc14 = BTContainer(data=[ # Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle # stop-loss: 5%, ROI: 10% (should not apply) tc15 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4900, 5100, 6172, 1, 0], [2, 5100, 5251, 4650, 5100, 6172, 0, 0], @@ -255,7 +255,7 @@ tc15 = BTContainer(data=[ # Causes negative profit even though sell-reason is ROI. # stop-loss: 10%, ROI: 10% (should not apply), -100% after 65 minutes (limits trade duration) tc16 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0], @@ -271,7 +271,7 @@ tc16 = BTContainer(data=[ # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) # Uses open as sell-rate (special case) - since the roi-time is a multiple of the timeframe. tc17 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0], @@ -287,7 +287,7 @@ tc17 = BTContainer(data=[ # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) # uses open_rate as sell-price tc18 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5200, 6172, 0, 0], @@ -302,7 +302,7 @@ tc18 = BTContainer(data=[ # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) # uses calculated ROI (1%) as sell rate, otherwise identical to tc18 tc19 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5200, 6172, 0, 0], @@ -317,7 +317,7 @@ tc19 = BTContainer(data=[ # stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) # uses calculated ROI (1%) as sell rate, otherwise identical to tc18 tc20 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5200, 6172, 0, 0], @@ -333,7 +333,7 @@ tc20 = BTContainer(data=[ # which cannot happen in reality # stop-loss: 10%, ROI: 4%, Trailing stop adjusted at the sell candle tc21 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 4650, 5100, 6172, 0, 0], @@ -349,7 +349,7 @@ tc21 = BTContainer(data=[ # applying a positive trailing stop of 3% - ROI should apply before trailing stop. # stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2 tc22 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], @@ -361,6 +361,23 @@ tc22 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) + +# Test 22s: trailing_stop Raises in candle 2 - but ROI applies at the same time. +# applying a positive trailing stop of 3% - ROI should apply before trailing stop. +# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2 +tc22s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5050, 4900, 4900, 6172, 0, 0, 0, 0], + [2, 4900, 4900, 4749, 4900, 6172, 0, 0, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]], + stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2, is_short=True)] +) + # Test 23: trailing_stop Raises in candle 2 (does not trigger) # applying a positive trailing stop of 3% since stop_positive_offset is reached. # ROI is changed after this to 4%, dropping ROI below trailing_stop_positive, causing a sell @@ -368,7 +385,7 @@ tc22 = BTContainer(data=[ # Stoploss would trigger in this candle too, but it's no longer relevant. # stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2, ROI adjusted in candle 3 (causing the sell) tc23 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], @@ -384,7 +401,7 @@ tc23 = BTContainer(data=[ # Stoploss at 1%. # Stoploss wins over Sell-signal (because sell-signal is acted on in the next candle) tc24 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], @@ -399,7 +416,7 @@ tc24 = BTContainer(data=[ # Stoploss at 1%. # Sell-signal wins over stoploss tc25 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], @@ -410,11 +427,44 @@ tc25 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) +# Test 25l: (copy of test25 with leverage) +# Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Sell-signal wins over stoploss +tc25l = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4986, 6172, 0, 0], + [3, 5010, 5010, 4986, 5010, 6172, 0, 1], + [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], + stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, + leverage=5.0, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +) + +# Test 25s: (copy of test25 with leverage and as short) +# Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Sell-signal wins over stoploss +tc25s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5025, 4975, 4987, 6172, 0, 0, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0, 0, 0], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4986, 6172, 0, 0, 0, 0], + [3, 5010, 5010, 4986, 5010, 6172, 0, 0, 0, 1], + [4, 4990, 5010, 4855, 4995, 6172, 0, 0, 0, 0], # Triggers stoploss + sellsignal acted on + [5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0]], + stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, + leverage=5.0, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)] +) # Test 26: Sell with signal sell in candle 3 (ROI at signal candle) # Stoploss at 10% (irrelevant), ROI at 5% (will trigger) # Sell-signal wins over stoploss tc26 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], @@ -428,7 +478,7 @@ tc26 = BTContainer(data=[ # Test 27: Sell with signal sell in candle 3 (ROI at signal candle) # Stoploss at 10% (irrelevant), ROI at 5% (will trigger) - Wins over Sell-signal tc27 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], @@ -444,7 +494,7 @@ tc27 = BTContainer(data=[ # therefore "open" will be used # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 tc28 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], @@ -456,11 +506,30 @@ tc28 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) +# Test 28s: trailing_stop should raise so candle 3 causes a stoploss +# Same case than tc11 - but candle 3 "gaps down" - the stoploss will be above the candle, +# therefore "open" will be used +# stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2 +tc28s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5050, 4890, 4890, 6172, 0, 0, 0, 0], + [2, 4890, 4890, 4749, 4890, 6172, 0, 0, 0, 0], + [3, 5150, 5350, 4950, 4950, 6172, 0, 0, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[ + BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True) + ] +) + # Test 29: trailing_stop should be triggered by low of next candle, without adjusting stoploss using # high of stoploss candle. # stop-loss: 10%, ROI: 10% (should not apply) tc29 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5050, 5000, 5000, 6172, 0, 0], # enter trade (signal on last candle) [2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Triggers trailing-stoploss @@ -474,7 +543,7 @@ tc29 = BTContainer(data=[ # Test 30: trailing_stop should be triggered immediately on trade open candle. # stop-loss: 10%, ROI: 10% (should not apply) tc30 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4900, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0], @@ -488,7 +557,7 @@ tc30 = BTContainer(data=[ # Test 31: trailing_stop should be triggered immediately on trade open candle. # stop-loss: 10%, ROI: 10% (should not apply) tc31 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4900, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0], @@ -503,7 +572,7 @@ tc31 = BTContainer(data=[ # Test 32: trailing_stop should be triggered immediately on trade open candle. # stop-loss: 1%, ROI: 10% (should not apply) tc32 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # enter trade and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0], @@ -534,10 +603,31 @@ tc33 = BTContainer(data=[ enter_tag='buy_signal_01' )] ) +# Test 33s: trailing_stop should be triggered immediately on trade open candle. +# copy of Test33 using shorts. +# stop-loss: 1%, ROI: 10% (should not apply) +tc33s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0, 'short_signal_01'], + [1, 5000, 5049, 4500, 5000, 6172, 0, 0, 0, 0, None], # enter trade and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0, None], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0, None], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0, None]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, + trailing_stop_positive=0.01, use_custom_stoploss=True, + trades=[BTrade( + sell_reason=SellType.TRAILING_STOP_LOSS, + open_tick=1, + close_tick=1, + enter_tag='short_signal_01', + is_short=True, + )] +) # Test 34: Custom-entry-price below all candles should timeout - so no trade happens. tc34 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # timeout [2, 4900, 5250, 4500, 5100, 6172, 0, 0], @@ -549,7 +639,7 @@ tc34 = BTContainer(data=[ # Test 35: Custom-entry-price above all candles should have rate adjusted to "entry candle high" tc35 = BTContainer(data=[ - # D O H L C V B S + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Timeout [2, 4900, 5250, 4500, 5100, 6172, 0, 0], @@ -558,7 +648,21 @@ tc35 = BTContainer(data=[ stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, custom_entry_price=7200, trades=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1) -] +]) + +# Test 35s: Custom-entry-price above all candles should have rate adjusted to "entry candle high" +tc35s = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0, 0, 0], # Timeout + [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, + custom_entry_price=4000, + trades=[ + BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True) + ] ) # Test 36: Custom-entry-price around candle low @@ -566,7 +670,7 @@ tc35 = BTContainer(data=[ # below open, we treat this as cheating, and delay the sell by 1 candle. # details: https://github.com/freqtrade/freqtrade/issues/6261 tc36 = BTContainer(data=[ - # D O H L C V B S BT + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 4999, 6172, 0, 0], # Enter and immediate ROI [2, 4900, 5250, 4500, 5100, 6172, 0, 0], @@ -581,7 +685,7 @@ tc36 = BTContainer(data=[ # Would cause immediate ROI exit below close # details: https://github.com/freqtrade/freqtrade/issues/6261 tc37 = BTContainer(data=[ - # D O H L C V B S BT + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5400, 5500, 4951, 5100, 6172, 0, 0], # Enter and immediate ROI [2, 4900, 5250, 4500, 5100, 6172, 0, 0], @@ -595,7 +699,7 @@ tc37 = BTContainer(data=[ # Test 38: Custom exit price below all candles # Price adjusted to candle Low. tc38 = BTContainer(data=[ - # D O H L C V B S BT + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], [2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout @@ -610,10 +714,10 @@ tc38 = BTContainer(data=[ # Test 39: Custom exit price above all candles # causes sell signal timeout tc39 = BTContainer(data=[ - # D O H L C V B S BT + # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5500, 4951, 5000, 6172, 0, 0], - [2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout + [2, 4950, 5250, 4900, 5100, 6172, 0, 1], # exit - entry timeout [3, 5100, 5100, 4950, 4950, 6172, 0, 0], [4, 5000, 5100, 4950, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, @@ -622,21 +726,32 @@ tc39 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] ) -# Test 39: (copy of test25 with leverage) -# Sell with signal sell in candle 3 (stoploss also triggers on this candle) -# Stoploss at 1%. -# Sell-signal wins over stoploss -tc39 = BTContainer(data=[ - # D O H L C V B S - [0, 5000, 5025, 4975, 4987, 6172, 1, 0], - [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) - [2, 4987, 5012, 4986, 4986, 6172, 0, 0], - [3, 5010, 5010, 4986, 5010, 6172, 0, 1], - [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on - [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], - stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, - leverage=5.0, - trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +# Test 39: Custom short exit price above below candles +# causes sell signal timeout +tc39a = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0], + [1, 5000, 5000, 4951, 5000, 6172, 0, 0, 0, 0], + [2, 4910, 5150, 4910, 5100, 6172, 0, 0, 0, 1], # exit - entry timeout + [3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0], + [4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, + use_sell_signal=True, + custom_exit_price=4700, + trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)] +) + +# Test 40: Colliding long and short signal +tc40 = BTContainer(data=[ + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0, 0, 0], + [2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 0], + [3, 5100, 5100, 4950, 4950, 6172, 0, 0, 0, 0], + [4, 5000, 5100, 4950, 4950, 6172, 0, 0, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, + use_sell_signal=True, + trades=[] ) @@ -664,24 +779,31 @@ TESTS = [ tc20, tc21, tc22, + tc22s, tc23, tc24, tc25, + tc25l, + tc25s, tc26, tc27, tc28, + tc28s, tc29, tc30, tc31, tc32, tc33, + tc33s, tc34, tc35, + tc35s, tc36, tc37, tc38, tc39, - # TODO-lev: Add tests for short here + tc39a, + tc40, ] @@ -708,11 +830,10 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) + # TODO: Should we initialize this properly?? + backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) backtesting.required_startup = 0 - if data.leverage > 1.0: - # TODO: Should we initialize this properly?? - backtesting._can_short = True backtesting.strategy.advise_entry = lambda a, m: frame backtesting.strategy.advise_exit = lambda a, m: frame if data.custom_entry_price: @@ -739,8 +860,9 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: assert round(results["profit_ratio"].sum(), 3) == round(data.profit_perc, 3) for c, trade in enumerate(data.trades): - res = results.iloc[c] + res: BTrade = results.iloc[c] assert res.sell_reason == trade.sell_reason.value assert res.enter_tag == trade.enter_tag assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) + assert res.is_short == trade.is_short diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 0d15c23e8..3c6e5df4b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -582,13 +582,17 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: pair = 'UNITTEST/USDT:USDT' row = [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), - 1, # Buy 0.001, # Open - 0.0011, # Close - 0, # Sell - 0.00099, # Low 0.0012, # High - '', # Buy Signal Name + 0.00099, # Low + 0.0011, # Close + 1, # enter_long + 0, # exit_long + 1, # enter_short + 0, # exit_hsort + '', # Long Signal Name + '', # Short Signal Name + '', # Exit Signal Name ] backtesting.strategy.leverage = MagicMock(return_value=5.0) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 8d7e2b5c1..522a43a1c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -782,6 +782,19 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None get_patched_freqtradebot(mocker, default_conf) +def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> None: + default_conf['pairlists'] = [{'method': 'StaticPairList'}, {'method': 'SpreadFilter'}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + exchange_has=MagicMock(return_value=False), + ) + + with pytest.raises(OperationalException, + match=r'Exchange does not support fetchTickers, .*'): + get_patched_freqtradebot(mocker, default_conf) + + @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): whitelist_conf['pairlists'][0]['method'] = pairlist diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 11fb5e0a1..d93208607 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -650,8 +650,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): rpc._fiat_converter = CryptoToFiatConverter() result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) - assert prec_satoshi(result['total'], 30.309096315) - assert prec_satoshi(result['value'], 454636.44472997) + assert prec_satoshi(result['total'], 30.30909624) + assert prec_satoshi(result['value'], 454636.44360691) assert tickers.call_count == 1 assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] @@ -685,7 +685,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 5.0, 'balance': 10.0, 'currency': 'USDT', - 'est_stake': 0.0011563153318162476, + 'est_stake': 0.0011562404610161968, 'used': 5.0, 'stake': 'BTC', 'is_position': False, diff --git a/tests/test_periodiccache.py b/tests/test_periodiccache.py index f874f9041..b2bd8ba2b 100644 --- a/tests/test_periodiccache.py +++ b/tests/test_periodiccache.py @@ -26,7 +26,9 @@ def test_ttl_cache(): assert 'a' in cache1h t.move_to("2021-09-01 05:59:59 +00:00") + assert 'a' not in cache assert 'a' in cache1h t.move_to("2021-09-01 06:00:00 +00:00") + assert 'a' not in cache assert 'a' not in cache1h