# Auto-generated by Strategy Lab — Auto-Quant Factory (Omni-Strategy Edition) # One massive template with Boolean switches for every indicator. # Hyperopt mutates True/False switches + numeric thresholds to find the best combo. # Optimise with: freqtrade hyperopt --strategy HermesTestStrategy --spaces buy roi stoploss from __future__ import annotations from datetime import datetime from functools import reduce import operator from freqtrade.strategy import CategoricalParameter, IntParameter, DecimalParameter, IStrategy, Trade, stoploss_from_open from pandas import DataFrame import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib import numpy as np class HermesTestStrategy(IStrategy): INTERFACE_VERSION: int = 3 minimal_roi = {"0": 0.03, "10": 0.02, "30": 0.01, "60": 0} stoploss = -0.025 timeframe = "5m" trailing_stop = False use_custom_stoploss = True process_only_new_candles = True # ── Boolean indicator switches (True = active, False = ignored) ─────────── use_rsi = CategoricalParameter([True, False], default=True, space="buy", optimize=True) use_macd = CategoricalParameter([True, False], default=True, space="buy", optimize=True) use_bb = CategoricalParameter([True, False], default=True, space="buy", optimize=True) use_ema_cross = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_adx = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_atr = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_stoch = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_cci = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_mfi = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_willr = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_obv = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_sar = CategoricalParameter([True, False], default=False, space="buy", optimize=True) use_natr = CategoricalParameter([True, False], default=False, space="buy", optimize=True) # ── Per-indicator numeric thresholds ───────────────────────────────────── rsi_threshold = IntParameter(20, 50, default=30, space="buy", optimize=True) adx_threshold = IntParameter(15, 40, default=25, space="buy", optimize=True) macd_hist_min = DecimalParameter(0.0, 0.005, default=0.0, decimals=4, space="buy", optimize=True) bb_factor = DecimalParameter(0.95, 1.0, default=1.0, decimals=3, space="buy", optimize=True) ema_fast_p = IntParameter(5, 30, default=50, space="buy", optimize=False) ema_slow_p = IntParameter(50, 250, default=200, space="buy", optimize=False) stoch_threshold = IntParameter(20, 40, default=30, space="buy", optimize=True) cci_threshold = IntParameter(-100, -50, default=-100, space="buy", optimize=True) mfi_threshold = IntParameter(20, 40, default=30, space="buy", optimize=True) willr_threshold = IntParameter(-80, -60, default=-80, space="buy", optimize=True) # ── ATR regime window ───────────────────────────────────────────────────── atr_window = IntParameter(10, 60, default=20, space="buy", optimize=True) # ── Stepped trailing profit lock-in tiers ───────────────────────────────── ts_tier1_trigger = DecimalParameter(0.020, 0.040, default=0.030, decimals=3, space="buy", optimize=True) ts_tier1_lock = DecimalParameter(0.001, 0.010, default=0.003, decimals=3, space="buy", optimize=True) ts_tier2_trigger = DecimalParameter(0.050, 0.080, default=0.060, decimals=3, space="buy", optimize=True) ts_tier2_lock = DecimalParameter(0.030, 0.050, default=0.035, decimals=3, space="buy", optimize=True) ts_tier3_trigger = DecimalParameter(0.100, 0.180, default=0.120, decimals=3, space="buy", optimize=True) ts_tier3_lock = DecimalParameter(0.060, 0.100, default=0.080, decimals=3, space="buy", optimize=True) # ── ATR-based position sizing for winning pairs ─────────────────────────── # ATR dictionary for each winning pair (pair -> ATR value) atr_dict = {} target_risk_pct = 0.02 # 2% risk per trade # ── Trading window filter (Session Optimizer) ─────────────────────────── # Blocked hours and days based on historical performance analysis blocked_hours = [] # e.g., [0, 1, 2, 3, 4, 5] for early morning UTC blocked_days = [] # e.g., [5, 6] for weekends (0=Monday, 6=Sunday) def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> float | None: if current_profit >= self.ts_tier3_trigger.value: return stoploss_from_open( self.ts_tier3_lock.value, current_profit, is_short=trade.is_short, leverage=trade.leverage, ) or 1 if current_profit >= self.ts_tier2_trigger.value: return stoploss_from_open( self.ts_tier2_lock.value, current_profit, is_short=trade.is_short, leverage=trade.leverage, ) or 1 if current_profit >= self.ts_tier1_trigger.value: return stoploss_from_open( self.ts_tier1_lock.value, current_profit, is_short=trade.is_short, leverage=trade.leverage, ) or 1 return None def custom_stake_amount(self, pair: str, current_time, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs) -> float: """Calculate position size based on ATR to normalize risk across pairs. Formula: position_size = base_stake * (target_risk_pct / (ATR / current_price)) High-volatility pairs get smaller position sizes to keep risk normalized. """ # Division by zero protection if current_rate <= 0: return proposed_stake atr = self.atr_dict.get(pair, current_rate * 0.02) # fallback to 2% of price atr_pct = atr / current_rate # Division by zero protection for ATR if atr_pct <= 0: return proposed_stake position_size = proposed_stake * (self.target_risk_pct / atr_pct) # Clamp to reasonable bounds (0.5x to 2.0x of base stake) position_size = min(max_stake, max(min_stake if min_stake else proposed_stake * 0.5, position_size)) position_size = min(position_size, proposed_stake * 2.0) return position_size def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # RSI dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) # MACD macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9) dataframe["macd"] = macd["macd"] dataframe["macdsignal"] = macd["macdsignal"] dataframe["macdhist"] = macd["macdhist"] # Bollinger Bands bollinger = qtpylib.bollinger_bands( qtpylib.typical_price(dataframe), window=20, stds=2 ) dataframe["bb_lowerband"] = bollinger["lower"] dataframe["bb_upperband"] = bollinger["upper"] # EMA 50 / 200 dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=self.ema_fast_p.value) dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=self.ema_slow_p.value) # ADX dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) # ATR + rolling median (trend filter) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["atr_median"] = ( dataframe["atr"] .rolling(window=self.atr_window.value, min_periods=1) .median() ) # STOCH (Stochastic Oscillator) stoch = ta.STOCH(dataframe, fastk_period=14, slowk_period=3, slowd_period=3) dataframe["stoch_slowk"] = stoch["slowk"] dataframe["stoch_slowd"] = stoch["slowd"] # CCI (Commodity Channel Index) dataframe["cci"] = ta.CCI(dataframe, timeperiod=20) # MFI (Money Flow Index) dataframe["mfi"] = ta.MFI(dataframe, timeperiod=14) # WILLR (Williams %R) dataframe["willr"] = ta.WILLR(dataframe, timeperiod=14) # OBV (On Balance Volume) dataframe["obv"] = ta.OBV(dataframe) # SAR (Parabolic SAR) dataframe["sar"] = ta.SAR(dataframe, acceleration=0.02, maximum=0.2) # NATR (Normalized ATR) dataframe["natr"] = ta.NATR(dataframe, timeperiod=14) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [dataframe["volume"] > 0] # Trading window filter: prevent entries during blocked hours/days if self.blocked_hours or self.blocked_days: # Get current hour and day from dataframe index (assumes datetime index) dataframe["hour"] = dataframe.index.hour dataframe["day"] = dataframe.index.dayofweek # Block entries during excluded hours if self.blocked_hours: hour_filter = ~dataframe["hour"].isin(self.blocked_hours) conditions.append(hour_filter) # Block entries during excluded days if self.blocked_days: day_filter = ~dataframe["day"].isin(self.blocked_days) conditions.append(day_filter) if self.use_rsi.value: conditions.append(dataframe["rsi"] < self.rsi_threshold.value) if self.use_macd.value: conditions.append( qtpylib.crossed_above(dataframe["macd"], dataframe["macdsignal"]) & (dataframe["macdhist"] > self.macd_hist_min.value) ) if self.use_bb.value: conditions.append( dataframe["close"] <= dataframe["bb_lowerband"] * self.bb_factor.value ) if self.use_ema_cross.value: conditions.append(dataframe["ema_fast"] > dataframe["ema_slow"]) if self.use_adx.value: conditions.append(dataframe["adx"] > self.adx_threshold.value) if self.use_atr.value: conditions.append(dataframe["atr"] > dataframe["atr_median"]) if self.use_stoch.value: conditions.append(dataframe["stoch_slowk"] < self.stoch_threshold.value) if self.use_cci.value: conditions.append(dataframe["cci"] < self.cci_threshold.value) if self.use_mfi.value: conditions.append(dataframe["mfi"] < self.mfi_threshold.value) if self.use_willr.value: conditions.append(dataframe["willr"] < self.willr_threshold.value) if self.use_obv.value: conditions.append(dataframe["obv"] > dataframe["obv"].rolling(20).mean()) if self.use_sar.value: conditions.append(dataframe["close"] > dataframe["sar"]) if self.use_natr.value: conditions.append(dataframe["natr"] > dataframe["natr"].rolling(20).mean()) # Need at least one signal beyond the volume guard if len(conditions) > 1: combined = reduce(operator.and_, conditions) dataframe.loc[combined, "enter_long"] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return dataframe