# --- Do not remove these libs --- from freqtrade.strategy import IStrategy, CategoricalParameter, DecimalParameter, IntParameter from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib # -------------------------------- # Strategy for Freqtrade bot # # Bollinger Bands Crossover Strategy with Trailing Stop-Loss # # Author: Jules (AI Assistant) # Version: 1.0 # # Entry Conditions: # - Long: Price closes above the lower Bollinger Band (after being below or on it). # - Short: Price closes below the upper Bollinger Band (after being above or on it). # # Exit Conditions: # - Take Profit: 1.5% (full position). # - Stop-Loss: # - Initial: 1% from entry price. # - Trailing: Activates after +1% profit. Stop trails 0.5% behind the peak profit. # # -------------------------------- class BBTrailSimpleStrategy(IStrategy): """ Bollinger Bands Crossover Strategy with Trailing Stop-Loss """ # Strategy interface version - attribute needed by Freqtrade INTERFACE_VERSION = 3 # ROI table: # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi". minimal_roi = { "0": 0.015 # Take profit at 1.5% } # Stoploss: # Default stoploss for this strategy, pourcentage is expressed as a decimal. # This attribute will be overridden if the config file contains "stoploss". stoploss = -0.01 # Initial stoploss at -1% # Trailing stop: # Trailing stoploss for this strategy, pourcentage is expressed as a decimal. # This attribute will be overridden if the config file contains "trailing_stop". trailing_stop = True trailing_stop_positive = 0.005 # Trail by 0.5% trailing_stop_positive_offset = 0.01 # Start trailing after 1% profit trailing_only_offset_is_reached = True # Wait for +1% profit before trailing activates # Optimal timeframe for the strategy. timeframe = '5m' # Default, user can change this # Can this strategy go short? can_short = True # Run "populate_indicators()" only for new candle. process_only_new_candles = True # These values can be overridden in the config. use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False # Optional order type mapping. order_types = { 'entry': 'limit', 'exit': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': False } # Optional order time in force. order_time_in_force = { 'entry': 'gtc', 'exit': 'gtc' } # --- Indicator Definitions --- # Bollinger Bands buy_bb_period = IntParameter(10, 50, default=20, space="buy") buy_bb_stddev = DecimalParameter(1.0, 3.0, default=2.0, space="buy") sell_bb_period = IntParameter(10, 50, default=20, space="sell") sell_bb_stddev = DecimalParameter(1.0, 3.0, default=2.0, space="sell") def informative_pairs(self): return [] def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame Performance Note: For the best performance be frugal on the number of indicators and try to use confirmed TA-Lib indicators. """ # Bollinger Bands - Buy bollinger_buy = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=self.buy_bb_period.value, stds=self.buy_bb_stddev.value) dataframe['bb_lowerband_buy'] = bollinger_buy['lower'] dataframe['bb_middleband_buy'] = bollinger_buy['mid'] dataframe['bb_upperband_buy'] = bollinger_buy['upper'] # Bollinger Bands - Sell (for shorting) # Potentially different parameters for shorting if desired, otherwise use buy params if self.can_short: bollinger_sell = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=self.sell_bb_period.value, stds=self.sell_bb_stddev.value) dataframe['bb_lowerband_sell'] = bollinger_sell['lower'] dataframe['bb_middleband_sell'] = bollinger_sell['mid'] dataframe['bb_upperband_sell'] = bollinger_sell['upper'] # Other indicators can be added here return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the entry tínals for the given dataframe :param dataframe: DataFrame :param metadata: Additional information, like current pair :return: DataFrame with entry columns populated """ # Long Entry: Price crosses above the lower Bollinger Band dataframe.loc[ ( qtpylib.crossed_above(dataframe['close'], dataframe['bb_lowerband_buy']) ), ['enter_long', 'enter_tag']] = (1, 'bb_cross_lower_long') if self.can_short: # Short Entry: Price crosses below the upper Bollinger Band dataframe.loc[ ( qtpylib.crossed_below(dataframe['close'], dataframe['bb_upperband_sell']) ), ['enter_short', 'enter_tag']] = (1, 'bb_cross_upper_short') return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the exit signals for the given dataframe :param dataframe: DataFrame :param metadata: Additional information, like current pair :return: DataFrame with exit columns populated """ # No custom exit signals beyond ROI, stoploss, and trailing stoploss defined in class variables. # If you wanted custom exit signals, you would define them here. # For example: # dataframe.loc[ # ( # (qtpylib.crossed_above(dataframe['close'], dataframe['bb_upperband_buy'])) & # Long exit condition # (dataframe['volume'] > 0) # Make sure Volume is not 0 # ), # ['exit_long', 'exit_tag']] = (1, 'bb_exit_long_custom') # if self.can_short: # dataframe.loc[ # ( # (qtpylib.crossed_below(dataframe['close'], dataframe['bb_lowerband_sell'])) & # Short exit condition # (dataframe['volume'] > 0) # Make sure Volume is not 0 # ), # ['exit_short', 'exit_tag']] = (1, 'bb_exit_short_custom') return dataframe # Note: custom_stoploss can be used if Freqtrade's native trailing stop isn't precise enough. # For this version, we rely on the native trailing_stop, trailing_stop_positive, # and trailing_stop_positive_offset / trailing_only_offset_is_reached. # If these do not provide the exact 0.5% trail from peak after 1% profit, # then custom_stoploss would be the place to implement it. # Example of custom_stoploss structure if needed later: # def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, # current_rate: float, current_profit: float, **kwargs) -> float: # """ # Custom stoploss logic, called only if positive_stoploss is enabled. # # For this strategy: # - Initial stop: -1% (handled by `stoploss = -0.01`) # - Trailing stop: Activates at +1% profit. Trails by 0.5% from the highest rate achieved. # # :param pair: Pair that's currently analyzed # :param trade: trade object. # :param current_time: datetime object, containing the current datetime # :param current_rate: Current rate of the pair # :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: New stoploss value, relative to the current rate # If None is returned, original stoploss value is used. # """ # # Hard stoploss at -10% to prevent catastrophic loss from bad data / market manipulation # # This is not strictly part of the user's request but good practice. # # if current_profit < -0.10: # # return -0.99 # Effectively market sell immediately, or a fixed large stop. # # This should be handled by stoploss = -0.01 and Freqtrade's default stoploss mechanism. # # The custom_stoploss is more for dynamic/trailing stops. # # # Initial stoploss is -0.01 (1%), handled by the main `stoploss` parameter. # # This function is primarily for the trailing stop logic if native options are insufficient. # # if current_profit > 0.01: # If profit is > 1% # # Calculate the desired stop price: 0.5% below the highest rate since 1% profit was achieved. # # Freqtrade's `trade.max_rate` holds the highest rate observed for the trade. # desired_stop_price = trade.max_rate * (1 - 0.005) # For longs # if trade.is_short: # desired_stop_price = trade.max_rate * (1 + 0.005) # For shorts # # # Convert this absolute price back to a relative stoploss from current_rate # # (stoploss_value = (stop_price / current_rate) - 1) # # Important: custom_stoploss returns a *rate* relative to open_rate or an *absolute price*. # # Let's return an absolute price for the stop. # return desired_stop_price # # # If profit is not yet > 1%, the initial `stoploss = -0.01` applies. # # Returning -1 or None from here would mean "use `stoploss` parameter". # # However, to be explicit, if we are not yet in trailing mode, # # we don't want to override the initial stoploss. # # Freqtrade's `custom_stoploss` is called frequently. # # The `stoploss` parameter defines the initial stop. # # `trailing_stop` parameters should handle the rest. # # If they don't, then this function needs to be more robust. # # # If we rely on native trailing, this function might not even be needed # # or could simply return -1 (or a value that refers to initial stoploss). # # For now, let's assume native trailing works as specified. # # If it doesn't, this is where the logic would go. # return -1 # Indicates to use the stoploss from the configuration # Note on Hyperopt: # The IntParameter and DecimalParameter for BB period and stddev are already set up # for hyperoptimization. Other parameters like timeframe, stoploss values, # and ROI targets can also be made into hyperoptable parameters if desired. # For example: # trailing_stop_positive_sell = DecimalParameter(0.001, 0.02, default=0.005, space="sell", optimize=True) # trailing_stop_positive_offset_sell = DecimalParameter(0.005, 0.03, default=0.01, space="sell", optimize=True) # minimal_roi_param = CategoricalParameter([{"0": 0.01}, {"0": 0.015}, {"0": 0.02}], default={"0": 0.015}, space="buy", optimize=True) # stoploss_param = DecimalParameter(-0.05, -0.005, default=-0.01, space="buy", optimize=True) # --- Custom methods for easier access to parameters --- @property def protections(self): return [ { "method": "StoplossGuard", "lookback_period_candles": 24, # Check for X last candles "trade_limit": 2, # Avoid entering X trades in the lookback period "stop_duration_candles": 6, # Don't enter new trades for X candles "only_per_pair": False # Look at all pairs, or only per pair } ] # --- HYPEROPT PARAMETERS --- # Define buy and sell spaces if you want to hyperopt them separately. # Otherwise, parameters defined directly in the class are used for both. # Example of defining separate hyperopt spaces for buy and sell # class HyperOpt: # @staticmethod # def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: # # Bollinger Bands # bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), # window=20, # Example fixed or from common space # stds=2) # Example fixed or from common space # dataframe['bb_lowerband'] = bollinger['lower'] # dataframe['bb_middleband'] = bollinger['mid'] # dataframe['bb_upperband'] = bollinger['upper'] # return dataframe # @staticmethod # def buy_strategy_generator(params: dict) -> Callable: # def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: # dataframe.loc[ # ( # qtpylib.crossed_above(dataframe['close'], dataframe['bb_lowerband']) & # (dataframe['bb_period'] == params['bb_period']) # Hypothetical use of param # ), # 'buy'] = 1 # return dataframe # return populate_buy_trend # @staticmethod # def indicator_space() -> list: # For common indicators if needed # return [ # IntParameter(10, 50, default=20, name='bb_period'), # DecimalParameter(1.0, 3.0, default=2.0, name='bb_stddev'), # ] # @staticmethod # def sell_indicator_space() -> list: # If sell indicators are different # return [ # IntParameter(10, 50, default=20, name='sell_bb_period'), # DecimalParameter(1.0, 3.0, default=2.0, name='sell_bb_stddev'), # ] # If you want to use the custom_stoploss, uncomment it and ensure it's correctly implemented. # For now, relying on Freqtrade's built-in trailing stop mechanisms. # If the built-in trailing stop (`trailing_stop = True`, `trailing_stop_positive`, # `trailing_stop_positive_offset`, `trailing_only_offset_is_reached = True`) # correctly implements the "trail by 0.5% after 1% profit" logic, then `custom_stoploss` is not strictly needed # for that part. The initial `stoploss = -0.01` handles the first static stop. # The `custom_stoploss` function is called only if `trailing_stop` is True AND # `use_custom_stoploss` is True. # However, modern Freqtrade might use `custom_stoploss` if defined, even without `use_custom_stoploss`. # The interaction is: # 1. `stoploss` (initial fixed stop). # 2. If `trailing_stop` is True: # - If `custom_stoploss` is defined, it can override/provide dynamic stop values. # - `trailing_stop_positive*` parameters configure built-in trailing if `custom_stoploss` doesn't take full control or isn't used. # Given the requirements: # - Initial 1% stop: `stoploss = -0.01` # - After +1% profit, trail by 0.5%: # `trailing_stop = True` # `trailing_stop_positive = 0.005` (the amount to trail by) # `trailing_stop_positive_offset = 0.01` (the profit threshold to activate trailing) # `trailing_only_offset_is_reached = True` (ensure trailing only starts after offset is hit) # This setup *should* work as intended. `custom_stoploss` can be a fallback or for more complex scenarios. pass