# --- Do not remove these libs --- from datetime import datetime import numpy as np from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, CategoricalParameter from pandas import DataFrame, DatetimeIndex, merge, Series import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib import numpy # noqa # custom indicators # ################################################################################################## def RMI(dataframe, *, length=20, mom=5): """ Source: https://github.com/freqtrade/technical/blob/master/technical/indicators/indicators.py#L912 """ df = dataframe.copy() df['maxup'] = (df['close'] - df['close'].shift(mom)).clip(lower=0) df['maxdown'] = (df['close'].shift(mom) - df['close']).clip(lower=0) df.fillna(0, inplace=True) df["emaInc"] = ta.EMA(df, price='maxup', timeperiod=length) df["emaDec"] = ta.EMA(df, price='maxdown', timeperiod=length) df['RMI'] = np.where(df['emaDec'] == 0, 0, 100 - 100 / (1 + df["emaInc"] / df["emaDec"])) return df["RMI"] def zema(dataframe, period, field='close'): """ Source: https://github.com/freqtrade/technical/blob/master/technical/indicators/overlap_studies.py#L79 Modified slightly to use ta.EMA instead of technical ema """ df = dataframe.copy() df['ema1'] = ta.EMA(df[field], timeperiod=period) df['ema2'] = ta.EMA(df['ema1'], timeperiod=period) df['d'] = df['ema1'] - df['ema2'] df['zema'] = df['ema1'] + df['d'] return df['zema'] def same_length(bigger, shorter): return np.concatenate((np.full((bigger.shape[0] - shorter.shape[0]), np.nan), shorter)) def mastreak(dataframe: DataFrame, period: int = 4, field='close') -> Series: """ MA Streak Port of: https://www.tradingview.com/script/Yq1z7cIv-MA-Streak-Can-Show-When-a-Run-Is-Getting-Long-in-the-Tooth/ """ df = dataframe.copy() avgval = zema(df, period, field) arr = np.diff(avgval) pos = np.clip(arr, 0, 1).astype(bool).cumsum() neg = np.clip(arr, -1, 0).astype(bool).cumsum() streak = np.where(arr >= 0, pos - np.maximum.accumulate(np.where(arr <= 0, pos, 0)), -neg + np.maximum.accumulate(np.where(arr >= 0, neg, 0))) res = same_length(df['close'], streak) return res def linear_growth(start: float, end: float, start_time: int, end_time: int, trade_time: int) -> float: """ Simple linear growth function. Grows from start to end after end_time minutes (starts after start_time minutes) """ time = max(0, trade_time - start_time) rate = (end - start) / (end_time - start_time) return min(end, start + (rate * time)) def pcc(dataframe: DataFrame, period: int = 20, mult: int = 2): """ Percent Change Channel PCC is like KC unless it uses percentage changes in price to set channel distance. https://www.tradingview.com/script/6wwAWXA1-MA-Streak-Change-Channel/ """ df = dataframe.copy() df['previous_close'] = df['close'].shift() df['close_change'] = (df['close'] - df['previous_close']) / df['previous_close'] * 100 df['high_change'] = (df['high'] - df['close']) / df['close'] * 100 df['low_change'] = (df['low'] - df['close']) / df['close'] * 100 df['delta'] = df['high_change'] - df['low_change'] mid = zema(df, period, 'close_change') rangema = zema(df, period, 'delta') upper = mid + rangema * mult lower = mid - rangema * mult return upper, rangema, lower def SSLChannels_ATR(dataframe, length=7): """ SSL Channels with ATR: https://www.tradingview.com/script/SKHqWzql-SSL-ATR-channel/ Credit to @JimmyNixx for python """ df = dataframe.copy() df['ATR'] = ta.ATR(df, timeperiod=14) df['smaHigh'] = df['high'].rolling(length).mean() + df['ATR'] df['smaLow'] = df['low'].rolling(length).mean() - df['ATR'] df['hlv'] = np.where(df['close'] > df['smaHigh'], 1, np.where(df['close'] < df['smaLow'], -1, np.NAN)) df['hlv'] = df['hlv'].ffill() df['sslDown'] = np.where(df['hlv'] < 0, df['smaHigh'], df['smaLow']) df['sslUp'] = np.where(df['hlv'] < 0, df['smaLow'], df['smaHigh']) return df['sslDown'], df['sslUp'] def SROC(dataframe, roclen=21, emalen=13, smooth=21): df = dataframe.copy() roc = ta.ROC(df, timeperiod=roclen) ema = ta.EMA(df, timeperiod=emalen) sroc = ta.ROC(ema, timeperiod=smooth) return sroc def linear_decay(start: float, end: float, start_time: int, end_time: int, trade_time: int) -> float: """ Simple linear decay function. Decays from start to end after end_time minutes (starts after start_time minutes) """ time = max(0, trade_time - start_time) rate = (start - end) / (end_time - start_time) return max(end, start - (rate * time)) # ##################################################################################################### class ReinforcedQuickie(IStrategy): """ author@: Gert Wohlgemuth works on new objectify branch! idea: only buy on an upward tending market """ # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi" minimal_roi = { "0": 100 } # Optimal stoploss designed for the strategy # This attribute will be overridden if the config file contains "stoploss" stoploss = -0.99 # Optimal timeframe for the strategy timeframe = '5m' process_only_new_candles = True startup_candle_count = 30 use_custom_stoploss = True custom_trade_info = {} order_types = { 'entry': 'market', 'exit': 'market', 'emergency_exit': 'market', 'force_entry': 'market', 'force_exit': "market", 'stoploss': 'market', 'stoploss_on_exchange': False, 'stoploss_on_exchange_interval': 60, 'stoploss_on_exchange_limit_ratio': 0.99 } # resample factor to establish our general trend. Basically don't buy if a trend is not given # resample_factor = 12 buy_op = True resample_factor = IntParameter(low=1, high=100, default=25, space='buy', optimize=buy_op) leverage_optimize = True leverage_num = IntParameter(low=1, high=10, default=1, space='buy', optimize=leverage_optimize) # custom exit ce_op = True csell_pullback_amount = DecimalParameter(0.005, 0.15, default=0.01, space='sell', load=True, optimize=ce_op) csell_roi_type = CategoricalParameter(['static', 'decay', 'step'], default='step', space='sell', load=True, optimize=ce_op) csell_roi_start = DecimalParameter(0.01, 0.15, default=0.01, space='sell', load=True, optimize=ce_op) csell_roi_end = DecimalParameter(0.0, 0.01, default=0, space='sell', load=True, optimize=ce_op) csell_roi_time = IntParameter(720, 1440, default=720, space='sell', load=True, optimize=ce_op) csell_trend_type = CategoricalParameter(['rmi', 'ssl', 'candle', 'any', 'none'], default='any', space='sell', load=True, optimize=ce_op) csell_pullback = CategoricalParameter([True, False], default=True, space='sell', load=True, optimize=ce_op) csell_pullback_respect_roi = CategoricalParameter([True, False], default=False, space='sell', load=True, optimize=ce_op) csell_endtrend_respect_roi = CategoricalParameter([True, False], default=False, space='sell', load=True, optimize=ce_op) # Custom Stoploss cs_op = True cstop_loss_threshold = DecimalParameter(-0.35, -0.01, default=-0.03, space='sell', load=True, optimize=cs_op) cstop_bail_how = CategoricalParameter(['roc', 'time', 'any', 'none'], default='none', space='sell', load=True, optimize=cs_op) cstop_bail_roc = DecimalParameter(-5.0, -1.0, default=-3.0, space='sell', load=True, optimize=cs_op) cstop_bail_time = IntParameter(60, 1440, default=720, space='sell', load=True, optimize=cs_op) cstop_bail_time_trend = CategoricalParameter([True, False], default=True, space='sell', load=True, optimize=cs_op) cstop_max_stoploss = DecimalParameter(-0.30, -0.01, default=-0.10, space='sell', load=True, optimize=cs_op) EMA_SHORT_TERM = 5 EMA_MEDIUM_TERM = 12 EMA_LONG_TERM = 21 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: if not metadata['pair'] in self.custom_trade_info: self.custom_trade_info[metadata['pair']] = {} if 'had-trend' not in self.custom_trade_info[metadata["pair"]]: self.custom_trade_info[metadata['pair']]['had-trend'] = False dataframe = self.resample(dataframe, self.timeframe, self.resample_factor.value) ################################################################################## # buy and sell indicators dataframe['ema_{}'.format(self.EMA_SHORT_TERM)] = ta.EMA( dataframe, timeperiod=self.EMA_SHORT_TERM ) dataframe['ema_{}'.format(self.EMA_MEDIUM_TERM)] = ta.EMA( dataframe, timeperiod=self.EMA_MEDIUM_TERM ) dataframe['ema_{}'.format(self.EMA_LONG_TERM)] = ta.EMA( dataframe, timeperiod=self.EMA_LONG_TERM ) bollinger = qtpylib.bollinger_bands( qtpylib.typical_price(dataframe), window=20, stds=2 ) dataframe['bb_lowerband'] = bollinger['lower'] dataframe['bb_middleband'] = bollinger['mid'] dataframe['bb_upperband'] = bollinger['upper'] dataframe['min'] = ta.MIN(dataframe, timeperiod=self.EMA_MEDIUM_TERM) dataframe['max'] = ta.MAX(dataframe, timeperiod=self.EMA_MEDIUM_TERM) dataframe['cci'] = ta.CCI(dataframe) dataframe['mfi'] = ta.MFI(dataframe) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7) dataframe['average'] = (dataframe['close'] + dataframe['open'] + dataframe['high'] + dataframe['low']) / 4 dataframe['rmi'] = RMI(dataframe, length=24, mom=5) dataframe['rmi-up'] = np.where(dataframe['rmi'] >= dataframe['rmi'].shift(), 1, 0) dataframe['rmi-up-trend'] = np.where(dataframe['rmi-up'].rolling(5).sum() >= 3, 1, 0) # Indicators used only for ROI and Custom Stoploss ssldown, sslup = SSLChannels_ATR(dataframe, length=21) dataframe['sroc'] = SROC(dataframe, roclen=21, emalen=13, smooth=21) dataframe['ssl-dir'] = np.where(sslup > ssldown, 'up', 'down') # Trends, Peaks and Crosses dataframe['candle-up'] = np.where(dataframe['close'] >= dataframe['open'], 1, 0) dataframe['candle-up-trend'] = np.where(dataframe['candle-up'].rolling(5).sum() >= 3, 1, 0) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame :return: DataFrame with buy column """ dataframe.loc[ ( ( ( (dataframe['close'] < dataframe['ema_{}'.format(self.EMA_SHORT_TERM)]) & (dataframe['close'] < dataframe['ema_{}'.format(self.EMA_MEDIUM_TERM)]) & (dataframe['close'] == dataframe['min']) & (dataframe['close'] <= dataframe['bb_lowerband']) ) | # simple v bottom shape (lopsided to the left to increase reactivity) # which has to be below a very slow average # this pattern only catches a few, but normally very good buy points ( (dataframe['average'].shift(5) > dataframe['average'].shift(4)) & (dataframe['average'].shift(4) > dataframe['average'].shift(3)) & (dataframe['average'].shift(3) > dataframe['average'].shift(2)) & (dataframe['average'].shift(2) > dataframe['average'].shift(1)) & (dataframe['average'].shift(1) < dataframe['average'].shift(0)) & (dataframe['low'].shift(1) < dataframe['bb_middleband']) & (dataframe['cci'].shift(1) < -100) & (dataframe['rsi'].shift(1) < 30) & (dataframe['mfi'].shift(1) < 30) ) ) # safeguard against down trending markets and a pump and dump & ( (dataframe['volume'] < (dataframe['volume'].rolling(window=30).mean().shift(1) * 20)) & (dataframe['resample_sma'] < dataframe['close']) & (dataframe['resample_sma'].shift(1) < dataframe['resample_sma']) ) ) , 'enter_long'] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame :return: DataFrame with buy column """ dataframe.loc[ ( (dataframe['close'] > dataframe['ema_{}'.format(self.EMA_SHORT_TERM)]) & (dataframe['close'] > dataframe['ema_{}'.format(self.EMA_MEDIUM_TERM)]) & (dataframe['close'] >= dataframe['max']) & (dataframe['close'] >= dataframe['bb_upperband']) & (dataframe['mfi'] > 80) ) | # always sell on eight green candles # with a high rsi ( (dataframe['open'] < dataframe['close']) & (dataframe['open'].shift(1) < dataframe['close'].shift(1)) & (dataframe['open'].shift(2) < dataframe['close'].shift(2)) & (dataframe['open'].shift(3) < dataframe['close'].shift(3)) & (dataframe['open'].shift(4) < dataframe['close'].shift(4)) & (dataframe['open'].shift(5) < dataframe['close'].shift(5)) & (dataframe['open'].shift(6) < dataframe['close'].shift(6)) & (dataframe['open'].shift(7) < dataframe['close'].shift(7)) & (dataframe['rsi'] > 70) ) , 'exit_long' ] = 0 return dataframe def resample(self, dataframe, interval, factor): # defines the reinforcement logic # resampled dataframe to establish if we are in an uptrend, downtrend or sideways trend df = dataframe.copy() df = df.set_index(DatetimeIndex(df['date'])) ohlc_dict = { 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last' } df = df.resample(str(int(interval[:-1]) * factor) + 'min', label="right").agg(ohlc_dict).dropna(how='any') df['resample_sma'] = ta.SMA(df, timeperiod=25, price='close') df = df.drop(columns=['open', 'high', 'low', 'close']) df = df.resample(interval[:-1] + 'min') df = df.interpolate(method='time') df['date'] = df.index df.index = range(len(df)) dataframe = merge(dataframe, df, on='date', how='left') return dataframe def leverage(self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, side: str, **kwargs) -> float: return self.leverage_num.value """ Custom Stoploss """ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) last_candle = dataframe.iloc[-1].squeeze() trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60) in_trend = self.custom_trade_info[trade.pair]['had-trend'] if current_profit < self.cstop_max_stoploss.value: return 0.01 # Determine how we sell when we are in a loss if current_profit < self.cstop_loss_threshold.value: if self.cstop_bail_how.value == 'roc' or self.cstop_bail_how.value == 'any': # Dynamic bailout based on rate of change if last_candle['sroc'] <= self.cstop_bail_roc.value: return 0.01 if self.cstop_bail_how.value == 'time' or self.cstop_bail_how.value == 'any': # Dynamic bailout based on time, unless time_trend is true and there is a potential reversal if trade_dur > self.cstop_bail_time.value: if self.cstop_bail_time_trend.value and in_trend: return 1 else: return 0.01 return 1 """ Custom Sell """ def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, current_profit: float, **kwargs): dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) last_candle = dataframe.iloc[-1].squeeze() trade_dur = int((current_time.timestamp() - trade.open_date_utc.timestamp()) // 60) max_profit = max(0, trade.calc_profit_ratio(trade.max_rate)) pullback_value = max(0, (max_profit - self.csell_pullback_amount.value)) in_trend = False # Determine our current ROI point based on the defined type if self.csell_roi_type.value == 'static': min_roi = self.csell_roi_start.value elif self.csell_roi_type.value == 'decay': min_roi = linear_decay(self.csell_roi_start.value, self.csell_roi_end.value, 0, self.csell_roi_time.value, trade_dur) elif self.csell_roi_type.value == 'step': if trade_dur < self.csell_roi_time.value: min_roi = self.csell_roi_start.value else: min_roi = self.csell_roi_end.value # Determine if there is a trend if self.csell_trend_type.value == 'rmi' or self.csell_trend_type.value == 'any': if last_candle['rmi-up-trend'] == 1: in_trend = True if self.csell_trend_type.value == 'ssl' or self.csell_trend_type.value == 'any': if last_candle['ssl-dir'] == 'up': in_trend = True if self.csell_trend_type.value == 'candle' or self.csell_trend_type.value == 'any': if last_candle['candle-up-trend'] == 1: in_trend = True # Don't sell if we are in a trend unless the pullback threshold is met if in_trend and current_profit > 0: # Record that we were in a trend for this trade/pair for a more useful sell message later self.custom_trade_info[trade.pair]['had-trend'] = True # If pullback is enabled and profit has pulled back allow a sell, maybe if self.csell_pullback.value and (current_profit <= pullback_value): if self.csell_pullback_respect_roi.value and current_profit > min_roi: return 'intrend_pullback_roi' elif not self.csell_pullback_respect_roi.value: if current_profit > min_roi: return 'intrend_pullback_roi' else: return 'intrend_pullback_noroi' # We are in a trend and pullback is disabled or has not happened or various criteria were not met, hold return None # If we are not in a trend, just use the roi value elif not in_trend: if self.custom_trade_info[trade.pair]['had-trend']: if current_profit > min_roi: self.custom_trade_info[trade.pair]['had-trend'] = False return 'trend_roi' elif not self.csell_endtrend_respect_roi.value: self.custom_trade_info[trade.pair]['had-trend'] = False return 'trend_noroi' elif current_profit > min_roi: return 'notrend_roi' else: return None