import logging import numpy as np import pandas as pd from technical import qtpylib from pandas import DataFrame from datetime import datetime, timezone from typing import Optional from functools import reduce import talib.abstract as ta import pandas_ta as pta from freqtrade.strategy import BooleanParameter, CategoricalParameter, DecimalParameter, IStrategy, IntParameter, RealParameter, merge_informative_pair import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.persistence import Trade from technical import qtpylib, pivots_points class cryptotankV2(IStrategy): INTERFACE_VERSION = 3 use_custom_stoploss = True trailing_stop = True ignore_roi_if_entry_signal = True use_exit_signal = True minimal_roi = {'0': 0.1} # DCA settings exit_profit_only = True position_adjustment_enable = True max_entry_position_adjustment = 0 max_dca_multiplier = 1 stoploss = -0.25 timeframe = '5m' # Number of candles the strategy requires before producing valid signals startup_candle_count: int = 30 ### HYPER-OPT PARAMETERS ### # protections cooldown_lookback = IntParameter(2, 48, default=5, space='protection', optimize=True) stop_duration = IntParameter(1, 36, default=5, space='protection', optimize=True) use_stop_protection = BooleanParameter(default=True, space='protection', optimize=True) # entry optimization # max_epa = CategoricalParameter([0, 1], default=0, space="entry", optimize=True) # indicators entry_rsi_length = IntParameter(14, 16, default=14, space='entry', optimize=True) entry_rsi_ma_length = IntParameter(10, 14, default=14, space='entry', optimize=True) reference_ma_length = IntParameter(180, 200, default=200, space='entry', optimize=True) smoothing_length = IntParameter(25, 50, default=35, space='entry', optimize=True) filterlength = IntParameter(low=8, high=40, default=35, space='exit', optimize=True) entry_offset1 = DecimalParameter(low=0.9, high=0.99, decimals=2, default=0.96, space='entry', optimize=True, load=True) entry_offset2 = DecimalParameter(low=0.9, high=0.96, decimals=2, default=0.94, space='entry', optimize=True, load=True) exit_offset = DecimalParameter(low=1.01, high=1.1, decimals=2, default=1.08, space='exit', optimize=True, load=True) #trailing stop loss optimiziation tsl_target5 = DecimalParameter(low=0.15, high=0.2, decimals=2, default=0.3, space='exit', optimize=True, load=True) ts5 = DecimalParameter(low=0.04, high=0.06, default=0.05, decimals=2, space='exit', optimize=True, load=True) tsl_target4 = DecimalParameter(low=0.1, high=0.15, default=0.2, decimals=2, space='exit', optimize=True, load=True) ts4 = DecimalParameter(low=0.03, high=0.05, default=0.045, decimals=2, space='exit', optimize=True, load=True) tsl_target3 = DecimalParameter(low=0.08, high=0.1, default=0.15, decimals=2, space='exit', optimize=True, load=True) ts3 = DecimalParameter(low=0.025, high=0.04, default=0.035, decimals=3, space='exit', optimize=True, load=True) tsl_target2 = DecimalParameter(low=0.06, high=0.8, default=0.1, decimals=3, space='exit', optimize=True, load=True) ts2 = DecimalParameter(low=0.015, high=0.03, default=0.02, decimals=3, space='exit', optimize=True, load=True) tsl_target1 = DecimalParameter(low=0.04, high=0.06, default=0.06, decimals=3, space='exit', optimize=True, load=True) ts1 = DecimalParameter(low=0.01, high=0.016, default=0.013, decimals=3, space='exit', optimize=True, load=True) tsl_target0 = DecimalParameter(low=0.02, high=0.04, default=0.03, decimals=3, space='exit', optimize=True, load=True) ts0 = DecimalParameter(low=0.005, high=0.015, default=0.013, decimals=3, space='exit', optimize=True, load=True) ### entrying # rsi entry_rsi_upper = IntParameter(55, 65, default=55, space='entry', optimize=True) entry_rsi_lower = IntParameter(20, 35, default=35, space='entry', optimize=True) entry_rsi_ma_slope = DecimalParameter(-0.01, 0.1, default=0, decimals=2, space='entry', optimize=True) entry_ma_slope = DecimalParameter(-0.1, 0.1, default=0, decimals=2, space='entry', optimize=True) entry_lower = DecimalParameter(-10, 0, default=-2, decimals=1, space='entry', optimize=True) entry_rsi_dip = IntParameter(30, 40, default=35, space='entry', optimize=True) entry_dip_x = DecimalParameter(0.5, 2.0, default=1.5, decimals=1, space='entry', optimize=True) ### exiting # # rsi # exit_rsi_upper = IntParameter(80, 100, default=80, space="exit", optimize=True) # exit_rsi_lower = IntParameter(50, 79, default=55, space="exit", optimize=True) # exit_ma_slope = IntParameter(-10, 10, default=0, space="exit", optimize=True) # { # "main_plot": { # "enter_tag": { # "color": "#6fc96b", # "type": "line" # }, # "exit_tag": { # "color": "#d51a44", # "type": "line" # }, # "reference_ma_198": { # "color": "#dacd51", # "type": "line" # }, # "entry_offset2": { # "color": "#289531", # "type": "line" # }, # "entry_offset1": { # "color": "#289531", # "type": "line" # }, # "exit_offset": { # "color": "#e93c22", # "type": "line" # } # }, # "subplots": { # "RSI": { # "entry_rsi_18": { # "color": "#9262a5", # "type": "line" # }, # "entry_rsi_ma_5": { # "color": "#bddc16", # "type": "line" # } # }, # "Change": { # "change": { # "color": "#ec612c", # "type": "line" # }, # "smooth_change_11": { # "color": "#357cf9", # "type": "line" # } # }, # "SLOPE": { # "smooth_ma_slope": { # "color": "#5fc631", # "type": "line" # } # } # } # } @property def protections(self): prot = [] prot.append({'method': 'CooldownPeriod', 'stop_duration_candles': self.cooldown_lookback.value}) if self.use_stop_protection.value: prot.append({'method': 'StoplossGuard', 'lookback_period_candles': 24 * 3, 'trade_limit': 1, 'stop_duration_candles': self.stop_duration.value, 'only_per_pair': False}) return prot # @property # def max_entry_position_adjustment(self): # return self.max_epa.value ### Dollar Cost Averaging ### # This is called when placing the initial order (opening trade) def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: Optional[float], max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: # We need to leave most of the funds for possible further DCA orders # This also applies to fixed stakes return proposed_stake / self.max_dca_multiplier def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs) -> Optional[float]: if current_profit > 0.1 and current_profit < 0.15 and (trade.nr_of_successful_exits == 0): # Take 50% of the profit at +5% return -(trade.stake_amount / 2) if current_profit > -0.03 and trade.nr_of_successful_entries == 1: return None if current_profit > -0.05 and trade.nr_of_successful_entries == 2: return None if current_profit > -0.6 and trade.nr_of_successful_entries == 3: return None # # Obtain pair dataframe (just to show how to access it) # dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) # # Only entry when not actively falling price. # last_candle = dataframe.iloc[-1].squeeze() # previous_candle = dataframe.iloc[-2].squeeze() # if last_candle['close'] < previous_candle['close']: # return None filled_entries = trade.select_filled_orders(trade.entry_side) count_of_entries = trade.nr_of_successful_entries # Allow up to 3 additional increasingly larger entrys (4 in total) # Initial entry is 1x # If that falls to -5% profit, we entry 1.25x more, average profit should increase to roughly -2.2% # If that falls down to -5% again, we entry 1.5x more # If that falls once again down to -5%, we entry 1.75x more # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. # Total stake for this trade would be 1 + 1.5 + 2 + 2.5 = 5.5x of the initial allowed stake. # That is why max_dca_multiplier is 5.5 # Hope you have a deep wallet! try: # This returns first order stake size stake_amount = filled_entries[0].cost # This then calculates current safety order size stake_amount = stake_amount * (1 + count_of_entries * 0.5) return stake_amount except Exception as exception: return None return None ### Trailing Stop ### def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: for stop5 in self.tsl_target5.range: if current_profit > stop5: for stop5a in self.ts5.range: self.dp.send_msg(f'*** {pair} *** Profit: {current_profit} - lvl5 {stop5}/{stop5a} activated') return stop5a for stop4 in self.tsl_target4.range: if current_profit > stop4: for stop4a in self.ts4.range: self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl4 {stop4}/{stop4a} activated') return stop4a for stop3 in self.tsl_target3.range: if current_profit > stop3: for stop3a in self.ts3.range: self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl3 {stop3}/{stop3a} activated') return stop3a for stop2 in self.tsl_target2.range: if current_profit > stop2: for stop2a in self.ts2.range: self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl2 {stop2}/{stop2a} activated') return stop2a for stop1 in self.tsl_target1.range: if current_profit > stop1: for stop1a in self.ts1.range: self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl1 {stop1}/{stop1a} activated') return stop1a for stop0 in self.tsl_target0.range: if current_profit > stop0: for stop0a in self.ts0.range: self.dp.send_msg(f'*** {pair} *** Profit {current_profit} - lvl0 {stop0}/{stop0a} activated') return stop0a return self.stoploss def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """Generate all indicators used by the strategy""" # Pivot Points pivots = pivots_points.pivots_points(dataframe, timeperiod=50, levels=5) dataframe['pivot'] = pivots['pivot'] dataframe['s5'] = pivots['s5'] dataframe['r5'] = pivots['r5'] dataframe['s3'] = pivots['s3'] dataframe['r3'] = pivots['r3'] dataframe['s2'] = pivots['s2'] dataframe['r2'] = pivots['r2'] dataframe['r3-dif'] = (dataframe['r3'] - dataframe['r2']) / 4 dataframe['r2.25'] = dataframe['r2'] + dataframe['r3-dif'] dataframe['r2.50'] = dataframe['r2'] + dataframe['r3-dif'] * 2 dataframe['r2.75'] = dataframe['r2'] + dataframe['r3-dif'] * 3 # Filter ZEMA for length in self.filterlength.range: dataframe[f'ema_1{length}'] = ta.EMA(dataframe['close'], timeperiod=length) dataframe[f'ema_2{length}'] = ta.EMA(dataframe[f'ema_1{length}'], timeperiod=length) dataframe[f'ema_dif{length}'] = dataframe[f'ema_1{length}'] - dataframe[f'ema_2{length}'] dataframe[f'zema_{length}'] = dataframe[f'ema_1{length}'] + dataframe[f'ema_dif{length}'] # RSI # Calculate all rsi_entry values for valb in self.entry_rsi_length.range: dataframe[f'entry_rsi_{valb}'] = ta.RSI(dataframe, timeperiod=valb) # Calculate all rsi_entry ma values for valbm in self.entry_rsi_ma_length.range: dataframe[f'entry_rsi_ma_{valbm}'] = ta.SMA(dataframe[f'entry_rsi_{valb}'], timeperiod=valbm) dataframe['rsi_ma_slope'] = pta.momentum.slope(dataframe[f'entry_rsi_ma_{valbm}']) # % from reference MA for valma in self.reference_ma_length.range: dataframe[f'reference_ma_{valma}'] = ta.SMA(dataframe['close'], timeperiod=valma) # distance from reference ma to current close dataframe['change'] = (dataframe['close'] - dataframe[f'reference_ma_{valma}']) / dataframe['close'] * 100 dataframe['entry_offset1'] = dataframe[f'reference_ma_{valma}'] * self.entry_offset1.value dataframe['entry_offset2'] = dataframe[f'reference_ma_{valma}'] * self.entry_offset2.value dataframe['exit_offset'] = dataframe[f'reference_ma_{valma}'] * self.exit_offset.value for valsma in self.smoothing_length.range: dataframe[f'smooth_change_{valsma}'] = ta.SMA(dataframe['change'], timeperiod=valsma) dataframe['smooth_ma_slope'] = pta.momentum.slope(dataframe[f'smooth_change_{valsma}']) return dataframe def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: conditions = [] # df.loc[ # ( # (qtpylib.crossed_above(df['change'], df[f'smooth_change_{self.smoothing_length.value}'])) & # (df[f'entry_rsi_{self.entry_rsi_length.value}'] < self.entry_rsi_upper.value) & # (df['volume'] > 0) # Make sure Volume is not 0 # ), # ['enter_long', 'enter_tag']] = (1, 'XO ') # Make sure Volume is not 0 df.loc[qtpylib.crossed_above(df['change'], df[f'smooth_change_{self.smoothing_length.value}']) & (df['smooth_ma_slope'] > self.entry_ma_slope.value) & (df['close'] < df[f'reference_ma_{self.reference_ma_length.value}'] * self.exit_offset.value) & (df[f'smooth_change_{self.smoothing_length.value}'] > 0) & (df[f'rsi_ma_slope'] > self.entry_rsi_ma_slope.value) & (df['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'XO Above 0') #WIP # Make sure Volume is not 0 df.loc[qtpylib.crossed_above(df['change'], df[f'smooth_change_{self.smoothing_length.value}']) & (df['close'] < df[f'reference_ma_{self.reference_ma_length.value}']) & (df[f'smooth_change_{self.smoothing_length.value}'] < self.entry_lower.value * 2) & (df[f'entry_rsi_{self.entry_rsi_length.value}'] < self.entry_rsi_dip.value) & (df['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'XO Below Lower Entry * 2') # df.loc[ # ( # (qtpylib.crossed_above(df['change'], df[f'smooth_change_{self.smoothing_length.value}'])) & # (df['smooth_ma_slope'] > self.entry_ma_slope.value) & # (df[f'smooth_change_{self.smoothing_length.value}'] < self.entry_lower.value) & # (df['close'] < df[f'reference_ma_{self.reference_ma_length.value}'] * self.entry_offset1.value) & # (df['volume'] > 0) # Make sure Volume is not 0 # ), # ['enter_long', 'enter_tag']] = (1, 'XO Below Lower and OFFSET1') # df.loc[ # ( # (qtpylib.crossed_above(df['change'], df[f'smooth_change_{self.smoothing_length.value}'])) & # (df['smooth_ma_slope'] > self.entry_ma_slope.value) & # (df[f'smooth_change_{self.smoothing_length.value}'] < self.entry_lower.value) & # (df['close'] < df[f'reference_ma_{self.reference_ma_length.value}'] * self.entry_offset2.value) & # (df['volume'] > 0) # Make sure Volume is not 0 # ), # ['enter_long', 'enter_tag']] = (1, 'XO Below Lower and OFFSET2') # Make sure Volume is not 0 df.loc[qtpylib.crossed_above(df[f'entry_rsi_{self.entry_rsi_length.value}'], df[f'entry_rsi_ma_{self.entry_rsi_ma_length.value}']) & (df[f'entry_rsi_{self.entry_rsi_length.value}'] < self.entry_rsi_dip.value) & (df['change'] < df[f'smooth_change_{self.smoothing_length.value}'] - self.entry_dip_x.value) & (df['change'] < self.entry_lower.value) & (df['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'RSI XO | Change < Smooth + Dip < 0') # r3.. # Make sure Volume is not 0 df.loc[qtpylib.crossed_above(df['close'], df['r3']) & (df['change'] > df[f'smooth_change_{self.smoothing_length.value}']) & (df[f'entry_rsi_{self.entry_rsi_length.value}'] < self.entry_rsi_upper.value) & (df['close'] > df[f'reference_ma_{self.reference_ma_length.value}']) & (df['change'] > 0) & (df['change'] > df['change'].shift(1)) & (df[f'entry_rsi_{self.entry_rsi_length.value}'] > df[f'entry_rsi_{self.entry_rsi_length.value}'].shift(1)) & (df[f'entry_rsi_ma_{self.entry_rsi_ma_length.value}'] > df[f'entry_rsi_ma_{self.entry_rsi_ma_length.value}'].shift(1)) & (df['volume'] > 0), ['enter_long', 'enter_tag']] = (1, 'MEGA PUMP') # df.loc[ # ( # (df[f'entry_rsi_{self.entry_rsi_length.value}'] < self.entry_rsi_dip.value) & # (df['close'] < (df[f'reference_ma_{self.reference_ma_length.value}'] * self.entry_offset2.value)) & # (df['change'] < self.entry_lower.value) & # (df['volume'] > 0) # Make sure Volume is not 0 # ), # ['enter_long', 'enter_tag']] = (1, 'MEGA DUMP') return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: # Make sure Volume is not 0 df.loc[qtpylib.crossed_below(df[f'zema_{self.filterlength.value}'], df['r5']) & (df['close'] > df[f'reference_ma_{self.reference_ma_length.value}'] * self.exit_offset.value) & (df['volume'] > 0), ['exit_long', 'exit_tag']] = (1, 'R5 - XO') # Make sure Volume is not 0 df.loc[qtpylib.crossed_below(df[f'zema_{self.filterlength.value}'], df['r3']) & (df['volume'] > 0), ['exit_long', 'exit_tag']] = (1, 'R3 - XO') # Make sure Volume is not 0 df.loc[qtpylib.crossed_below(df[f'zema_{self.filterlength.value}'], df['r2.75']) & (df['volume'] > 0), ['exit_long', 'exit_tag']] = (1, 'R2.75 - XO') # Make sure Volume is not 0 df.loc[qtpylib.crossed_below(df[f'zema_{self.filterlength.value}'], df['r2.50']) & (df['volume'] > 0), ['exit_long', 'exit_tag']] = (1, 'R2.5 - XO') # Make sure Volume is not 0 df.loc[qtpylib.crossed_below(df[f'zema_{self.filterlength.value}'], df['r2.25']) & (df['volume'] > 0), ['exit_long', 'exit_tag']] = (1, 'R2.25 - XO') # Make sure Volume is not 0 df.loc[qtpylib.crossed_below(df[f'zema_{self.filterlength.value}'], df['r2']) & (df['volume'] > 0), ['exit_long', 'exit_tag']] = (1, 'R2 - XO') return df # { # "strategy_name": "cryptotankV2", # "params": { # "trailing": { # "trailing_stop": true, # "trailing_stop_positive": null, # "trailing_stop_positive_offset": 0.0, # "trailing_only_offset_is_reached": false # }, # "entry": { # "entry_ma_slope": -10, # "entry_offset1": 0.92, # "entry_offset2": 0.92, # "entry_rsi_length": 5, # "entry_rsi_lower": 33, # "entry_rsi_ma_length": 24, # "entry_rsi_upper": 64, # "filterlength": 30, # "max_epa": 1, # "reference_ma_length": 194, # "smoothing_length": 6 # }, # "exit": { # "exit_ma_slope": -8, # "exit_offset": 1.05, # "exit_rsi_lower": 77, # "exit_rsi_upper": 98, # "ts0": 0.01, # "ts1": 0.015, # "ts2": 0.026, # "ts3": 0.03, # "ts4": 0.05, # "ts5": 0.04, # "tsl_target0": 0.042, # "tsl_target1": 0.073, # "tsl_target2": 0.087, # "tsl_target3": 0.14, # "tsl_target4": 0.2, # "tsl_target5": 0.3 # }, # "protection": { # "cooldown_lookback": 27, # "stop_duration": 32, # "use_stop_protection": true # }, # "roi": { # "0": 0.45299999999999996, # "1268": 0.34199999999999997, # "7051": 0.13, # "21126": 0 # }, # "stoploss": { # "stoploss": -0.261 # } # }, # "ft_stratparam_v": 1, # "export_time": "2023-04-11 21:35:29.779280+00:00" # } # ================== SUMMARY METRICS ================== # | Metric | Value | # |-----------------------------+---------------------| # | Backtesting from | 2023-01-01 00:00:00 | # | Backtesting to | 2023-04-17 00:00:00 | # | Max open trades | 3 | # | | | # | Total/Daily Avg Trades | 164 / 1.55 | # | Starting balance | 1000 USDT | # | Final balance | 3817.038 USDT | # | Absolute profit | 2817.038 USDT | # | Total profit % | 281.70% | # | CAGR % | 9971.97% | # | Profit factor | 4.70 | # | Trades per day | 1.55 | # | Avg. daily profit % | 2.66% | # | Avg. stake amount | 722.729 USDT | # | Total trade volume | 118527.56 USDT | # | | | # | Best Pair | AKT/USDT 54.28% | # | Worst Pair | ATOM/USDT -18.78% | # | Best trade | RNDR/USDT 9.99% | # | Worst trade | ATOM/USDT -23.16% | # | Best day | 184.782 USDT | # | Worst day | -237.53 USDT | # | Days win/draw/lose | 66 / 34 / 4 | # | Avg. Duration Winners | 1 day, 5:50:00 | # | Avg. Duration Loser | 14 days, 19:38:00 | # | Rejected Entry signals | 270163 | # | Entry/Exit Timeouts | 0 / 0 | # | | | # | Min balance | 1000.968 USDT | # | Max balance | 3876.021 USDT | # | Max % of account underwater | 15.71% | # | Absolute Drawdown (Account) | 15.71% | # | Absolute Drawdown | 534.177 USDT | # | Drawdown high | 2401.028 USDT | # | Drawdown low | 1866.851 USDT | # | Drawdown Start | 2023-03-01 13:45:00 | # | Drawdown End | 2023-03-11 15:45:00 | # | Market change | 114.36% | # =====================================================