Merge pull request #4750 from rokups/rk/custom_sell

Add IStrategy.custom_sell method which allows per-trade sell signal evaluation
This commit is contained in:
Matthias
2021-04-29 06:50:56 +02:00
committed by GitHub
11 changed files with 249 additions and 129 deletions

View File

@@ -42,32 +42,38 @@ class AwesomeStrategy(IStrategy):
***
### Storing custom information using DatetimeIndex from `dataframe`
## Custom sell signal
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.
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 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
```python
import talib.abstract as ta
class AwesomeStrategy(IStrategy):
# Create custom dictionary
custom_info = {}
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()
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
# Sell when price falls below value in stoploss column of taken buy signal.
# above 20% profit, sell when rsi < 80
if current_profit > 0.2:
if trade_row['rsi'] < 80:
return 'rsi_below_80'
# 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.0 and (current_time - trade.open_date_utc).days >= 1:
return 'unclog'
```
!!! 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
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
@@ -93,7 +99,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.
@@ -110,7 +117,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
```
@@ -143,7 +150,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:
@@ -169,7 +177,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
@@ -195,7 +204,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
@@ -222,7 +232,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
@@ -235,7 +244,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:
@@ -248,18 +258,25 @@ 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"
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['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
from freqtrade.persistence import Trade
from freqtrade.state import RunMode
@@ -270,28 +287,20 @@ 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:
# Default return value
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):
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.
current_time = timeframe_to_prev_date(self.timeframe, 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-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

View File

@@ -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)