From b8475b46daea6cb04929604ccf4e2814cc5587cc Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Mon, 15 Mar 2021 20:34:06 +0100 Subject: [PATCH 1/6] Add an example how to use custom_info[trade.open_date] in custom_stoploss() by implementing a fixed risk/reward ratio. --- user_data/strategies/fixed_riskreward_loss.py | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 user_data/strategies/fixed_riskreward_loss.py diff --git a/user_data/strategies/fixed_riskreward_loss.py b/user_data/strategies/fixed_riskreward_loss.py new file mode 100644 index 0000000..5fc5f91 --- /dev/null +++ b/user_data/strategies/fixed_riskreward_loss.py @@ -0,0 +1,123 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# isort: skip_file +# --- Do not remove these libs --- +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame + +from freqtrade.strategy.interface import IStrategy + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +from datetime import datetime +from freqtrade.persistence import Trade +from freqtrade.state import RunMode +import logging +logger = logging.getLogger(__name__) + +class FixedRiskRewardLoss(IStrategy): + """ + This strategy uses custom_stoploss() to enforce a fixed risk/reward ratio + by first calculating a dynamic initial stoploss via ATR - last negative peak + + After that, we caculate that initial risk and multiply it with an risk_reward_ratio + Once this is reached, stoploss is set to it and sell signal is enabled + + Also there is a break even ratio. Once this is reached, the stoploss is adjusted to minimize + losses by setting it to the buy rate + fees. + """ + + custom_info = { + 'risk_reward_ratio': 3.5, + 'set_to_break_even_at_profit': 1, + } + use_custom_stoploss = True + stoploss = -0.9 + sell_profit_only = True + sell_profit_offset = 1 # 100%, get's set dynamically in trail + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + """ + custom_stoploss using a risk/reward ratio + """ + result = break_even_sl = takeprofit_sl = -1 + custom_info_pair = self.custom_info[pair] + if custom_info_pair is not None: + # using current_time/open_date directly via custom_info_pair[trade.open_daten] + # would only work in backtesting/hyperopt. + # in live/dry-run, we have to search for nearest row before it + timezone = custom_info_pair.index.tz + open_date = trade.open_date.replace(tzinfo=timezone) + open_date_mask = custom_info_pair.index.unique().get_loc(open_date, method='ffill') + open_df = custom_info_pair.iloc[open_date_mask] + initial_sl_abs = open_df['stoploss_rate'] + + # calculate initial stoploss at open_date + initial_sl = initial_sl_abs/current_rate-1 + + # calculate take profit treshold + # by using the initial risk and multiplying it + risk_distance = trade.open_rate-initial_sl_abs + reward_distance = risk_distance*self.custom_info['risk_reward_ratio'] + # take_profit tries to lock in profit once price gets over + # risk/reward ratio treshold + take_profit_price_abs = trade.open_rate+reward_distance + # take_profit gets triggerd at this profit + take_profit_pct = take_profit_price_abs/trade.open_rate-1 + + # break_even tries to set sl at open_rate+fees (0 loss) + break_even_profit_distance = risk_distance*self.custom_info['set_to_break_even_at_profit'] + # break_even gets triggerd at this profit + break_even_profit_pct = (break_even_profit_distance+current_rate)/current_rate-1 + + result = initial_sl + if(current_profit >= break_even_profit_pct): + break_even_sl = (trade.open_rate*(1+trade.fee_open+trade.fee_close) / current_rate)-1 + result = break_even_sl + + if(current_profit >= take_profit_pct): + takeprofit_sl = take_profit_price_abs/current_rate-1 + result = takeprofit_sl + + # enable sell signal only after take_profit treshold is reached + self.sell_profit_offset = take_profit_pct + + return result + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['min'] = dataframe['low'].rolling(48).min() + dataframe['atr'] = ta.ATR(dataframe) + dataframe['stoploss_rate'] = dataframe['min']-(dataframe['atr']) + self.custom_info[metadata['pair']] = dataframe[['date', 'stoploss_rate']].copy().set_index('date') + + # all "normal" indicators: + # e.g. + # dataframe['rsi'] = ta.RSI(dataframe) + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Placeholder Strategy: buys when SAR is smaller then candle before + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + # Allways buys + dataframe.loc[:, 'buy'] = 1 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Placeholder Strategy: does nothing + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + + # Always sells + dataframe.loc[:, 'sell'] = 1 + return dataframe From 2362091dbb1eea9e9c8001a7ddf741259183e3ea Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 16 Mar 2021 12:43:47 +0100 Subject: [PATCH 2/6] fixup(bb88d64): use get(), won't throw if doesn't exist --- user_data/strategies/fixed_riskreward_loss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_data/strategies/fixed_riskreward_loss.py b/user_data/strategies/fixed_riskreward_loss.py index 5fc5f91..efd8327 100644 --- a/user_data/strategies/fixed_riskreward_loss.py +++ b/user_data/strategies/fixed_riskreward_loss.py @@ -45,7 +45,7 @@ class FixedRiskRewardLoss(IStrategy): custom_stoploss using a risk/reward ratio """ result = break_even_sl = takeprofit_sl = -1 - custom_info_pair = self.custom_info[pair] + custom_info_pair = self.custom_info.get(pair) if custom_info_pair is not None: # using current_time/open_date directly via custom_info_pair[trade.open_daten] # would only work in backtesting/hyperopt. From 08c75be22ef621bfddbfc26c6a53c79bdc5042d5 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 16 Mar 2021 12:45:33 +0100 Subject: [PATCH 3/6] fixup(bb88d64): use trade.open_date_utc directly instead of getting tz from DatetimeIndex --- user_data/strategies/fixed_riskreward_loss.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/user_data/strategies/fixed_riskreward_loss.py b/user_data/strategies/fixed_riskreward_loss.py index efd8327..4703247 100644 --- a/user_data/strategies/fixed_riskreward_loss.py +++ b/user_data/strategies/fixed_riskreward_loss.py @@ -50,9 +50,7 @@ class FixedRiskRewardLoss(IStrategy): # using current_time/open_date directly via custom_info_pair[trade.open_daten] # would only work in backtesting/hyperopt. # in live/dry-run, we have to search for nearest row before it - timezone = custom_info_pair.index.tz - open_date = trade.open_date.replace(tzinfo=timezone) - open_date_mask = custom_info_pair.index.unique().get_loc(open_date, method='ffill') + open_date_mask = custom_info_pair.index.unique().get_loc(trade.open_date_utc, method='ffill') open_df = custom_info_pair.iloc[open_date_mask] initial_sl_abs = open_df['stoploss_rate'] From db5157a7ca0f009461c2ed44e5c140f3d724d535 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 16 Mar 2021 12:46:43 +0100 Subject: [PATCH 4/6] fixup(bb88d64): add guard for trades extends range of candle data in memory --- user_data/strategies/fixed_riskreward_loss.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user_data/strategies/fixed_riskreward_loss.py b/user_data/strategies/fixed_riskreward_loss.py index 4703247..3eca3fb 100644 --- a/user_data/strategies/fixed_riskreward_loss.py +++ b/user_data/strategies/fixed_riskreward_loss.py @@ -52,6 +52,12 @@ class FixedRiskRewardLoss(IStrategy): # in live/dry-run, we have to search for nearest row before it open_date_mask = custom_info_pair.index.unique().get_loc(trade.open_date_utc, method='ffill') open_df = custom_info_pair.iloc[open_date_mask] + + # trade might be open too long for us to find opening candle + if(len(open_df) != 1): + self.sell_profit = False # re-activate sell signal at any profit + return -1 # won't update current stoploss + initial_sl_abs = open_df['stoploss_rate'] # calculate initial stoploss at open_date From 7063945ad3e6238fceb11a7a086ee64b227bab35 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Mon, 22 Mar 2021 11:19:24 +0100 Subject: [PATCH 5/6] fix(fixed_riskreward_loss): remove `sell_profit_only` related operations doesn't work, because it won't update because it's only loaded once on strategy init --- user_data/strategies/fixed_riskreward_loss.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/user_data/strategies/fixed_riskreward_loss.py b/user_data/strategies/fixed_riskreward_loss.py index 3eca3fb..b18e230 100644 --- a/user_data/strategies/fixed_riskreward_loss.py +++ b/user_data/strategies/fixed_riskreward_loss.py @@ -35,8 +35,6 @@ class FixedRiskRewardLoss(IStrategy): } use_custom_stoploss = True stoploss = -0.9 - sell_profit_only = True - sell_profit_offset = 1 # 100%, get's set dynamically in trail def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: @@ -55,7 +53,6 @@ class FixedRiskRewardLoss(IStrategy): # trade might be open too long for us to find opening candle if(len(open_df) != 1): - self.sell_profit = False # re-activate sell signal at any profit return -1 # won't update current stoploss initial_sl_abs = open_df['stoploss_rate'] @@ -87,9 +84,6 @@ class FixedRiskRewardLoss(IStrategy): takeprofit_sl = take_profit_price_abs/current_rate-1 result = takeprofit_sl - # enable sell signal only after take_profit treshold is reached - self.sell_profit_offset = take_profit_pct - return result def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From 16bbd1f737db22b0d6a74b817dbed9df38f6a92a Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Mon, 22 Mar 2021 11:20:57 +0100 Subject: [PATCH 6/6] fix(fixed_riskreward_loss): simplify example stoploss calculations just use classic 2 times ATR as a stoploss --- user_data/strategies/fixed_riskreward_loss.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/user_data/strategies/fixed_riskreward_loss.py b/user_data/strategies/fixed_riskreward_loss.py index b18e230..4e2a29f 100644 --- a/user_data/strategies/fixed_riskreward_loss.py +++ b/user_data/strategies/fixed_riskreward_loss.py @@ -87,9 +87,8 @@ class FixedRiskRewardLoss(IStrategy): return result def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['min'] = dataframe['low'].rolling(48).min() dataframe['atr'] = ta.ATR(dataframe) - dataframe['stoploss_rate'] = dataframe['min']-(dataframe['atr']) + dataframe['stoploss_rate'] = dataframe['close']-(dataframe['atr']*2) self.custom_info[metadata['pair']] = dataframe[['date', 'stoploss_rate']].copy().set_index('date') # all "normal" indicators: @@ -116,6 +115,6 @@ class FixedRiskRewardLoss(IStrategy): :return: DataFrame with buy column """ - # Always sells - dataframe.loc[:, 'sell'] = 1 + # Never sells + dataframe.loc[:, 'sell'] = 0 return dataframe