From 0fd68aee51ad010a7155aba0ff0caee8f62d57ef Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Apr 2021 10:49:09 +0300 Subject: [PATCH 01/19] Add IStrategy.custom_sell method which allows per-trade sell signal evaluation. --- freqtrade/strategy/interface.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 54c7f2353..b0710e833 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -285,6 +285,27 @@ class IStrategy(ABC, HyperStrategyMixin): """ return self.stoploss + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> bool: + """ + Custom sell signal logic indicating that specified position should be sold. Returning True + from this method is equal to setting sell signal on a candle at specified time. + This method is not called when sell signal is set. + + This method should be overridden to create sell signals that depend on trade parameters. For + example you could implement a stoploss relative to candle when trade was opened, or a custom + 1:2 risk-reward ROI. + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: Whether trade should exit now. + """ + return False + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -535,9 +556,12 @@ class IStrategy(ABC, HyperStrategyMixin): and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False - else: - sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True) + elif ask_strategy.get('use_sell_signal', True): + sell = sell or self.custom_sell(trade.pair, trade, date, current_rate, current_profit) + sell_signal = sell and not buy # TODO: return here if sell-signal should be favored over ROI + else: + sell_signal = False # Start evaluations # Sequence: From 1292e08fe47ab5ae8455c24f70612708988bd46c Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Apr 2021 11:26:03 +0300 Subject: [PATCH 02/19] Use strategy_safe_wrapper() when calling custom_sell(). --- freqtrade/strategy/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b0710e833..1a007da15 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -557,7 +557,8 @@ class IStrategy(ABC, HyperStrategyMixin): # sell_profit_only and profit doesn't reach the offset - ignore sell signal sell_signal = False elif ask_strategy.get('use_sell_signal', True): - sell = sell or self.custom_sell(trade.pair, trade, date, current_rate, current_profit) + sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( + trade.pair, trade, date, current_rate, current_profit) sell_signal = sell and not buy # TODO: return here if sell-signal should be favored over ROI else: From a77337e4241245a8cb77f2c5cf47fe71f3bd5104 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 20 Apr 2021 10:37:45 +0300 Subject: [PATCH 03/19] Document IStrategy.custom_sell. --- docs/strategy-advanced.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 96c927965..758d08fca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -69,6 +69,20 @@ class AwesomeStrategy(IStrategy): See `custom_stoploss` examples below on how to access the saved dataframe columns +## Custom sell signal + +It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. + +An example of how we can sell trades that were open longer than 1 day: + +``` python +class AwesomeStrategy(IStrategy): + def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> bool: + time_delta = datetime.datetime.utcnow() - trade.open_date_utc + return time_delta.days >= 1 +``` + ## Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. From 1aad128d85dd2e9962631a1ffd8d5a3fd0dacbd1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 20 Apr 2021 11:17:00 +0300 Subject: [PATCH 04/19] Support returning a string from custom_sell() and have it recorded as custom sell reason. --- freqtrade/freqtradebot.py | 11 ++++--- freqtrade/optimize/backtesting.py | 2 +- freqtrade/strategy/interface.py | 50 ++++++++++++++++++++----------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ceb822472..9cce8c105 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -961,7 +961,7 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell.sell_type) + self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) return True return False @@ -1150,12 +1150,15 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, + custom_reason: Optional[str] = None) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order - :param sellreason: Reason the sell was triggered + :param sell_reason: Reason the sell was triggered + :param custom_reason: A custom sell reason. Provided only if + sell_reason == SellType.CUSTOM_SELL, :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' @@ -1213,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = sell_reason.value + trade.sell_reason = custom_reason or sell_reason.value # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fb9826a23..57ac70cc1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_type.value + trade.sell_reason = sell.sell_reason or sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1a007da15..74e92f389 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,7 +7,7 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Dict, List, NamedTuple, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) +CUSTOM_SELL_MAX_LENGTH = 64 class SignalType(Enum): @@ -45,6 +46,7 @@ class SellType(Enum): SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" EMERGENCY_SELL = "emergency_sell" + CUSTOM_SELL = "custom_sell" NONE = "" def __str__(self): @@ -58,6 +60,7 @@ class SellCheckTuple(NamedTuple): """ sell_flag: bool sell_type: SellType + sell_reason: Optional[str] = None class IStrategy(ABC, HyperStrategyMixin): @@ -286,25 +289,28 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> bool: + current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ - Custom sell signal logic indicating that specified position should be sold. Returning True - from this method is equal to setting sell signal on a candle at specified time. - This method is not called when sell signal is set. + Custom sell signal logic indicating that specified position should be sold. Returning a + string or True from this method is equal to setting sell signal on a candle at specified + time. This method is not called when sell signal is set. This method should be overridden to create sell signals that depend on trade parameters. For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. + Custom sell reason max length is 64. Exceeding this limit will raise OperationalException. + :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: Whether trade should exit now. + :return: To execute sell, return a string with custom sell reason or True. Otherwise return + None or False. """ - return False + return None def informative_pairs(self) -> ListPairsWithTimeframes: """ @@ -552,17 +558,27 @@ class IStrategy(ABC, HyperStrategyMixin): and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) + sell_signal = SellType.NONE + custom_reason = None if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal - sell_signal = False - elif ask_strategy.get('use_sell_signal', True): - sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit) - sell_signal = sell and not buy + pass + elif ask_strategy.get('use_sell_signal', True) and not buy: + if sell: + sell_signal = SellType.SELL_SIGNAL + else: + custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( + trade.pair, trade, date, current_rate, current_profit) + if custom_reason: + sell_signal = SellType.CUSTOM_SELL + if isinstance(custom_reason, bool): + custom_reason = None + elif isinstance(custom_reason, str): + if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: + raise OperationalException('Custom sell reason returned ' + 'from custom_sell is too long.') # TODO: return here if sell-signal should be favored over ROI - else: - sell_signal = False # Start evaluations # Sequence: @@ -574,10 +590,10 @@ class IStrategy(ABC, HyperStrategyMixin): f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - if sell_signal: + if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " - f"sell_type=SellType.SELL_SIGNAL") - return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) + f"sell_type={sell_signal}, custom_reason={custom_reason}") + return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: From a90e7956958c6bcb63101f65c91d442ceaf10c83 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 21 Apr 2021 09:37:16 +0300 Subject: [PATCH 05/19] Warn and trim custom sell reason if it is too long. --- freqtrade/strategy/interface.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 74e92f389..435ac3ed3 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -572,12 +572,14 @@ class IStrategy(ABC, HyperStrategyMixin): trade.pair, trade, date, current_rate, current_profit) if custom_reason: sell_signal = SellType.CUSTOM_SELL - if isinstance(custom_reason, bool): - custom_reason = None - elif isinstance(custom_reason, str): + if isinstance(custom_reason, str): if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: - raise OperationalException('Custom sell reason returned ' - 'from custom_sell is too long.') + logger.warning(f'Custom sell reason returned from custom_sell is too ' + f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} ' + f'characters.') + custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH] + else: + custom_reason = None # TODO: return here if sell-signal should be favored over ROI # Start evaluations From bfad4e82adf1c6413835a8e1e27b8d49f5ae4c5d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 21 Apr 2021 10:11:54 +0300 Subject: [PATCH 06/19] Make execute_sell() use SellCheckTuple for sell reason. --- freqtrade/freqtradebot.py | 20 ++++++++++---------- freqtrade/rpc/rpc.py | 5 +++-- freqtrade/strategy/interface.py | 16 +++++++++++----- tests/test_freqtradebot.py | 22 +++++++++++++--------- 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9cce8c105..7888e64bc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -28,7 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State -from freqtrade.strategy.interface import IStrategy, SellType +from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -850,7 +850,8 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') - self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) + self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( + sell_flag=True, sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None @@ -961,7 +962,7 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) + self.execute_sell(trade, sell_rate, should_sell) return True return False @@ -1150,8 +1151,7 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, - custom_reason: Optional[str] = None) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance @@ -1162,7 +1162,7 @@ class FreqtradeBot(LoggingMixin): :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' - if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1179,10 +1179,10 @@ class FreqtradeBot(LoggingMixin): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") order_type = self.strategy.order_types[sell_type] - if sell_reason == SellType.EMERGENCY_SELL: + if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason == SellType.FORCE_SELL: + if sell_reason.sell_type == SellType.FORCE_SELL: # Force sells (default to the sell_type defined in the strategy, # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) @@ -1193,7 +1193,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, - sell_reason=sell_reason.value): + sell_reason=sell_reason.sell_type.value): logger.info(f"User requested abortion of selling {trade.pair}") return False @@ -1216,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = custom_reason or sell_reason.value + trade.sell_reason = sell_reason.sell_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4a8907582..957f31b63 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -24,7 +24,7 @@ from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.strategy.interface import SellType +from freqtrade.strategy.interface import SellCheckTuple, SellType logger = logging.getLogger(__name__) @@ -554,7 +554,8 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.FORCE_SELL) + self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 435ac3ed3..c73574729 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,7 +7,7 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Dict, List, NamedTuple, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -54,13 +54,18 @@ class SellType(Enum): return self.value -class SellCheckTuple(NamedTuple): +class SellCheckTuple(object): """ NamedTuple for Sell type + reason """ - sell_flag: bool + sell_flag: bool # TODO: Remove? sell_type: SellType - sell_reason: Optional[str] = None + sell_reason: Optional[str] + + def __init__(self, sell_flag: bool, sell_type: SellType, sell_reason: Optional[str] = None): + self.sell_flag = sell_flag + self.sell_type = sell_type + self.sell_reason = sell_reason or sell_type.value class IStrategy(ABC, HyperStrategyMixin): @@ -594,7 +599,8 @@ class IStrategy(ABC, HyperStrategyMixin): if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " - f"sell_type={sell_signal}, custom_reason={custom_reason}") + f"sell_type=SellType.{sell_signal.name}" + + (f", custom_reason={custom_reason}" if custom_reason else "")) return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ad000515e..aecff95ee 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2606,14 +2606,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N fetch_ticker=ticker_sell_up ) # Prevented sell ... - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 # Repatch with true freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2665,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2722,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe trade.stop_loss = 0.00001099 * 0.99 freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2774,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade.stoploss_order_id = "abcd" freqtrade.execute_sell(trade=trade, limit=1234, - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert sellmock.call_count == 1 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2824,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.SELL_SIGNAL) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() assert trade @@ -2929,7 +2931,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee, ) freqtrade.config['order_types']['sell'] = 'market' - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert not trade.is_open assert trade.close_profit == 0.0620716 @@ -2983,8 +2986,9 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.ROI) + sell_reason=sell_reason) assert mock_insuf.call_count == 1 @@ -3226,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) From 961b38636fa2ae7c8f4c9a97e5b90c83ff97c232 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 09:21:19 +0300 Subject: [PATCH 07/19] Remove explicit sell_flag parameter from SellCheckTuple. --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/strategy/interface.py | 26 ++++++++++++++------------ tests/test_freqtradebot.py | 24 ++++++++++++------------ tests/test_integration.py | 14 +++++++------- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7888e64bc..08c69bb53 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -851,7 +851,7 @@ class FreqtradeBot(LoggingMixin): logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( - sell_flag=True, sell_type=SellType.EMERGENCY_SELL)) + sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 957f31b63..fd97ad7d4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -554,7 +554,7 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.FORCE_SELL) + sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c73574729..bc8b3e59f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -58,15 +58,17 @@ class SellCheckTuple(object): """ NamedTuple for Sell type + reason """ - sell_flag: bool # TODO: Remove? sell_type: SellType sell_reason: Optional[str] - def __init__(self, sell_flag: bool, sell_type: SellType, sell_reason: Optional[str] = None): - self.sell_flag = sell_flag + def __init__(self, sell_type: SellType, sell_reason: Optional[str] = None): self.sell_type = sell_type self.sell_reason = sell_reason or sell_type.value + @property + def sell_flag(self): + return self.sell_type != SellType.NONE + class IStrategy(ABC, HyperStrategyMixin): """ @@ -593,25 +595,25 @@ class IStrategy(ABC, HyperStrategyMixin): # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: - logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " + logger.debug(f"{trade.pair} - Required profit reached. " f"sell_type=SellType.ROI") - return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: - logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " + logger.debug(f"{trade.pair} - Sell signal received. " f"sell_type=SellType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) - return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) + return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + logger.debug(f"{trade.pair} - Stoploss hit. " f"sell_type={stoplossflag.sell_type}") return stoplossflag # This one is noisy, commented out... - # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + # logger.debug(f"{trade.pair} - No sell signal.") + return SellCheckTuple(sell_type=SellType.NONE) def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, @@ -675,9 +677,9 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"{trade.pair} - Trailing stop saved " f"{trade.stop_loss - trade.initial_stop_loss:.6f}") - return SellCheckTuple(sell_flag=True, sell_type=sell_type) + return SellCheckTuple(sell_type=sell_type) - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + return SellCheckTuple(sell_type=SellType.NONE) def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index aecff95ee..7a2f6a1ed 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1973,7 +1973,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, # if ROI is reached we must sell patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has("ETH/BTC - Required profit reached. sell_flag=True, sell_type=SellType.ROI", + assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", caplog) @@ -2002,7 +2002,7 @@ def test_handle_trade_use_sell_signal( patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) - assert log_has("ETH/BTC - Sell signal received. sell_flag=True, sell_type=SellType.SELL_SIGNAL", + assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) @@ -2607,7 +2607,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N ) # Prevented sell ... freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -2615,7 +2615,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2667,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2724,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe trade.stop_loss = 0.00001099 * 0.99 freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2776,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade.stoploss_order_id = "abcd" freqtrade.execute_sell(trade=trade, limit=1234, - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert sellmock.call_count == 1 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2826,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() assert trade @@ -2932,7 +2932,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, freqtrade.config['order_types']['sell'] = 'market' freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) + sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert not trade.is_open assert trade.close_profit == 0.0620716 @@ -2986,7 +2986,7 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) - sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + sell_reason = SellCheckTuple(sell_type=SellType.ROI) assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=sell_reason) assert mock_insuf.call_count == 1 @@ -3081,7 +3081,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_flag=False, sell_type=SellType.NONE)) + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() @@ -3230,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) diff --git a/tests/test_integration.py b/tests/test_integration.py index be0dd1137..217910961 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -51,8 +51,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) # Sell 3rd trade (not called for the first trade) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)] + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.SELL_SIGNAL)] ) cancel_order_mock = MagicMock() mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) @@ -156,11 +156,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc _notify_sell=MagicMock(), ) should_sell_mock = MagicMock(side_effect=[ - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL), - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), - SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)] + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.SELL_SIGNAL), + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.NONE), + SellCheckTuple(sell_type=SellType.NONE)] ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) From 595b8735f80df633834a4d8266694cdcb52287b8 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:15:56 +0300 Subject: [PATCH 08/19] Add dataframe parameter to custom_stoploss() and custom_sell() methods. --- freqtrade/freqtradebot.py | 13 +++++++------ freqtrade/optimize/backtesting.py | 7 ++++--- freqtrade/strategy/interface.py | 21 ++++++++++++--------- tests/strategy/test_default_strategy.py | 3 ++- tests/strategy/test_interface.py | 4 ++-- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 08c69bb53..f8c757189 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache +from pandas import DataFrame from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -783,10 +784,10 @@ class FreqtradeBot(LoggingMixin): config_ask_strategy = self.config.get('ask_strategy', {}) + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) @@ -813,13 +814,13 @@ class FreqtradeBot(LoggingMixin): # resulting in outdated RPC messages self._sell_rate_cache[trade.pair] = sell_rate - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): return True else: logger.debug('checking sell') sell_rate = self.get_sell_rate(trade.pair, True) - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -950,13 +951,13 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, trade: Trade, sell_rate: float, + def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: """ Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.now(timezone.utc), buy, sell, + dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 57ac70cc1..559938e9e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -247,9 +247,10 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade, + sell_row: Tuple) -> Optional[LocalTrade]: - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore + sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) @@ -396,7 +397,7 @@ class Backtesting: for trade in open_trades[pair]: # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(trade, row) + trade_entry = self._get_sell_trade_entry(processed[pair], trade, row) # Sell occured if trade_entry: # logger.debug(f"{pair} - Backtesting sell {trade}") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index bc8b3e59f..9a6901712 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -274,7 +274,7 @@ class IStrategy(ABC, HyperStrategyMixin): return True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: + current_profit: float, dataframe: DataFrame, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -296,7 +296,8 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> Optional[Union[str, bool]]: + current_profit: float, dataframe: DataFrame, + **kwargs) -> Optional[Union[str, bool]]: """ Custom sell signal logic indicating that specified position should be sold. Returning a string or True from this method is equal to setting sell signal on a candle at specified @@ -534,8 +535,8 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, - sell: bool, low: float = None, high: float = None, + def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime, + buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell @@ -551,8 +552,9 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=date, current_profit=current_profit, + stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate, + trade=trade, current_time=date, + current_profit=current_profit, force_stoploss=force_stoploss, high=high) # Set current rate to high for backtesting sell @@ -576,7 +578,7 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit) + trade.pair, trade, date, current_rate, current_profit, dataframe) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -615,7 +617,7 @@ class IStrategy(ABC, HyperStrategyMixin): # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE) - def stop_loss_reached(self, current_rate: float, trade: Trade, + def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ @@ -633,7 +635,8 @@ class IStrategy(ABC, HyperStrategyMixin): )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, - current_profit=current_profit) + current_profit=current_profit, + dataframe=dataframe) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index ec7b3c33d..a8862e9c9 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -41,4 +41,5 @@ def test_default_strategy(result, fee): rate=20000, time_in_force='gtc', sell_reason='roi') is True assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05) == strategy.stoploss + current_rate=20_000, current_profit=0.05, dataframe=None + ) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 347d35b19..d2a09e466 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, current_time=now, current_profit=profit, - force_stoploss=0, high=None) + force_stoploss=0, high=None, dataframe=None) assert isinstance(sl_flag, SellCheckTuple) assert sl_flag.sell_type == expected if expected == SellType.NONE: @@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, current_time=now, current_profit=profit2, - force_stoploss=0, high=None) + force_stoploss=0, high=None, dataframe=None) assert sl_flag.sell_type == expected2 if expected2 == SellType.NONE: assert sl_flag.sell_flag is False From 004550529ea498fc584668d8b292e2fcab7d37e1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:31:53 +0300 Subject: [PATCH 09/19] Document dataframe parameter in custom_stoploss(). --- docs/strategy-advanced.md | 83 ++++++++++++++------------------------- 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 758d08fca..7648498d3 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -42,33 +42,6 @@ class AwesomeStrategy(IStrategy): *** -### Storing custom information using DatetimeIndex from `dataframe` - -Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps. - -```python -import talib.abstract as ta -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # using "ATR" here as example - dataframe['atr'] = ta.ATR(dataframe) - if self.dp.runmode.value in ('backtest', 'hyperopt'): - # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date') - return dataframe -``` - -!!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. - -!!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - -See `custom_stoploss` examples below on how to access the saved dataframe columns - ## Custom sell signal It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. @@ -107,7 +80,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: Dataframe, + **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -157,7 +131,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. if current_time - timedelta(minutes=120) > trade.open_date_utc: @@ -183,7 +158,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -209,7 +185,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: if current_profit < 0.04: return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss @@ -249,7 +226,8 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: @@ -266,14 +244,20 @@ class AwesomeStrategy(IStrategy): Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" -See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info` - !!! Warning - only use .iat[-1] in live mode, not in backtesting/hyperopt - otherwise you will look into the future + Only use `dataframe` values up until and including `current_time` value. Reading past + `current_time` you will look into the future, which will produce incorrect backtesting results + and throw an exception in dry/live runs. see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. +!!! Note + DataFrame is indexed by candle date. During dry/live runs `current_time` and + `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw + an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous + candle before using it as a `dataframe` index. + ``` python +from freqtrade.exchange import timeframe_to_prev_date from freqtrade.persistence import Trade from freqtrade.state import RunMode @@ -284,28 +268,19 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: result = 1 if self.custom_info and pair in self.custom_info and trade: - # using current_time directly (like below) will only work in backtesting. - # so check "runmode" to make sure that it's only used in backtesting/hyperopt - if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'): - relative_sl = self.custom_info[pair].loc[current_time]['atr'] - # in live / dry-run, it'll be really the current time - else: - # but we can just use the last entry from an already analyzed dataframe instead - dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, - timeframe=self.timeframe) - # WARNING - # only use .iat[-1] in live mode, not in backtesting/hyperopt - # otherwise you will look into the future - # see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies - relative_sl = dataframe['atr'].iat[-1] - - if (relative_sl is not None): + # Using current_time directly would only work in backtesting. Live/dry runs need time to + # be rounded to previous candle to be used as dataframe index. Rounding must also be + # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. + current_time = timeframe_to_prev_date(self.timeframe, current_time) + current_row = dataframe.loc[current_time] + if 'atr' in current_row: # new stoploss relative to current_rate - new_stoploss = (current_rate-relative_sl)/current_rate + new_stoploss = (current_rate - current_row['atr']) / current_rate # turn into relative negative offset required by `custom_stoploss` return implementation result = new_stoploss - 1 From e58fe7a8cbbb978aa7c7a8ffcec4ebe0e3660414 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:45:34 +0300 Subject: [PATCH 10/19] Update custom_sell documentation. --- docs/strategy-advanced.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7648498d3..59c4a6a35 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -46,16 +46,31 @@ class AwesomeStrategy(IStrategy): It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. -An example of how we can sell trades that were open longer than 1 day: +An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: ``` python class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> bool: - time_delta = datetime.datetime.utcnow() - trade.open_date_utc - return time_delta.days >= 1 + current_profit: float, dataframe: Dataframe, **kwargs) -> bool: + trade_row = dataframe.loc[timeframe_to_prev_date(trade.open_date_utc)] + + # Sell when price falls below value in stoploss column of taken buy signal. + if 'stop_loss' in trade_row: + if current_rate <= trade_row['stop_loss'] < trade.open_rate: + return 'stop_loss' + + # Sell when price reaches value in take_profit column of taken buy signal. + if 'take_profit' in trade_row: + if trade.open_rate < trade_row['take_profit'] <= current_rate: + return 'take_profit' + + # Sell any positions at a loss if they are helpd for more than two days. + if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: + return 'unclog' ``` +See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. + ## Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. From 98f6fce2ecf6decd360d5706d1c6ffca38643b5d Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 09:48:24 +0300 Subject: [PATCH 11/19] Use correct sell reason in case of custom sell reason. --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f8c757189..6dcd920c7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1194,7 +1194,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, - sell_reason=sell_reason.sell_type.value): + sell_reason=sell_reason.sell_reason): logger.info(f"User requested abortion of selling {trade.pair}") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 559938e9e..88957bafb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -256,7 +256,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_reason or sell.sell_type.value + trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -266,7 +266,7 @@ class Backtesting: pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, time_in_force=time_in_force, - sell_reason=sell.sell_type.value): + sell_reason=sell.sell_reason): return None trade.close(closerate, show_msg=False) From 9c21c75cf5ef34cdd59d340e8e0b1e3f7b504ba0 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 25 Apr 2021 13:12:33 +0300 Subject: [PATCH 12/19] Fix inaccuracy in docs. --- docs/strategy-advanced.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 59c4a6a35..f869a3c3a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -52,8 +52,8 @@ An example of how we can set stop-loss and take-profit targets in the dataframe class AwesomeStrategy(IStrategy): def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, dataframe: Dataframe, **kwargs) -> bool: - trade_row = dataframe.loc[timeframe_to_prev_date(trade.open_date_utc)] - + trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze() + # Sell when price falls below value in stoploss column of taken buy signal. if 'stop_loss' in trade_row: if current_rate <= trade_row['stop_loss'] < trade.open_rate: @@ -292,7 +292,7 @@ class AwesomeStrategy(IStrategy): # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. current_time = timeframe_to_prev_date(self.timeframe, current_time) - current_row = dataframe.loc[current_time] + current_row = dataframe.loc[dataframe['date'] == current_time].squeeze() if 'atr' in current_row: # new stoploss relative to current_rate new_stoploss = (current_rate - current_row['atr']) / current_rate From 31a2285eacaa7ce011a98548809fbe314f4f3c79 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 26 Apr 2021 10:42:24 +0300 Subject: [PATCH 13/19] Fix mypy complaints. --- freqtrade/strategy/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9a6901712..503a587ea 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -59,9 +59,9 @@ class SellCheckTuple(object): NamedTuple for Sell type + reason """ sell_type: SellType - sell_reason: Optional[str] + sell_reason: str = '' - def __init__(self, sell_type: SellType, sell_reason: Optional[str] = None): + def __init__(self, sell_type: SellType, sell_reason: str = ''): self.sell_type = sell_type self.sell_reason = sell_reason or sell_type.value @@ -568,7 +568,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_time=date)) sell_signal = SellType.NONE - custom_reason = None + custom_reason = '' if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal From dbf33271b5853d32d4bd952508ce33be2574948c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 19:27:22 +0200 Subject: [PATCH 14/19] Small doc changes --- docs/strategy-advanced.md | 24 +++++++++++-------- docs/strategy-customization.md | 5 ++-- freqtrade/freqtradebot.py | 2 -- freqtrade/strategy/interface.py | 1 + .../subtemplates/strategy_methods_advanced.j2 | 6 +++-- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f869a3c3a..f702b1448 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -49,10 +49,13 @@ It is possible to define custom sell signals. This is very useful when we need t An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: ``` python +from freqtrade.strategy import IStrategy, timeframe_to_prev_date + class AwesomeStrategy(IStrategy): - def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, dataframe: Dataframe, **kwargs) -> bool: - trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze() + def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, dataframe: DataFrame, **kwargs): + trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc) + trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() # Sell when price falls below value in stoploss column of taken buy signal. if 'stop_loss' in trade_row: @@ -64,12 +67,12 @@ class AwesomeStrategy(IStrategy): if trade.open_rate < trade_row['take_profit'] <= current_rate: return 'take_profit' - # Sell any positions at a loss if they are helpd for more than two days. + # Sell any positions at a loss if they are held for more than two days. if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: return 'unclog' ``` -See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. +See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. ## Custom stoploss @@ -95,7 +98,7 @@ class AwesomeStrategy(IStrategy): use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, dataframe: Dataframe, + current_rate: float, current_profit: float, dataframe: DataFrame, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). @@ -113,7 +116,7 @@ class AwesomeStrategy(IStrategy): :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New stoploss value, relative to the currentrate + :return float: New stoploss value, relative to the current rate """ return -0.04 ``` @@ -228,7 +231,6 @@ Instead of continuously trailing behind the current price, this example sets fix * Once profit is > 25% - set stoploss to 15% above open price. * Once profit is > 40% - set stoploss to 25% above open price. - ``` python from datetime import datetime from freqtrade.persistence import Trade @@ -255,6 +257,7 @@ class AwesomeStrategy(IStrategy): # return maximum stoploss value, keeping current stoploss price unchanged return 1 ``` + #### Custom stoploss using an indicator from dataframe example Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR" @@ -266,7 +269,7 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. !!! Note - DataFrame is indexed by candle date. During dry/live runs `current_time` and + `dataframe` is indexed by candle date. During dry/live runs `current_time` and `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous candle before using it as a `dataframe` index. @@ -286,8 +289,9 @@ class AwesomeStrategy(IStrategy): current_rate: float, current_profit: float, dataframe: DataFrame, **kwargs) -> float: + # Default return value result = 1 - if self.custom_info and pair in self.custom_info and trade: + if trade: # Using current_time directly would only work in backtesting. Live/dry runs need time to # be rounded to previous candle to be used as dataframe index. Rounding must also be # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 256b28990..59bfbde48 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -631,9 +631,10 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: - # once the profit has risin above 10%, keep the stoploss at 7% above the open price + # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: return stoploss_from_open(0.07, current_profit) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6dcd920c7..c2b15d23f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1158,8 +1158,6 @@ class FreqtradeBot(LoggingMixin): :param trade: Trade instance :param limit: limit rate for the sell order :param sell_reason: Reason the sell was triggered - :param custom_reason: A custom sell reason. Provided only if - sell_reason == SellType.CUSTOM_SELL, :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 503a587ea..5c3264c35 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -290,6 +290,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 53ededa19..c69b52cad 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -14,8 +14,9 @@ def bot_loop_start(self, **kwargs) -> None: use_custom_stoploss = True -def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs) -> float: +def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', + current_rate: float, current_profit: float, dataframe: DataFrame, + **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -31,6 +32,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', c :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: New stoploss value, relative to the currentrate """ From 2061162d794fa53eb847dfbf2811282e5825146a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 19:54:47 +0200 Subject: [PATCH 15/19] Convert trade-opendate to python datetime --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 88957bafb..b03a459ea 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,7 @@ class Backtesting: low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade.close_date = sell_row[DATE_IDX] + trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -294,7 +294,7 @@ class Backtesting: trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], - open_date=row[DATE_IDX], + open_date=row[DATE_IDX].to_pydatetime(), stake_amount=stake_amount, amount=round(stake_amount / row[OPEN_IDX], 8), fee_open=self.fee, @@ -316,7 +316,7 @@ class Backtesting: for trade in open_trades[pair]: sell_row = data[pair][-1] - trade.close_date = sell_row[DATE_IDX] + trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.sell_reason = SellType.FORCE_SELL.value trade.close(sell_row[OPEN_IDX], show_msg=False) LocalTrade.close_bt_trade(trade) From 55faa6a84a6ed7a4fa65781987eb9f05d8de4152 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 20:18:03 +0200 Subject: [PATCH 16/19] safe_wrapper should use kwargs to call methods --- freqtrade/strategy/interface.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5c3264c35..645e70e8a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -579,7 +579,8 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit, dataframe) + pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, + current_profit=current_profit, dataframe=dataframe) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): @@ -598,8 +599,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: - logger.debug(f"{trade.pair} - Required profit reached. " - f"sell_type=SellType.ROI") + logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: @@ -610,8 +610,7 @@ class IStrategy(ABC, HyperStrategyMixin): if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. " - f"sell_type={stoplossflag.sell_type}") + logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}") return stoplossflag # This one is noisy, commented out... From cc916ab2e90458a54cdc8d4f247518f904e8fdd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Apr 2021 20:26:14 +0200 Subject: [PATCH 17/19] Add test for custom_sell --- tests/strategy/test_interface.py | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index d2a09e466..bd81bc80c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -382,6 +382,50 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.custom_stoploss = original_stopvalue +def test_custom_sell(default_conf, fee, caplog) -> None: + + default_conf.update({'strategy': 'DefaultStrategy'}) + + strategy = StrategyResolver.load_strategy(default_conf) + trade = Trade( + pair='ETH/BTC', + stake_amount=0.01, + amount=1, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + ) + + now = arrow.utcnow().datetime + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + + assert res.sell_flag is False + assert res.sell_type == SellType.NONE + + strategy.custom_sell = MagicMock(return_value=True) + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_flag is True + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_reason == 'custom_sell' + + strategy.custom_sell = MagicMock(return_value='hello world') + + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_flag is True + assert res.sell_reason == 'hello world' + + caplog.clear() + strategy.custom_sell = MagicMock(return_value='h' * 100) + res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0) + assert res.sell_type == SellType.CUSTOM_SELL + assert res.sell_flag is True + assert res.sell_reason == 'h' * 64 + assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From 7c8a367442b81c6d0913af47d91089bd88c3534f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 20:36:06 +0200 Subject: [PATCH 18/19] Update docs to not promote stoploss / take-profit --- docs/strategy-advanced.md | 23 ++++++++++++----------- freqtrade/optimize/backtesting.py | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f702b1448..3ef121091 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -44,9 +44,9 @@ class AwesomeStrategy(IStrategy): ## Custom sell signal -It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade. +It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. -An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day: +An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than 1 day: ``` python from freqtrade.strategy import IStrategy, timeframe_to_prev_date @@ -58,21 +58,22 @@ class AwesomeStrategy(IStrategy): trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze() # Sell when price falls below value in stoploss column of taken buy signal. - if 'stop_loss' in trade_row: - if current_rate <= trade_row['stop_loss'] < trade.open_rate: - return 'stop_loss' + # above 20% profit, sell when rsi < 80 + if current_profit > 0.2: + if trade_row['rsi'] < 80: + return 'rsi_below_80' - # Sell when price reaches value in take_profit column of taken buy signal. - if 'take_profit' in trade_row: - if trade.open_rate < trade_row['take_profit'] <= current_rate: - return 'take_profit' + # Between 2% and 10%, sell if EMA-long above EMA-short + if 0.02 < current_profit < 0.1: + if trade_row['emalong'] > trade_row['emashort']: + return 'ema_long_below_80' # Sell any positions at a loss if they are held for more than two days. - if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1: + if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: return 'unclog' ``` -See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. +See [Custom stoploss using an indicator from dataframe example](#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. ## Custom stoploss diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b03a459ea..7b62661d3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -251,7 +251,8 @@ class Backtesting: sell_row: Tuple) -> Optional[LocalTrade]: sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore - sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX], + sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], + sell_row[SELL_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: From 3285f6caa35ef555f8956eecd3468eae04c2e9c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Apr 2021 20:42:15 +0200 Subject: [PATCH 19/19] Improve wording in Note box --- docs/strategy-advanced.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 3ef121091..58e658509 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -270,10 +270,10 @@ Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info. !!! Note - `dataframe` is indexed by candle date. During dry/live runs `current_time` and - `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw - an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous - candle before using it as a `dataframe` index. + `dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and + `trade.open_date_utc` will not match the candle date precisely and using them directly will throw + an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to the candle's open date + before using it to access `dataframe`. ``` python from freqtrade.exchange import timeframe_to_prev_date