From 53ecdb931b040ef6afe5d632e0f84249cea2879b Mon Sep 17 00:00:00 2001 From: dingzhoufeng Date: Tue, 8 Mar 2022 12:26:43 +0800 Subject: [PATCH 01/58] add leverage --- freqtrade/optimize/backtesting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fa3deb86f..2b5b4ee14 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -355,6 +355,8 @@ class Backtesting: def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, trade_dur: int) -> float: + leverage = trade.leverage or 1.0 + is_short = trade.is_short or False """ Get close rate for backtesting result """ @@ -382,7 +384,7 @@ class Backtesting: 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)) + stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct / leverage)) 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". @@ -400,7 +402,7 @@ class Backtesting: return sell_row[OPEN_IDX] # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - close_rate = - (trade.open_rate * roi + trade.open_rate * + close_rate = - (trade.open_rate * roi / leverage + trade.open_rate * (1 + trade.fee_open)) / (trade.fee_close - 1) if (trade_dur > 0 and trade_dur == roi_entry From 61182f849bd252b5b38a91c48fe29b25e7f01ebb Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 9 Mar 2022 00:45:10 -0600 Subject: [PATCH 02/58] exchange.fetch_order and exchange.cancel_order added params argument --- freqtrade/exchange/exchange.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index da89a7c8a..81d20fd21 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -376,7 +376,7 @@ class Exchange: raise OperationalException( 'Could not load markets, therefore cannot start. ' 'Please investigate the above error for more details.' - ) + ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( @@ -882,11 +882,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) return order except ccxt.OrderNotFound as e: @@ -929,7 +929,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) @@ -940,7 +940,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) return order except ccxt.InvalidOrder as e: From e3ced55f5c2979c30d3ad849ef6e20a000bd12da Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 9 Mar 2022 00:45:50 -0600 Subject: [PATCH 03/58] gateio.fetch_order and gateio.cancel_order --- freqtrade/exchange/gateio.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 7e1f21921..7580281cf 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -31,4 +31,18 @@ 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.') + 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} + ) From ae4742afcb78d58d30ec88125cdb526204780a62 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 9 Mar 2022 00:59:28 -0600 Subject: [PATCH 04/58] test_fetch_stoploss_order_gateio and test_cancel_stoploss_order_gateio --- tests/exchange/test_gateio.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 6f7862909..a3ffcad65 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,29 @@ 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} From d47274066ecdf09e5364773efd32853e2a763d7c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 9 Mar 2022 01:05:21 -0600 Subject: [PATCH 05/58] Added stoploss_on_exchange flag to gateio --- freqtrade/exchange/gateio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 7580281cf..9f857c344 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -22,6 +22,7 @@ class Gateio(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, "ohlcv_volume_currency": "quote", + "stoploss_on_exchange": True, } _headers = {'X-Gate-Channel-Id': 'freqtrade'} From 82e0eca128d3c7013b65f1ce750afb400871b338 Mon Sep 17 00:00:00 2001 From: adriance Date: Wed, 9 Mar 2022 20:00:06 +0800 Subject: [PATCH 06/58] add short close rate calu --- freqtrade/optimize/backtesting.py | 125 +++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 37 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9523ca92f..a76138bbf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -362,11 +362,18 @@ 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] + if is_short: + if trade.stop_loss < sell_row[LOW_IDX]: + # our stoploss was already lower than candle high, + # possibly due to a cancelled trade exit. + # sell at open price. + return sell_row[OPEN_IDX] + else: + 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 @@ -379,16 +386,29 @@ class Backtesting: and self.strategy.trailing_stop_positive ): # Worst case: price reaches stop_positive_offset and dives down. - stop_rate = (sell_row[OPEN_IDX] * + if is_short: + stop_rate = (sell_row[OPEN_IDX] * + (1 - abs(self.strategy.trailing_stop_positive_offset) + + abs(self.strategy.trailing_stop_positive))) + else: + 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 / leverage)) - assert stop_rate < sell_row[HIGH_IDX] + if is_short: + stop_rate = sell_row[OPEN_IDX] * (1 + abs(trade.stop_loss_pct / leverage)) + assert stop_rate > sell_row[HIGH_IDX] + else: + stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct / leverage)) + 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) + 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 @@ -402,32 +422,60 @@ class Backtesting: return sell_row[OPEN_IDX] # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - close_rate = - (trade.open_rate * roi / leverage + trade.open_rate * + if is_short: + close_rate = (trade.open_rate * + (1 - trade.fee_open) - trade.open_rate * roi / leverage) / (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] + else: + close_rate = - (trade.open_rate * roi / leverage + 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 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 is_short: + 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.") + else: + 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.") - 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]) + if is_short: + return max(min(close_rate, sell_row[HIGH_IDX]), sell_row[LOW_IDX]) + else: + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... @@ -610,7 +658,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) @@ -700,13 +751,13 @@ class Backtesting: trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - trade.set_isolated_liq(self.exchange.get_liquidation_price( - pair=pair, - open_rate=propose_rate, - amount=amount, - leverage=leverage, - is_short=is_short, - )) + # trade.set_isolated_liq(self.exchange.get_liquidation_price( + # pair=pair, + # open_rate=propose_rate, + # amount=amount, + # leverage=leverage, + # is_short=is_short, + # )) order = Order( id=self.order_id_counter, From 1c86e69c34e3e35923cac1a4b400de32a75525da Mon Sep 17 00:00:00 2001 From: adriance Date: Wed, 9 Mar 2022 21:55:13 +0800 Subject: [PATCH 07/58] use filled time calculate duration --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/models.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a76138bbf..5aa3974d0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -534,7 +534,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time - trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) + trade_dur = int((trade.close_date_utc - trade.filled_date_utc).total_seconds() // 60) try: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) except ValueError: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b80d75dc0..3836424c4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -366,6 +366,10 @@ class LocalTrade(): else: return self.amount + @property + def filled_date_utc(self): + return self.select_order('buy', is_open=False).order_filled_date.replace(tzinfo=timezone.utc) + @property def open_date_utc(self): return self.open_date.replace(tzinfo=timezone.utc) From d579febfec9cd7c02f8bc95c59c0d5f91b69173c Mon Sep 17 00:00:00 2001 From: adriance Date: Wed, 9 Mar 2022 23:55:57 +0800 Subject: [PATCH 08/58] add filled time --- freqtrade/data/btanalysis.py | 3 ++- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/persistence/models.py | 8 ++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 4df8b2838..f0e0ccfd8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -19,7 +19,7 @@ from freqtrade.persistence import LocalTrade, Trade, init_db logger = logging.getLogger(__name__) # Newest format -BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', +BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'buy_filled_date', 'close_date', 'open_rate', 'close_rate', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', @@ -316,6 +316,7 @@ def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: if len(df) > 0: df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True) df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True) + df.loc[:, 'buy_filled_date'] = pd.to_datetime(df['buy_filled_date'], utc=True) df.loc[:, 'close_rate'] = df['close_rate'].astype('float64') return df diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5aa3974d0..cb057d8eb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -534,7 +534,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time - trade_dur = int((trade.close_date_utc - trade.filled_date_utc).total_seconds() // 60) + trade_dur = int((trade.close_date_utc - trade.buy_filled_date_utc).total_seconds() // 60) try: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) except ValueError: @@ -960,6 +960,7 @@ class Backtesting: if order and self._get_order_filled(order.price, row): order.close_bt_order(current_time) trade.open_order_id = None + trade.buy_filled_date = current_time LocalTrade.add_bt_trade(trade) self.wallets.update() diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3836424c4..fbf150ec5 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -302,6 +302,7 @@ class LocalTrade(): amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime + buy_filled_date: datetime close_date: Optional[datetime] = None open_order_id: Optional[str] = None # absolute value of the stop loss @@ -367,8 +368,8 @@ class LocalTrade(): return self.amount @property - def filled_date_utc(self): - return self.select_order('buy', is_open=False).order_filled_date.replace(tzinfo=timezone.utc) + def buy_filled_date_utc(self): + return self.buy_filled_date.replace(tzinfo=timezone.utc) @property def open_date_utc(self): @@ -448,6 +449,9 @@ class LocalTrade(): 'open_rate_requested': self.open_rate_requested, 'open_trade_value': round(self.open_trade_value, 8), + 'buy_filled_date': self.buy_filled_date.strftime(DATETIME_PRINT_FORMAT), + 'buy_filled_timestamp': int(self.buy_filled_date.replace(tzinfo=timezone.utc).timestamp() * 1000), + 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), 'close_timestamp': int(self.close_date.replace( From 499e9c3e982d437e429aa4c9ae42724ca2f429ea Mon Sep 17 00:00:00 2001 From: adriance Date: Thu, 10 Mar 2022 00:34:59 +0800 Subject: [PATCH 09/58] fix duration --- freqtrade/optimize/backtesting.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cb057d8eb..de42aa5f4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -357,6 +357,7 @@ class Backtesting: trade_dur: int) -> float: leverage = trade.leverage or 1.0 is_short = trade.is_short or False + filled_dur = int((trade.close_date_utc - trade.buy_filled_date_utc).total_seconds() // 60) """ Get close rate for backtesting result """ @@ -378,7 +379,7 @@ class Backtesting: # 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 sell.sell_type == SellType.TRAILING_STOP_LOSS and (trade_dur == 0 or filled_dur == 0): if ( not self.strategy.use_custom_stoploss and self.strategy.trailing_stop and self.strategy.trailing_only_offset_is_reached @@ -425,7 +426,7 @@ class Backtesting: if is_short: close_rate = (trade.open_rate * (1 - trade.fee_open) - trade.open_rate * roi / leverage) / (trade.fee_close + 1) - if (trade_dur > 0 and trade_dur == roi_entry + if (trade_dur > 0 and filled_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. @@ -435,7 +436,7 @@ class Backtesting: close_rate = - (trade.open_rate * roi / leverage + trade.open_rate * (1 + trade.fee_open)) / (trade.fee_close - 1) - if (trade_dur > 0 and trade_dur == roi_entry + if (trade_dur > 0 and filled_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. @@ -444,7 +445,7 @@ class Backtesting: if is_short: if ( - trade_dur == 0 + (trade_dur == 0 or filled_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 @@ -457,7 +458,7 @@ class Backtesting: raise ValueError("Opening candle ROI on red candles.") else: if ( - trade_dur == 0 + (trade_dur == 0 or filled_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 @@ -534,7 +535,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time - trade_dur = int((trade.close_date_utc - trade.buy_filled_date_utc).total_seconds() // 60) + trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) try: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) except ValueError: From 6bb93bdc25d3a00c97aca3eb2ab6a8355f1ad268 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 9 Mar 2022 15:47:16 -0600 Subject: [PATCH 10/58] moved binance.stoploss_adjust to exchange class --- freqtrade/exchange/binance.py | 7 ------- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 12 ++++++++++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 37ead6dd8..ecdd002b2 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -23,13 +23,6 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: - """ - Verify stop_loss against stoploss-order value (limit or price) - Returns True if adjustment is necessary. - """ - return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) - async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, is_new_pair: bool = False, raise_: bool = False diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 81d20fd21..8db261f63 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -795,7 +795,7 @@ class Exchange: Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - raise OperationalException(f"stoploss is not implemented for {self.name}.") + return stop_loss > float(order['stopPrice']) def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: params = self._params.copy() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 527e8050b..1b33e836b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3058,3 +3058,15 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_r ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected + + +@pytest.mark.parametrize('exchange_name', ['binance', 'gateio']) +def test_stoploss_adjust(mocker, default_conf, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + order = { + 'type': 'stop_loss_limit', + 'price': 1500, + 'stopPrice': 1500, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) From 7db28b1b1697b937af0106668250670a1c22ca94 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 9 Mar 2022 15:54:17 -0600 Subject: [PATCH 11/58] gateio stoploss docs --- docs/exchanges.md | 3 +++ docs/stoploss.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 8adf19081..df11a5971 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" + Binance 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 d0e106d8f..62081b540 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. From 6f4d6079023fc75987f7c08087ea4d9e16020387 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 9 Mar 2022 19:31:51 -0600 Subject: [PATCH 12/58] stoploss_adjust fixed breaking tests --- tests/exchange/test_binance.py | 14 -------------- tests/exchange/test_exchange.py | 3 --- tests/test_freqtradebot.py | 4 +--- 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index d88ae9b1d..bf9340217 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -94,20 +94,6 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_binance(mocker, default_conf): - exchange = get_patched_exchange(mocker, default_conf, id='binance') - order = { - 'type': 'stop_loss_limit', - 'price': 1500, - 'info': {'stopPrice': 1500}, - } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) - # Test with invalid order case - order['type'] = 'stop_loss' - assert not exchange.stoploss_adjust(1501, order) - - @pytest.mark.asyncio async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): ohlcv = [ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1b33e836b..fa9020ba4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2698,9 +2698,6 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss_adjust(1, {}) - def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e5f8d3694..492e55715 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1563,9 +1563,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, 'type': 'stop_loss_limit', 'price': 3, 'average': 2, - 'info': { - 'stopPrice': '2.178' - } + 'stopPrice': '2.178' }) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) From f343036e6695be138d07be3ca8e59c507379b8c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Mar 2022 19:23:20 +0100 Subject: [PATCH 13/58] Add stoploss-ordertypes mapping for gateio --- freqtrade/exchange/gateio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 9f857c344..ca57f85b3 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -22,6 +22,7 @@ class Gateio(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, "ohlcv_volume_currency": "quote", + "stoploss_order_types": {"limit": "limit"}, "stoploss_on_exchange": True, } From 2ba79a32a02abb5a08b3fa3c18a296e6b3cb43ae Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 12 Mar 2022 19:42:40 -0600 Subject: [PATCH 14/58] Update docs/exchanges.md Co-authored-by: Matthias --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index df11a5971..c2368170d 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -211,7 +211,7 @@ OKX requires a passphrase for each api key, you will therefore need to add this ## Gate.io !!! Tip "Stoploss on Exchange" - Binance 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 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. From 7e7e5963724e56ed052cb607df2cf4c145867eb0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 12 Mar 2022 20:07:50 -0600 Subject: [PATCH 15/58] Revert "moved binance.stoploss_adjust to exchange class" This reverts commit 6bb93bdc25d3a00c97aca3eb2ab6a8355f1ad268. --- freqtrade/exchange/binance.py | 7 +++++++ freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 12 ------------ 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index ecdd002b2..37ead6dd8 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -23,6 +23,13 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, is_new_pair: bool = False, raise_: bool = False diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 211531c34..a502ad034 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -795,7 +795,7 @@ class Exchange: Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return stop_loss > float(order['stopPrice']) + raise OperationalException(f"stoploss is not implemented for {self.name}.") def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: params = self._params.copy() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index db4d66fa5..b4b19a6d4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3062,15 +3062,3 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_r ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected - - -@pytest.mark.parametrize('exchange_name', ['binance', 'gateio']) -def test_stoploss_adjust(mocker, default_conf, exchange_name): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - order = { - 'type': 'stop_loss_limit', - 'price': 1500, - 'stopPrice': 1500, - } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) From 91549d3254a481bc187865b4d48d4406cc68013b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 12 Mar 2022 20:07:56 -0600 Subject: [PATCH 16/58] Revert "stoploss_adjust fixed breaking tests" This reverts commit 6f4d6079023fc75987f7c08087ea4d9e16020387. --- tests/exchange/test_binance.py | 14 ++++++++++++++ tests/exchange/test_exchange.py | 3 +++ tests/test_freqtradebot.py | 4 +++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index bf9340217..d88ae9b1d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -94,6 +94,20 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): assert order['amount'] == 1 +def test_stoploss_adjust_binance(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='binance') + order = { + 'type': 'stop_loss_limit', + 'price': 1500, + 'info': {'stopPrice': 1500}, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case + order['type'] = 'stop_loss' + assert not exchange.stoploss_adjust(1501, order) + + @pytest.mark.asyncio async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): ohlcv = [ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b4b19a6d4..ff8383997 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2705,6 +2705,9 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): + exchange.stoploss_adjust(1, {}) + def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index caa9a824f..1aeb56cdd 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1588,7 +1588,9 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, 'type': 'stop_loss_limit', 'price': 3, 'average': 2, - 'stopPrice': '2.178' + 'info': { + 'stopPrice': '2.178' + } }) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) From 843606c9cbd4563beba7fbf405f03ddc012508c1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 12 Mar 2022 20:14:23 -0600 Subject: [PATCH 17/58] gateio stoploss adjust --- freqtrade/exchange/gateio.py | 7 +++++++ tests/exchange/test_gateio.py | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index ca57f85b3..bfe996e86 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -48,3 +48,10 @@ class Gateio(Exchange): pair=pair, params={'stop': True} ) + + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return stop_loss > float(order['stopPrice']) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index a3ffcad65..ce356be8c 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -55,3 +55,13 @@ def test_cancel_stoploss_order_gateio(default_conf, mocker): 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} + + +def test_stoploss_adjust_gateio(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='gateio') + order = { + 'price': 1500, + 'stopPrice': 1500, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) From 32c06f4a05c4c804f98be8a923f8fda7f53f13b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 16:45:11 +0100 Subject: [PATCH 18/58] Improve test --- tests/test_periodiccache.py | 2 ++ 1 file changed, 2 insertions(+) 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 From c63b5fbbbf22807526da4eb34411287eeb415be9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Mar 2022 17:53:52 +0100 Subject: [PATCH 19/58] Use last to get rates for /balance endpoints --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7a602978e..3d4fffbc9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -582,7 +582,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/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 6bfee8e86..d738760be 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -605,8 +605,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'], 12.309096315) - assert prec_satoshi(result['value'], 184636.44472997) + assert prec_satoshi(result['total'], 12.30909624) + assert prec_satoshi(result['value'], 184636.443606915) assert tickers.call_count == 1 assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] @@ -624,17 +624,16 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'est_stake': 0.30794, 'used': 4.0, 'stake': 'BTC', - }, {'free': 5.0, 'balance': 10.0, 'currency': 'USDT', - 'est_stake': 0.0011563153318162476, + 'est_stake': 0.0011562404610161968, 'used': 5.0, 'stake': 'BTC', } ] - assert result['total'] == 12.309096315331816 + assert result['total'] == 12.309096240461017 def test_rpc_start(mocker, default_conf) -> None: From be5b0acfbda471a966b967cea1a6c6c213641cbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:01:00 +0000 Subject: [PATCH 20/58] Bump pytest from 7.0.1 to 7.1.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.0.1...7.1.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5c94ac7c8..1d393538b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.6.0 mypy==0.931 -pytest==7.0.1 +pytest==7.1.0 pytest-asyncio==0.18.2 pytest-cov==3.0.0 pytest-mock==3.7.0 From 7764ad1541fcc81e40ab3b94ad33ca4a0612ea6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:01:03 +0000 Subject: [PATCH 21/58] Bump types-cachetools from 4.2.10 to 5.0.0 Bumps [types-cachetools](https://github.com/python/typeshed) from 4.2.10 to 5.0.0. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5c94ac7c8..0f9d4d105 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ time-machine==2.6.0 nbconvert==6.4.2 # mypy types -types-cachetools==4.2.10 +types-cachetools==5.0.0 types-filelock==3.2.5 types-requests==2.27.11 types-tabulate==0.8.5 From 3a0ad2f26e2b96a2210f4da44ce08e767c4054cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:01:07 +0000 Subject: [PATCH 22/58] Bump uvicorn from 0.17.5 to 0.17.6 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.17.5 to 0.17.6. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.17.5...0.17.6) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f8b6049f1..095735180 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 3fc1c94aba6bdb7045153d793aab9b1427c73c8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:01:14 +0000 Subject: [PATCH 23/58] Bump ccxt from 1.75.12 to 1.76.5 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.75.12 to 1.76.5. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.75.12...1.76.5) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f8b6049f1..245fa63d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 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 From 4cbdc9a74ffe6cc39002b03739335f4a4d58cbb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:01:17 +0000 Subject: [PATCH 24/58] Bump types-requests from 2.27.11 to 2.27.12 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.11 to 2.27.12. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5c94ac7c8..beae48283 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ nbconvert==6.4.2 # mypy types types-cachetools==4.2.10 types-filelock==3.2.5 -types-requests==2.27.11 +types-requests==2.27.12 types-tabulate==0.8.5 # Extensions to datetime library From a7133f1974bf3f873eef7eb441ea1c924a85b10b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:01:20 +0000 Subject: [PATCH 25/58] Bump nbconvert from 6.4.2 to 6.4.4 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.4.2 to 6.4.4. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.4.2...6.4.4) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5c94ac7c8..26c873b1f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,7 +17,7 @@ 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 From 3fbe4a9944369ad9b1e829838709ac53bf5a6914 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:01:30 +0000 Subject: [PATCH 26/58] Bump numpy from 1.22.2 to 1.22.3 Bumps [numpy](https://github.com/numpy/numpy) from 1.22.2 to 1.22.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.22.2...v1.22.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f8b6049f1..b9c6d3d06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.22.2 +numpy==1.22.3 pandas==1.4.1 pandas-ta==0.3.14b From 3d9c55d5193dedb4861d8ed7df9159e8453e4041 Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 11:29:26 +0800 Subject: [PATCH 27/58] restore set_isolated_liq --- freqtrade/optimize/backtesting.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e7d340130..ff6db1b08 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -751,13 +751,13 @@ class Backtesting: trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - # trade.set_isolated_liq(self.exchange.get_liquidation_price( - # pair=pair, - # open_rate=propose_rate, - # amount=amount, - # leverage=leverage, - # is_short=is_short, - # )) + trade.set_isolated_liq(self.exchange.get_liquidation_price( + pair=pair, + open_rate=propose_rate, + amount=amount, + leverage=leverage, + is_short=is_short, + )) order = Order( id=self.order_id_counter, From f9e93cf3f8bf35822ed2d9be58efe9cfa23147e6 Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 11:55:36 +0800 Subject: [PATCH 28/58] fix buy filled date none --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index fbf150ec5..50ff5db3d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -302,7 +302,7 @@ class LocalTrade(): amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime - buy_filled_date: datetime + buy_filled_date: Optional[datetime] = None close_date: Optional[datetime] = None open_order_id: Optional[str] = None # absolute value of the stop loss From a7503697964c7dc333167786d62ee44ef726f8cc Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 12:09:13 +0800 Subject: [PATCH 29/58] adjust none --- freqtrade/persistence/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 50ff5db3d..f5e63159b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -449,8 +449,10 @@ class LocalTrade(): 'open_rate_requested': self.open_rate_requested, 'open_trade_value': round(self.open_trade_value, 8), - 'buy_filled_date': self.buy_filled_date.strftime(DATETIME_PRINT_FORMAT), - 'buy_filled_timestamp': int(self.buy_filled_date.replace(tzinfo=timezone.utc).timestamp() * 1000), + 'buy_filled_date': (self.buy_filled_date.strftime(DATETIME_PRINT_FORMAT) + if self.buy_filled_date else None), + 'buy_filled_timestamp': int(self.buy_filled_date.replace( + tzinfo=timezone.utc).timestamp() * 1000) if self.buy_filled_date else None, 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), From 404d700a74b919cfefb467bae9b1910d5a0f056a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Mar 2022 06:23:48 +0100 Subject: [PATCH 30/58] Raise min-requirement for ccxt --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ec41228c1..a89e717a1 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', From 5462ff0ebc9f6ad09af17db0bc46d6be3bfd23f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 05:25:16 +0000 Subject: [PATCH 31/58] Bump mypy from 0.931 to 0.940 Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.940. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.931...v0.940) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0c3622f72..c2f3eae8a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.6.0 -mypy==0.931 +mypy==0.940 pytest==7.1.0 pytest-asyncio==0.18.2 pytest-cov==3.0.0 From bea38a2e7c463cf3ed3670ca1c4923f68185ecfd Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 13:42:52 +0800 Subject: [PATCH 32/58] remove filled date logic --- freqtrade/data/btanalysis.py | 3 +-- freqtrade/optimize/backtesting.py | 12 ++++++------ freqtrade/persistence/models.py | 10 ---------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f0e0ccfd8..4df8b2838 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -19,7 +19,7 @@ from freqtrade.persistence import LocalTrade, Trade, init_db logger = logging.getLogger(__name__) # Newest format -BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'buy_filled_date', 'close_date', +BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'open_rate', 'close_rate', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', @@ -316,7 +316,6 @@ def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: if len(df) > 0: df.loc[:, 'close_date'] = pd.to_datetime(df['close_date'], utc=True) df.loc[:, 'open_date'] = pd.to_datetime(df['open_date'], utc=True) - df.loc[:, 'buy_filled_date'] = pd.to_datetime(df['buy_filled_date'], utc=True) df.loc[:, 'close_rate'] = df['close_rate'].astype('float64') return df diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff6db1b08..00dfca7d8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -356,7 +356,7 @@ class Backtesting: trade_dur: int) -> float: leverage = trade.leverage or 1.0 is_short = trade.is_short or False - filled_dur = int((trade.close_date_utc - trade.buy_filled_date_utc).total_seconds() // 60) + """ Get close rate for backtesting result """ @@ -378,7 +378,7 @@ class Backtesting: # 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 or filled_dur == 0): + 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 @@ -425,7 +425,7 @@ class Backtesting: if is_short: close_rate = (trade.open_rate * (1 - trade.fee_open) - trade.open_rate * roi / leverage) / (trade.fee_close + 1) - if (trade_dur > 0 and filled_dur > 0 and trade_dur == roi_entry + 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. @@ -435,7 +435,7 @@ class Backtesting: close_rate = - (trade.open_rate * roi / leverage + trade.open_rate * (1 + trade.fee_open)) / (trade.fee_close - 1) - if (trade_dur > 0 and filled_dur > 0 and trade_dur == roi_entry + 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. @@ -444,7 +444,7 @@ class Backtesting: if is_short: if ( - (trade_dur == 0 or filled_dur == 0) + 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 @@ -457,7 +457,7 @@ class Backtesting: raise ValueError("Opening candle ROI on red candles.") else: if ( - (trade_dur == 0 or filled_dur == 0) + 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 diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f5e63159b..b80d75dc0 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -302,7 +302,6 @@ class LocalTrade(): amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime - buy_filled_date: Optional[datetime] = None close_date: Optional[datetime] = None open_order_id: Optional[str] = None # absolute value of the stop loss @@ -367,10 +366,6 @@ class LocalTrade(): else: return self.amount - @property - def buy_filled_date_utc(self): - return self.buy_filled_date.replace(tzinfo=timezone.utc) - @property def open_date_utc(self): return self.open_date.replace(tzinfo=timezone.utc) @@ -449,11 +444,6 @@ class LocalTrade(): 'open_rate_requested': self.open_rate_requested, 'open_trade_value': round(self.open_trade_value, 8), - 'buy_filled_date': (self.buy_filled_date.strftime(DATETIME_PRINT_FORMAT) - if self.buy_filled_date else None), - 'buy_filled_timestamp': int(self.buy_filled_date.replace( - tzinfo=timezone.utc).timestamp() * 1000) if self.buy_filled_date else None, - 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), 'close_timestamp': int(self.close_date.replace( From 26a74220fdcf91a947b4afe19092fc3fc0416563 Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 13:43:42 +0800 Subject: [PATCH 33/58] remove buy filled logic --- freqtrade/optimize/backtesting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 00dfca7d8..cb54a6f4c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -960,7 +960,6 @@ class Backtesting: if order and self._get_order_filled(order.price, row): order.close_bt_order(current_time) trade.open_order_id = None - trade.buy_filled_date = current_time LocalTrade.add_bt_trade(trade) self.wallets.update() From 1d4eeacc6df58fde771c60ef00ac444d8be390de Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 17:55:42 +0800 Subject: [PATCH 34/58] fix test_backtest__enter_trade_futures row data error --- tests/optimize/test_backtesting.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 0d15c23e8..50c383346 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -581,14 +581,18 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: backtesting._set_strategy(backtesting.strategylist[0]) 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 + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 0.001, # Open + 0.0012, # High + 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) From 31182c4d80927bac6b7c1ca9d8ce4231e107dc6f Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 18:38:44 +0800 Subject: [PATCH 35/58] format --- freqtrade/optimize/backtesting.py | 195 ++++++++++++++++++------------ 1 file changed, 117 insertions(+), 78 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cb54a6f4c..8e30b2215 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -354,26 +354,25 @@ class Backtesting: def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, trade_dur: int) -> float: - leverage = trade.leverage or 1.0 - is_short = trade.is_short or False - """ Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI + is_short = trade.is_short or False + if is_short: + return self._get_short_close_rate(sell_row, trade, sell, trade_dur) + else: + return self._get_long_close_rate(sell_row, trade, sell, trade_dur) + + def _get_short_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, + trade_dur: int) -> float: + leverage = trade.leverage or 1.0 if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - if is_short: - if trade.stop_loss < sell_row[LOW_IDX]: - # our stoploss was already lower than candle high, - # possibly due to a cancelled trade exit. - # sell at open price. - return sell_row[OPEN_IDX] - else: - 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] + if trade.stop_loss < sell_row[LOW_IDX]: + # our stoploss was already lower 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 @@ -386,29 +385,96 @@ class Backtesting: and self.strategy.trailing_stop_positive ): # Worst case: price reaches stop_positive_offset and dives down. - if is_short: - stop_rate = (sell_row[OPEN_IDX] * + stop_rate = (sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive_offset) + abs(self.strategy.trailing_stop_positive))) - else: - 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. - if is_short: - stop_rate = sell_row[OPEN_IDX] * (1 + abs(trade.stop_loss_pct / leverage)) - assert stop_rate > sell_row[HIGH_IDX] - else: - stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct / leverage)) - assert stop_rate < sell_row[HIGH_IDX] + stop_rate = sell_row[OPEN_IDX] * (1 + abs(trade.stop_loss_pct / leverage)) + 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) + return min(sell_row[HIGH_IDX], stop_rate) + + # Set close_rate to stoploss + return trade.stop_loss + 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] + + # - (Expected abs profit - open_rate - open_fee) / (fee_close -1) + open_fee_rate = trade.open_rate * (1 - trade.fee_open) + roi_rate = trade.open_rate * roi / leverage + close_rate = (roi_rate - open_fee_rate) / (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 max(min(close_rate, sell_row[HIGH_IDX]), sell_row[LOW_IDX]) + + else: + # This should not be reached... + return sell_row[OPEN_IDX] + else: + return sell_row[OPEN_IDX] + + def _get_long_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, + trade_dur: int) -> float: + leverage = trade.leverage or 1.0 + 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: - return max(sell_row[LOW_IDX], stop_rate) + # Worst case: price ticks tiny bit above open and dives down. + stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct / leverage)) + 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 @@ -422,60 +488,33 @@ class Backtesting: return sell_row[OPEN_IDX] # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - if is_short: - close_rate = (trade.open_rate * - (1 - trade.fee_open) - trade.open_rate * roi / leverage) / (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] - else: - close_rate = - (trade.open_rate * roi / leverage + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) + close_rate = -(trade.open_rate * roi / leverage + 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 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 is_short: - 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.") - else: - 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.") + 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. - if is_short: - return max(min(close_rate, sell_row[HIGH_IDX]), sell_row[LOW_IDX]) - else: - return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... From 7dd57e8c04c90dfee2066ddbf311c3c548822f5b Mon Sep 17 00:00:00 2001 From: adriance Date: Mon, 14 Mar 2022 18:39:11 +0800 Subject: [PATCH 36/58] format --- tests/optimize/test_backtesting.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 50c383346..3c6e5df4b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -581,18 +581,18 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: backtesting._set_strategy(backtesting.strategylist[0]) pair = 'UNITTEST/USDT:USDT' row = [ - pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), - 0.001, # Open - 0.0012, # High - 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 + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 0.001, # Open + 0.0012, # High + 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) From 18030a30e7d4e081a893eb37ef9501e0a20648d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Mar 2022 19:21:58 +0100 Subject: [PATCH 37/58] Add exchange parameter to test-pairlist command This will allow for quick tests of the same pairlist config against multiple exchanges. --- docs/utils.md | 15 ++++++++++----- freqtrade/commands/arguments.py | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) 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 201ec09bf..28f7d7148 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -51,7 +51,7 @@ ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] 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"] From 7059892304c05c492b0d36de12fd610d2d67c83b Mon Sep 17 00:00:00 2001 From: adriance Date: Tue, 15 Mar 2022 12:04:02 +0800 Subject: [PATCH 38/58] Optimize the code. Fix stop_rate judgment error --- freqtrade/optimize/backtesting.py | 234 ++++++++++++------------------ 1 file changed, 94 insertions(+), 140 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8e30b2215..ba6aab71e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -358,168 +358,122 @@ class Backtesting: Get close rate for backtesting result """ # Special handling if high or low hit STOP_LOSS or ROI - is_short = trade.is_short or False - if is_short: - return self._get_short_close_rate(sell_row, trade, sell, trade_dur) - else: - return self._get_long_close_rate(sell_row, trade, sell, trade_dur) - - def _get_short_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, - trade_dur: int) -> float: - leverage = trade.leverage or 1.0 if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur) + elif sell.sell_type == (SellType.ROI): + return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur) + else: + return sell_row[OPEN_IDX] + + 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]: - # our stoploss was already lower than candle high, - # possibly due to a cancelled trade exit. - # sell at open price. + 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 - abs(self.strategy.trailing_stop_positive_offset) + - abs(self.strategy.trailing_stop_positive))) + # 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) + + 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: - # Worst case: price ticks tiny bit above open and dives down. - stop_rate = sell_row[OPEN_IDX] * (1 + abs(trade.stop_loss_pct / leverage)) - assert stop_rate > sell_row[HIGH_IDX] + 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". + # 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 - 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] + # Set close_rate to stoploss + return trade.stop_loss - # - (Expected abs profit - open_rate - open_fee) / (fee_close -1) - open_fee_rate = trade.open_rate * (1 - trade.fee_open) - roi_rate = trade.open_rate * roi / leverage - close_rate = (roi_rate - open_fee_rate) / (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] + 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] - if ( - trade_dur == 0 + # - (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 = -side_1 * (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), 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 max(min(close_rate, sell_row[HIGH_IDX]), sell_row[LOW_IDX]) - - else: - # This should not be reached... - return sell_row[OPEN_IDX] - else: - return sell_row[OPEN_IDX] - - def _get_long_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, - trade_dur: int) -> float: - leverage = trade.leverage or 1.0 - 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 / leverage)) - 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 - 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] - - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - close_rate = -(trade.open_rate * roi / leverage + 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 + ) + or + ( + not is_short # 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.") + ) + )): + # 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. + # 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. + if is_short: + return max(min(close_rate, sell_row[HIGH_IDX]), sell_row[LOW_IDX]) + else: return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) - else: - # This should not be reached... - return sell_row[OPEN_IDX] else: + # This should not be reached... return sell_row[OPEN_IDX] def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple From fd211166f067ab990ff0298e5616dfc673eb2e3c Mon Sep 17 00:00:00 2001 From: adriance Date: Tue, 15 Mar 2022 12:23:59 +0800 Subject: [PATCH 39/58] fixed side error --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ba6aab71e..c597c6748 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -392,8 +392,8 @@ class Backtesting: ): # 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) + - abs(self.strategy.trailing_stop_positive / leverage))) + (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 - From cbbdf00ddd745ef15c5887ce7d639561d399c4c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Mar 2022 06:39:07 +0100 Subject: [PATCH 40/58] Update comments in short backtest rates --- freqtrade/optimize/backtesting.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c597c6748..ea9850624 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -444,22 +444,22 @@ class Backtesting: if (trade_dur == 0 and ( ( is_short - # Red candle (for longs), TODO: green candle (for shorts) + # 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 below open_rate - and close_rate < sell_row[CLOSE_IDX] + 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 - # Red candle (for longs), TODO: green candle (for shorts) - and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle + # 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] + 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. + # 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.") From ceba4d6e9b10627fbec55e9a165e59d448ca4ac4 Mon Sep 17 00:00:00 2001 From: adriance Date: Tue, 15 Mar 2022 14:03:06 +0800 Subject: [PATCH 41/58] Remove meaningless code --- freqtrade/optimize/backtesting.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ea9850624..ac5d4e652 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -467,10 +467,7 @@ class Backtesting: # 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. - if is_short: - return max(min(close_rate, sell_row[HIGH_IDX]), sell_row[LOW_IDX]) - else: - return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... From 7c9d2dd20a9e040e7092e5ebc363a5ab188e5bd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 07:00:50 +0100 Subject: [PATCH 42/58] Fix a few more short bugs in backtesting --- freqtrade/optimize/backtesting.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ac5d4e652..cf499d4e2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -575,8 +575,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, @@ -756,8 +756,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, @@ -839,17 +839,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 @@ -945,8 +945,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 @@ -958,7 +958,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 From c47b5b9087682bde8ff1134142029b4fcedc0df5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 07:15:20 +0100 Subject: [PATCH 43/58] Update bt_detail column descriptions --- tests/optimize/test_backtest_detail.py | 85 +++++++++++++------------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index ea95a500f..a8260af49 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], @@ -368,7 +368,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 +384,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 +399,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], @@ -414,7 +414,7 @@ tc25 = BTContainer(data=[ # 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 +428,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 +444,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], @@ -460,7 +460,7 @@ tc28 = BTContainer(data=[ # 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 +474,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 +488,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 +503,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], @@ -537,7 +537,7 @@ tc33 = BTContainer(data=[ # 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 +549,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], @@ -566,7 +566,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 +581,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 +595,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,7 +610,7 @@ 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 @@ -622,12 +622,12 @@ tc39 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] ) -# Test 39: (copy of test25 with leverage) +# Test 40: (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 +tc40 = 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], @@ -681,6 +681,7 @@ TESTS = [ tc37, tc38, tc39, + tc40, # TODO-lev: Add tests for short here ] From 298797cbfd834be22c4e9dee779ea2baef45e7de Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 19:26:08 +0100 Subject: [PATCH 44/58] Add stoploss short test --- tests/optimize/__init__.py | 1 + tests/optimize/test_backtest_detail.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) 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 a8260af49..4d0a3f2cd 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -534,6 +534,27 @@ 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=[ @@ -675,6 +696,7 @@ TESTS = [ tc31, tc32, tc33, + tc33s, tc34, tc35, tc36, @@ -709,6 +731,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) + backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) backtesting.required_startup = 0 if data.leverage > 1.0: @@ -740,8 +763,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 From a89c1da19f17da81f61716b8448429737f7c7141 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 19:50:25 +0100 Subject: [PATCH 45/58] Fix 2 bugs in ROI calculation --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/strategy/interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cf499d4e2..267827a2c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -429,7 +429,7 @@ class Backtesting: # - (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 = -side_1 * (roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1) + 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: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 975e9d41f..179db1313 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. From c0781a98e8a0a95605cf014327bdafa85ed185fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 19:51:12 +0100 Subject: [PATCH 46/58] Add ROI test --- tests/optimize/test_backtest_detail.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 4d0a3f2cd..949b8d176 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -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 @@ -685,6 +702,7 @@ TESTS = [ tc20, tc21, tc22, + tc22s, tc23, tc24, tc25, From c934f939e3313bbd7e2d50bf66369bd3ed513f34 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 19:54:29 +0100 Subject: [PATCH 47/58] Update a few more short tests --- tests/optimize/test_backtest_detail.py | 57 +++++++++++++++++--------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 949b8d176..a2464b715 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -427,6 +427,39 @@ 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 @@ -660,22 +693,7 @@ tc39 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] ) -# Test 40: (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 -tc40 = 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)] -) + TESTS = [ @@ -706,6 +724,8 @@ TESTS = [ tc23, tc24, tc25, + tc25l, + tc25s, tc26, tc27, tc28, @@ -721,7 +741,6 @@ TESTS = [ tc37, tc38, tc39, - tc40, # TODO-lev: Add tests for short here ] @@ -749,12 +768,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: From d6309449cf1f26ad382b4c58e6baa0090b8377b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 20:08:36 +0100 Subject: [PATCH 48/58] Fix short bug where close_rate is wrongly adjusted --- freqtrade/optimize/backtesting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 267827a2c..6c5d098dc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -542,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'] From 295668d06cb089c1f8ba2b1e0e65d626f790b92c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 20:09:04 +0100 Subject: [PATCH 49/58] Add a few testcases --- tests/optimize/test_backtest_detail.py | 31 ++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a2464b715..8b36f8a90 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -684,7 +684,7 @@ tc39 = BTContainer(data=[ # 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, @@ -693,7 +693,33 @@ tc39 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.FORCE_SELL, 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=[] +) TESTS = [ @@ -741,7 +767,8 @@ TESTS = [ tc37, tc38, tc39, - # TODO-lev: Add tests for short here + tc39a, + tc40, ] From 2fab3de4d7e2c070d30135d079690e461287f4bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 20:11:32 +0100 Subject: [PATCH 50/58] More backtest-detail tests --- tests/optimize/test_backtest_detail.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 8b36f8a90..cc290e5a0 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -629,7 +629,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 @@ -763,6 +777,7 @@ TESTS = [ tc33s, tc34, tc35, + tc35s, tc36, tc37, tc38, From 20f02eb773e2c47d48163105a28cb3952b11482b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 21:28:24 +0100 Subject: [PATCH 51/58] Add test for stoploss case --- tests/optimize/test_backtest_detail.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index cc290e5a0..ad9ae7786 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -506,6 +506,23 @@ 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) @@ -769,6 +786,7 @@ TESTS = [ tc26, tc27, tc28, + tc28s, tc29, tc30, tc31, From 9b2ec5e653776cde53364373fcf904fc9a1ddbff Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Mar 2022 21:31:34 +0100 Subject: [PATCH 52/58] Fix missleading variable naming --- freqtrade/strategy/interface.py | 12 ++++++------ tests/optimize/test_backtest_detail.py | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 179db1313..3fddc98df 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -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/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index ad9ae7786..7ede1adc3 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -520,7 +520,9 @@ tc28s = BTContainer(data=[ 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)] + 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 From 6024fa482e1b09bae8e85b3afc9fc58e483c1512 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Mar 2022 07:41:08 +0100 Subject: [PATCH 53/58] Use brackets to break IF lines --- freqtrade/edge/edge_positioning.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 1950f0d08..08f43598d 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -219,9 +219,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: @@ -246,8 +248,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, From 96bf82dbc64b824617f75ca8556f2f2d0e4d9446 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Mar 2022 17:06:10 +0100 Subject: [PATCH 54/58] Remove gateio broker program --- freqtrade/exchange/gateio.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index bfe996e86..d0fd787b7 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -26,8 +26,6 @@ class Gateio(Exchange): "stoploss_on_exchange": True, } - _headers = {'X-Gate-Channel-Id': 'freqtrade'} - def validate_ordertypes(self, order_types: Dict) -> None: super().validate_ordertypes(order_types) From f55db8e262f2f64758682a9a5b96abbab6051d0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Mar 2022 20:21:10 +0100 Subject: [PATCH 55/58] Spreadfilter should fail to start if fetchTickers is not supported --- freqtrade/plugins/pairlist/SpreadFilter.py | 7 +++++++ tests/plugins/test_pairlist.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) 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/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 52158a889..346bf1792 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -782,6 +782,18 @@ 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 From 73fc344eb1d666cbf7f435abc8b5505e9f952798 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 06:38:54 +0100 Subject: [PATCH 56/58] Improve wording in docs --- docs/bot-basics.md | 2 +- tests/plugins/test_pairlist.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 8c6303063..4b5ba3a5b 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/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 346bf1792..08ba892fe 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -794,6 +794,7 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N 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 From f37038fb7dc433a86a2e6bb38d78b3662ef0170b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 10:35:00 +0100 Subject: [PATCH 57/58] Fix gateio stoploss_adjust header --- freqtrade/exchange/gateio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index efad60c39..d51d5597f 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -56,7 +56,7 @@ class Gateio(Exchange): params={'stop': True} ) - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + 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. From 0c63c0bbb39de46997d22044c3cd44669a054b4a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 10:41:38 +0100 Subject: [PATCH 58/58] Update Gateio stoploss adjust --- freqtrade/exchange/gateio.py | 3 ++- tests/exchange/test_gateio.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index d51d5597f..50ff0c872 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -61,4 +61,5 @@ class Gateio(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return stop_loss > float(order['stopPrice']) + return ((side == "sell" and stop_loss > float(order['stopPrice'])) or + (side == "buy" and stop_loss < float(order['stopPrice']))) diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 5680604e2..faac0c8af 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -57,11 +57,15 @@ def test_cancel_stoploss_order_gateio(default_conf, mocker): assert cancel_order_mock.call_args_list[0][1]['params'] == {'stop': True} -def test_stoploss_adjust_gateio(mocker, default_conf): +@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(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(sl1, order, side) + assert not exchange.stoploss_adjust(sl2, order, side)