From 768a24c375014f755fa857c4b09c8bf4e0154922 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Dec 2020 07:45:41 +0100 Subject: [PATCH 01/19] Add stoplossvalue interface --- docs/strategy-advanced.md | 5 +++++ freqtrade/strategy/interface.py | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 359280694..85a5a6bc6 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -8,6 +8,11 @@ If you're just getting started, please be familiar with the methods described in !!! Note All callback methods described below should only be implemented in a strategy if they are actually used. +## Custom stoploss logic + +// TODO: Complete this section + + ## Custom order timeout rules Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 027c5d36e..33a7ef0c1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -254,6 +254,24 @@ class IStrategy(ABC): """ return True + def stoploss_value(self, pair: str, trade: Trade, rate: float, **kwargs) -> float: + """ + Define custom stoploss logic + The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. + + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns the initial stoploss value + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param rate: Rate that's going to be used when using limit orders + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New stoploss value, relative to the open price + """ + return self.stoploss + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. From 8f6aefb591f4d69e8a023c935e3994aab5e3b60c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Dec 2020 07:41:06 +0100 Subject: [PATCH 02/19] Extract stoploss assignment --- freqtrade/persistence/models.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7fa894e9c..e803b4383 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -342,6 +342,12 @@ class Trade(_DECL_BASE): self.max_rate = max(current_price, self.max_rate or self.open_rate) self.min_rate = min(current_price, self.min_rate or self.open_rate) + def _set_new_stoploss(self, new_loss: float, stoploss: float): + """Assign new stop value""" + self.stop_loss = new_loss + self.stop_loss_pct = -1 * abs(stoploss) + self.stoploss_last_update = datetime.utcnow() + def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False) -> None: """ @@ -360,19 +366,15 @@ class Trade(_DECL_BASE): # no stop loss assigned yet if not self.stop_loss: logger.debug(f"{self.pair} - Assigning new stoploss...") - self.stop_loss = new_loss - self.stop_loss_pct = -1 * abs(stoploss) + self._set_new_stoploss(new_loss, stoploss) self.initial_stop_loss = new_loss self.initial_stop_loss_pct = -1 * abs(stoploss) - self.stoploss_last_update = datetime.utcnow() # evaluate if the stop loss needs to be updated else: if new_loss > self.stop_loss: # stop losses only walk up, never down! logger.debug(f"{self.pair} - Adjusting stoploss...") - self.stop_loss = new_loss - self.stop_loss_pct = -1 * abs(stoploss) - self.stoploss_last_update = datetime.utcnow() + self._set_new_stoploss(new_loss, stoploss) else: logger.debug(f"{self.pair} - Keeping current stoploss...") From a414b57d5469402f4002d7affbf69fedb61b9f11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 07:06:16 +0100 Subject: [PATCH 03/19] Experiment with custom stoploss interface --- freqtrade/strategy/interface.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 33a7ef0c1..7f60ba62f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -89,6 +89,7 @@ class IStrategy(ABC): trailing_stop_positive: Optional[float] = None trailing_stop_positive_offset: float = 0.0 trailing_only_offset_is_reached = False + custom_stoploss: bool = False # associated timeframe ticker_interval: str # DEPRECATED @@ -254,21 +255,22 @@ class IStrategy(ABC): """ return True - def stoploss_value(self, pair: str, trade: Trade, rate: float, **kwargs) -> float: + def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, + **kwargs) -> float: """ Define custom stoploss logic The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. - For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value :param pair: Pair that's about to be sold. :param trade: trade object. - :param rate: Rate that's going to be used when using limit orders + :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 open price + :return float: New stoploss value, relative to the currentrate """ return self.stoploss @@ -549,6 +551,18 @@ class IStrategy(ABC): # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) + if self.custom_stoploss: + stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None + )(pair=trade.pair, trade=trade, + current_rate=current_rate, + current_profit=current_profit) + # Sanity check - error cases will return None + if stop_loss_value: + # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") + trade.adjust_stop_loss(current_rate, stop_loss_value) + else: + logger.warning("CustomStoploss function did not return valid stoploss") + if self.trailing_stop: # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset From 18795844d818e983cdda0041799659dad7a7f963 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Dec 2020 07:59:15 +0100 Subject: [PATCH 04/19] Add initial set of custom stoploss documentation --- docs/strategy-advanced.md | 48 +++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 85a5a6bc6..d4068c82b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -8,16 +8,56 @@ If you're just getting started, please be familiar with the methods described in !!! Note All callback methods described below should only be implemented in a strategy if they are actually used. -## Custom stoploss logic +## Custom stoploss -// TODO: Complete this section +A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. +Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. +The usage of the custom stoploss method must be enabled by setting `custom_stoploss=True` on the strategy object. +The method must return a stoploss value (float / number) with a relative ratio below the current price. +E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, + **kwargs) -> float: + # TODO: Add full docstring here + return 0.04 +``` + +!!! Tip "Trailing stoploss" + It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. + +### Custom stoploss examples + +Absolute stoploss. The below example sets absolute profit levels based on the current profit. + +* Use the regular stoploss until 20% profit is reached +* Once profit is > 20% - stoploss will be set to 7%.s +* Once profit is > 25% - stoploss will be 15%. +* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. + +``` python + def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, + **kwargs) -> float: + # TODO: Add full docstring here + + # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price + if current_profit > 0.20: + return (-0.7 + current_profit) + if current_profit > 0.25: + return (-0.15 + current_profit) + if current_profit > 0.40: + return (-0.25 + current_profit) + return 1 +``` ## Custom order timeout rules -Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. +Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. -However, freqtrade also offers a custom callback for both ordertypes, which allows you to decide based on custom criteria if a order did time out or not. +However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not. !!! Note Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. From f235ab8cf4fb148c122b20822f135cb5ce0a8c2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 11:39:21 +0100 Subject: [PATCH 05/19] Fix some typos in docs --- docs/strategy-advanced.md | 12 ++++++------ docs/strategy-customization.md | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index d4068c82b..02ee9b201 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -73,7 +73,7 @@ The function must return either `True` (cancel order) or `False` (keep order ali from datetime import datetime, timedelta from freqtrade.persistence import Trade -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -112,7 +112,7 @@ class Awesomestrategy(IStrategy): from datetime import datetime from freqtrade.persistence import Trade -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -148,7 +148,7 @@ This can be used to perform calculations which are pair independent (apply to al ``` python import requests -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -173,7 +173,7 @@ class Awesomestrategy(IStrategy): `confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). ``` python -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -209,7 +209,7 @@ class Awesomestrategy(IStrategy): from freqtrade.persistence import Trade -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # ... populate_* methods @@ -264,4 +264,4 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): trailing_stop = True ``` -Both attributes and methods may be overriden, altering behavior of the original strategy in a way you need. +Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ab64d3a67..688a1c338 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -309,7 +309,7 @@ Storing information can be accomplished by creating a new dictionary within the The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. ```python -class Awesomestrategy(IStrategy): +class AwesomeStrategy(IStrategy): # Create custom dictionary cust_info = {} From f7b54c24158f4e80ed927d4a333c739f97e09ce3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 11:46:49 +0100 Subject: [PATCH 06/19] Allow and document time-based custom stoploss closes #3206 --- docs/strategy-advanced.md | 28 +++++++++++++++++++++++++--- freqtrade/strategy/interface.py | 5 +++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 02ee9b201..60da11207 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -31,7 +31,9 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l ### Custom stoploss examples -Absolute stoploss. The below example sets absolute profit levels based on the current profit. +#### Absolute stoploss + +The below example sets absolute profit levels based on the current profit. * Use the regular stoploss until 20% profit is reached * Once profit is > 20% - stoploss will be set to 7%.s @@ -39,8 +41,10 @@ Absolute stoploss. The below example sets absolute profit levels based on the cu * Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. ``` python - def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, - **kwargs) -> float: + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price @@ -53,6 +57,24 @@ Absolute stoploss. The below example sets absolute profit levels based on the cu return 1 ``` +#### Time based trailing stop + +Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: + # TODO: Add full docstring here + + if current_time - timedelta(minutes=60) > trade.open_time: + return -0.10 + elif current_time - timedelta(minutes=120) > trade.open_time: + return -0.05 + return 1 +``` + ## Custom order timeout rules Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7f60ba62f..4574ca9f2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -255,8 +255,8 @@ class IStrategy(ABC): """ return True - def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, - **kwargs) -> float: + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: """ Define custom stoploss logic The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. @@ -554,6 +554,7 @@ class IStrategy(ABC): if self.custom_stoploss: stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None )(pair=trade.pair, trade=trade, + current_time=current_time, current_rate=current_rate, current_profit=current_profit) # Sanity check - error cases will return None From b2c109831668936cae9ea1c3eae54ffc763efd2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 11:58:42 +0100 Subject: [PATCH 07/19] more docs for dynamic stoploss method --- docs/strategy-advanced.md | 41 +++++++++++++++++++++++++++++---- freqtrade/strategy/interface.py | 1 + 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 60da11207..f54bec206 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -20,23 +20,29 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l ``` python custom_stoploss = True - def stoploss_value(self, pair: str, trade: Trade, current_rate: float, current_profit: float, - **kwargs) -> float: + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: # TODO: Add full docstring here return 0.04 ``` +!!! Note "Use of dates" + All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. + !!! Tip "Trailing stoploss" It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. ### Custom stoploss examples +The next section will show some examples on what's possible with the custom stoploss function. +Of course, many more things are possible, and all examples can be combined at will. + #### Absolute stoploss The below example sets absolute profit levels based on the current profit. * Use the regular stoploss until 20% profit is reached -* Once profit is > 20% - stoploss will be set to 7%.s +* Once profit is > 20% - stoploss will be set to 7%. * Once profit is > 25% - stoploss will be 15%. * Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. @@ -68,13 +74,34 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai current_profit: float, **kwargs) -> float: # TODO: Add full docstring here - if current_time - timedelta(minutes=60) > trade.open_time: + if current_time - timedelta(minutes=60) > trade.open_date: return -0.10 - elif current_time - timedelta(minutes=120) > trade.open_time: + elif current_time - timedelta(minutes=120) > trade.open_date: return -0.05 return 1 ``` +#### Different stoploss per pair + +Use a different stoploss depending on the pair. +In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: + # TODO: Add full docstring here + + if pair in ('ETH/BTC', 'XRP/BTC'): + return -0.10 + elif pair in ('LTC/BTC'): + return -0.05 + return -0.15 +``` + +--- + ## Custom order timeout rules Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. @@ -162,6 +189,8 @@ class AwesomeStrategy(IStrategy): return False ``` +--- + ## Bot loop start callback A simple callback which is called once at the start of every bot throttling iteration. @@ -267,6 +296,8 @@ class AwesomeStrategy(IStrategy): ``` +--- + ## Derived strategies The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4574ca9f2..021674bb9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -267,6 +267,7 @@ class IStrategy(ABC): :param pair: Pair that's about to be sold. :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. From 6892c08e9b9091ab68a89ea92ff654316813b7a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 13:18:06 +0100 Subject: [PATCH 08/19] Improve docstring --- freqtrade/strategy/interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 021674bb9..9378f1996 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -258,12 +258,14 @@ class IStrategy(ABC): def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ - Define custom stoploss logic + 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. The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value + Only called when custom_stoploss is set to True. :param pair: Pair that's about to be sold. :param trade: trade object. From 11e29156210828e1d3d97714eb45cfc2fcc237cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 17:40:01 +0100 Subject: [PATCH 09/19] Fix documentation problem --- docs/strategy-advanced.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f54bec206..79a1f0ec5 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -42,9 +42,9 @@ Of course, many more things are possible, and all examples can be combined at wi The below example sets absolute profit levels based on the current profit. * Use the regular stoploss until 20% profit is reached -* Once profit is > 20% - stoploss will be set to 7%. -* Once profit is > 25% - stoploss will be 15%. * Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. +* Once profit is > 25% - stoploss will be 15%. +* Once profit is > 20% - stoploss will be set to 7%. ``` python custom_stoploss = True @@ -54,12 +54,12 @@ The below example sets absolute profit levels based on the current profit. # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price - if current_profit > 0.20: - return (-0.7 + current_profit) - if current_profit > 0.25: - return (-0.15 + current_profit) if current_profit > 0.40: return (-0.25 + current_profit) + if current_profit > 0.25: + return (-0.15 + current_profit) + if current_profit > 0.20: + return (-0.7 + current_profit) return 1 ``` @@ -74,10 +74,11 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai current_profit: float, **kwargs) -> float: # TODO: Add full docstring here - if current_time - timedelta(minutes=60) > trade.open_date: - return -0.10 - elif current_time - timedelta(minutes=120) > trade.open_date: + # 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: return -0.05 + elif current_time - timedelta(minutes=60) > trade.open_date: + return -0.10 return 1 ``` From ea4238e86019a275f2f8131cd81d6e0cb51ebac8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 17:59:49 +0100 Subject: [PATCH 10/19] cleanup some tests --- tests/strategy/test_default_strategy.py | 19 ++++++++++++++++++- tests/strategy/test_interface.py | 6 ++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 1b1648db9..c5d76b4c5 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -1,3 +1,5 @@ +from datetime import datetime +from freqtrade.persistence.models import Trade from pandas import DataFrame from .strats.default_strategy import DefaultStrategy @@ -12,7 +14,7 @@ def test_default_strategy_structure(): assert hasattr(DefaultStrategy, 'populate_sell_trend') -def test_default_strategy(result): +def test_default_strategy(result, fee): strategy = DefaultStrategy({}) metadata = {'pair': 'ETH/BTC'} @@ -23,3 +25,18 @@ def test_default_strategy(result): assert type(indicators) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame + + trade = Trade( + open_rate=19_000, + amount=0.1, + pair='ETH/BTC', + fee_open=fee.return_value + ) + + assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, + rate=20000, time_in_force='gtc') is True + assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, + rate=20000, time_in_force='gtc', sell_reason='roi') is True + + assert strategy.stoploss_value(pair='ETH/BTC', trade=trade, current_time=datetime.now(), + current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 640849ba4..3625c964f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -105,9 +105,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) -def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): - # default_conf defines a 5m interval. we check interval * 2 + 5m - # this is necessary as the last candle is removed (partial candles) by default +def test_assert_df_raise(mocker, caplog, ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() @@ -127,7 +125,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): caplog) -def test_assert_df(default_conf, mocker, ohlcv_history, caplog): +def test_assert_df(ohlcv_history, caplog): df_len = len(ohlcv_history) - 1 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), From 22d64553c91e3f3d6c5c2541ee3363c611a8c6f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 18:00:44 +0100 Subject: [PATCH 11/19] Rename test file --- tests/strategy/{test_strategy.py => test_strategy_loading.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/strategy/{test_strategy.py => test_strategy_loading.py} (100%) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy_loading.py similarity index 100% rename from tests/strategy/test_strategy.py rename to tests/strategy/test_strategy_loading.py From 5f8610b28f9b3367ac371a1a405490d27a16265d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 20:08:03 +0100 Subject: [PATCH 12/19] Add explicit test for stop_loss_reached --- tests/strategy/test_interface.py | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 3625c964f..4aedc0c3a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 +from freqtrade.strategy.interface import SellCheckTuple, SellType import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -286,6 +287,62 @@ def test_min_roi_reached3(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) +@pytest.mark.parametrize('profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2', [ + # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, + # enable custom stoploss, expected after 1st call, expected after 2nd call + (0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE), + (0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS), + (0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS), + (0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE), + (0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS), + # Default custom case - trails with 10% + (0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE), + (0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS), +]) +def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, + profit2, adjusted2, expected2) -> 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='bittrex', + open_rate=1, + ) + trade.adjust_min_max_rates(trade.open_rate) + strategy.trailing_stop = trailing + strategy.trailing_stop_positive = -0.05 + strategy.custom_stoploss = custom + + 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) + assert isinstance(sl_flag, SellCheckTuple) + assert sl_flag.sell_type == expected + if expected == SellType.NONE: + assert sl_flag.sell_flag is False + else: + assert sl_flag.sell_flag is True + assert round(trade.stop_loss, 2) == adjusted + + 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) + assert sl_flag.sell_type == expected2 + if expected2 == SellType.NONE: + assert sl_flag.sell_flag is False + else: + assert sl_flag.sell_flag is True + assert round(trade.stop_loss, 2) == adjusted2 + + 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 f8639fe93849d9423d1ee8e04c90f9b8738334a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Dec 2020 20:22:32 +0100 Subject: [PATCH 13/19] Add more tests for custom_loss --- tests/strategy/test_interface.py | 44 ++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4aedc0c3a..e8a3ee8de 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,7 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper @@ -287,20 +287,30 @@ def test_min_roi_reached3(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) -@pytest.mark.parametrize('profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2', [ - # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, - # enable custom stoploss, expected after 1st call, expected after 2nd call - (0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE), - (0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS), - (0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS), - (0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE), - (0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS), - # Default custom case - trails with 10% - (0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE), - (0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS), -]) +@pytest.mark.parametrize( + 'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ + # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, + # enable custom stoploss, expected after 1st call, expected after 2nd call + (0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE, None), + (0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS, None), + (0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS, None), + (0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE, None), + (0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS, None), + # Default custom case - trails with 10% + (0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE, None), + (0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS, None), + (0.05, 1, SellType.NONE, False, True, -0.06, 1, SellType.TRAILING_STOP_LOSS, + lambda **kwargs: -0.05), + (0.05, 1, SellType.NONE, False, True, 0.09, 1.04, SellType.NONE, + lambda **kwargs: -0.05), + (0.05, 0.95, SellType.NONE, False, True, 0.09, 0.98, SellType.NONE, + lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)), + # Error case - static stoploss in place + (0.05, 0.9, SellType.NONE, False, True, 0.09, 0.9, SellType.NONE, + lambda **kwargs: None), + ]) def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, - profit2, adjusted2, expected2) -> None: + profit2, adjusted2, expected2, custom_stop) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) @@ -319,6 +329,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.trailing_stop = trailing strategy.trailing_stop_positive = -0.05 strategy.custom_stoploss = custom + original_stopvalue = strategy.stoploss_value + if custom_stop: + strategy.stoploss_value = custom_stop now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, @@ -342,6 +355,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili assert sl_flag.sell_flag is True assert round(trade.stop_loss, 2) == adjusted2 + strategy.stoploss_value = original_stopvalue + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) From 8574751a07725f5727c30726fa2c7ce996de678a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 10:13:47 +0100 Subject: [PATCH 14/19] Add stoploss_value to strategy template --- docs/strategy-advanced.md | 55 ++++++++++--------- .../subtemplates/strategy_methods_advanced.j2 | 24 ++++++++ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 79a1f0ec5..e00e3d78d 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -8,6 +8,9 @@ If you're just getting started, please be familiar with the methods described in !!! Note All callback methods described below should only be implemented in a strategy if they are actually used. +!!! Tip + You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` + ## Custom stoploss A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. @@ -37,32 +40,6 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l The next section will show some examples on what's possible with the custom stoploss function. Of course, many more things are possible, and all examples can be combined at will. -#### Absolute stoploss - -The below example sets absolute profit levels based on the current profit. - -* Use the regular stoploss until 20% profit is reached -* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. -* Once profit is > 25% - stoploss will be 15%. -* Once profit is > 20% - stoploss will be set to 7%. - -``` python - custom_stoploss = True - - def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here - - # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price - if current_profit > 0.40: - return (-0.25 + current_profit) - if current_profit > 0.25: - return (-0.15 + current_profit) - if current_profit > 0.20: - return (-0.7 + current_profit) - return 1 -``` - #### Time based trailing stop Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. @@ -101,6 +78,32 @@ In this example, we'll trail the highest price with 10% trailing stoploss for `E return -0.15 ``` +#### Absolute stoploss + +The below example sets absolute profit levels based on the current profit. + +* Use the regular stoploss until 20% profit is reached +* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit. +* Once profit is > 25% - stoploss will be 15%. +* Once profit is > 20% - stoploss will be set to 7%. + +``` python + custom_stoploss = True + + def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs) -> float: + # TODO: Add full docstring here + + # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price + if current_profit > 0.40: + return (-0.25 + current_profit) + if current_profit > 0.25: + return (-0.15 + current_profit) + if current_profit > 0.20: + return (-0.7 + current_profit) + return 1 +``` + --- ## Custom order timeout rules diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 5ca6e6971..e6ae477b9 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,6 +12,30 @@ def bot_loop_start(self, **kwargs) -> None: """ pass +custom_stoploss = True + +def stoploss_value(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **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. + The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns the initial stoploss value + Only called when custom_stoploss is set to True. + + :param pair: Pair that's about to be sold. + :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 float: New stoploss value, relative to the currentrate + """ + return self.stoploss + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, **kwargs) -> bool: """ From 277342f1679f1dea80fa24ed7061424afebf97a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:12:22 +0100 Subject: [PATCH 15/19] Rename flag to "use_custom_stoposs" --- docs/strategy-advanced.md | 10 +++++----- freqtrade/strategy/interface.py | 6 +++--- .../subtemplates/strategy_methods_advanced.j2 | 4 ++-- tests/strategy/test_interface.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index e00e3d78d..f2d8e4151 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -16,12 +16,12 @@ If you're just getting started, please be familiar with the methods described in A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price. Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. -The usage of the custom stoploss method must be enabled by setting `custom_stoploss=True` on the strategy object. +The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. The method must return a stoploss value (float / number) with a relative ratio below the current price. E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: @@ -45,7 +45,7 @@ Of course, many more things are possible, and all examples can be combined at wi Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: @@ -65,7 +65,7 @@ Use a different stoploss depending on the pair. In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: @@ -88,7 +88,7 @@ The below example sets absolute profit levels based on the current profit. * Once profit is > 20% - stoploss will be set to 7%. ``` python - custom_stoploss = True + use_custom_stoploss = True def stoploss_value(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9378f1996..d93dda849 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -89,7 +89,7 @@ class IStrategy(ABC): trailing_stop_positive: Optional[float] = None trailing_stop_positive_offset: float = 0.0 trailing_only_offset_is_reached = False - custom_stoploss: bool = False + use_custom_stoploss: bool = False # associated timeframe ticker_interval: str # DEPRECATED @@ -265,7 +265,7 @@ class IStrategy(ABC): For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value - Only called when custom_stoploss is set to True. + Only called when use_custom_stoploss is set to True. :param pair: Pair that's about to be sold. :param trade: trade object. @@ -554,7 +554,7 @@ class IStrategy(ABC): # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) - if self.custom_stoploss: + if self.use_custom_stoploss: stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index e6ae477b9..0ae3e077c 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,7 +12,7 @@ def bot_loop_start(self, **kwargs) -> None: """ pass -custom_stoploss = True +use_custom_stoploss = True def stoploss_value(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, current_profit: float, **kwargs) -> float: @@ -24,7 +24,7 @@ def stoploss_value(self, pair: str, trade: 'Trade', current_time: 'datetime', cu For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, returns the initial stoploss value - Only called when custom_stoploss is set to True. + Only called when use_custom_stoploss is set to True. :param pair: Pair that's about to be sold. :param trade: trade object. diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e8a3ee8de..5922ad9b2 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -328,7 +328,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili trade.adjust_min_max_rates(trade.open_rate) strategy.trailing_stop = trailing strategy.trailing_stop_positive = -0.05 - strategy.custom_stoploss = custom + strategy.use_custom_stoploss = custom original_stopvalue = strategy.stoploss_value if custom_stop: strategy.stoploss_value = custom_stop From 9d5961e2247295d086023cacc9d959722404d0f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:17:50 +0100 Subject: [PATCH 16/19] Rename method to custom_stoploss --- docs/strategy-advanced.md | 16 ++++++++-------- freqtrade/strategy/interface.py | 6 +++--- .../subtemplates/strategy_methods_advanced.j2 | 4 ++-- tests/strategy/test_default_strategy.py | 8 +++++--- tests/strategy/test_interface.py | 11 +++++------ 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f2d8e4151..bb455ff9f 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -23,8 +23,8 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l ``` python use_custom_stoploss = True - def stoploss_value(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, **kwargs) -> float: # TODO: Add full docstring here return 0.04 ``` @@ -47,8 +47,8 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai ``` python use_custom_stoploss = True - def stoploss_value(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, **kwargs) -> float: # TODO: Add full docstring here # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. @@ -67,8 +67,8 @@ In this example, we'll trail the highest price with 10% trailing stoploss for `E ``` python use_custom_stoploss = True - def stoploss_value(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, **kwargs) -> float: # TODO: Add full docstring here if pair in ('ETH/BTC', 'XRP/BTC'): @@ -90,8 +90,8 @@ The below example sets absolute profit levels based on the current profit. ``` python use_custom_stoploss = True - def stoploss_value(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, **kwargs) -> float: # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d93dda849..61c879e8f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -255,8 +255,8 @@ class IStrategy(ABC): """ return True - def stoploss_value(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, **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. @@ -555,7 +555,7 @@ class IStrategy(ABC): trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) if self.use_custom_stoploss: - stop_loss_value = strategy_safe_wrapper(self.stoploss_value, default_retval=None + stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 0ae3e077c..f4cda2e89 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -14,8 +14,8 @@ def bot_loop_start(self, **kwargs) -> None: use_custom_stoploss = True -def stoploss_value(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, **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. diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index c5d76b4c5..ec7b3c33d 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -1,7 +1,9 @@ from datetime import datetime -from freqtrade.persistence.models import Trade + from pandas import DataFrame +from freqtrade.persistence.models import Trade + from .strats.default_strategy import DefaultStrategy @@ -38,5 +40,5 @@ def test_default_strategy(result, fee): assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi') is True - assert strategy.stoploss_value(pair='ETH/BTC', trade=trade, current_time=datetime.now(), - current_rate=20_000, current_profit=0.05) == strategy.stoploss + assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), + current_rate=20_000, current_profit=0.05) == strategy.stoploss diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5922ad9b2..7eed43302 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from freqtrade.strategy.interface import SellCheckTuple, SellType import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -11,9 +10,10 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import OperationalException, StrategyError +from freqtrade.exceptions import StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -329,9 +329,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.trailing_stop = trailing strategy.trailing_stop_positive = -0.05 strategy.use_custom_stoploss = custom - original_stopvalue = strategy.stoploss_value + original_stopvalue = strategy.custom_stoploss if custom_stop: - strategy.stoploss_value = custom_stop + strategy.custom_stoploss = custom_stop now = arrow.utcnow().datetime sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, @@ -355,8 +355,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili assert sl_flag.sell_flag is True assert round(trade.stop_loss, 2) == adjusted2 - strategy.stoploss_value = original_stopvalue - + strategy.custom_stoploss = original_stopvalue def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: From 676dd0d664e9ee59f2008b518eb12795e3fe9d85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:22:15 +0100 Subject: [PATCH 17/19] Improve documentation --- docs/stoploss.md | 1 + docs/strategy-advanced.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/stoploss.md b/docs/stoploss.md index 1e21fc50d..671e643b0 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -78,6 +78,7 @@ At this stage the bot contains the following stoploss support modes: 2. Trailing stop loss. 3. Trailing stop loss, custom positive loss. 4. Trailing stop loss only once the trade has reached a certain offset. +5. [Custom stoploss function](strategy-advanced.md#custom-stoploss) ### Static Stop Loss diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index bb455ff9f..4833fbade 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -29,6 +29,8 @@ E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "l return 0.04 ``` +Stoploss on exchange works similar to trailing_stop, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). + !!! Note "Use of dates" All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. From fc0d14c1b5c9b15ef04d5b9abb32283bc38bd78c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 19:14:18 +0100 Subject: [PATCH 18/19] Improve documentation --- docs/strategy-advanced.md | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4833fbade..49720d729 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -20,16 +20,35 @@ The usage of the custom stoploss method must be enabled by setting `use_custom_s The method must return a stoploss value (float / number) with a relative ratio below the current price. E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`). +To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: + ``` python use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here - return 0.04 + """ + 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. + The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns the initial stoploss value + Only called when use_custom_stoploss is set to True. + + :param pair: Pair that's about to be sold. + :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 float: New stoploss value, relative to the currentrate + """ + return -0.04 ``` -Stoploss on exchange works similar to trailing_stop, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). +Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). !!! Note "Use of dates" All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. @@ -51,7 +70,6 @@ Use the initial stoploss for the first 60 minutes, after this change to 10% trai def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here # 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: @@ -71,7 +89,6 @@ In this example, we'll trail the highest price with 10% trailing stoploss for `E def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here if pair in ('ETH/BTC', 'XRP/BTC'): return -0.10 @@ -94,7 +111,6 @@ The below example sets absolute profit levels based on the current profit. def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - # TODO: Add full docstring here # Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price if current_profit > 0.40: From 512e1633556ce9cdb4b8afed6205f36fb33feb7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 31 Dec 2020 09:48:49 +0100 Subject: [PATCH 19/19] change docstring to better reflect what the method is for --- docs/strategy-advanced.md | 2 +- freqtrade/strategy/interface.py | 2 +- freqtrade/templates/subtemplates/strategy_methods_advanced.j2 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 49720d729..519a9ac62 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -37,7 +37,7 @@ To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum re When not implemented by a strategy, returns the initial stoploss value Only called when use_custom_stoploss is set to True. - :param pair: Pair that's about to be sold. + :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. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 61c879e8f..1e4fc8b12 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -267,7 +267,7 @@ class IStrategy(ABC): When not implemented by a strategy, returns the initial stoploss value Only called when use_custom_stoploss is set to True. - :param pair: Pair that's about to be sold. + :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. diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index f4cda2e89..53ededa19 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -69,7 +69,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's about to be sold. + :param pair: Pair that's currently analyzed :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in quote currency.