""" RegimeSwitchingHybrid_v6_1_Fett Fett ueberarbeitete Hybrid-Strategie – Phase 20 Bessere Exits, fixter Custom Stoploss, echtes Trailing, Short-Logik, besseres Risk:Reward Timeframe: 15m | HTF: 1h Fixes vs. Original-Entwurf: - Klassenname: Punkt durch Unterstrich ersetzt (Python-Syntax) - **kwargs in confirm_trade_entry und custom_stoploss (Freqtrade-Contract) - merge_informative_pair Import hinzugefuegt - enter_tag Ueberschreibungs-Bug behoben (Priorisierung: trend vor range) - trailing_stop auf True gesetzt (Frequtrade-native, ergaenzt custom_stoploss) """ import logging import sys from datetime import datetime, timedelta from typing import Optional import talib.abstract as ta from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, merge_informative_pair import freqtrade.vendor.qtpylib.indicators as qtpylib from pandas import DataFrame sys.path.insert(0, "/freqtrade/shared") from primo_signal import primo_gate_allows from fleetguard_v1 import FleetGuard, FleetGuardConfig logger = logging.getLogger(__name__) class RegimeSwitchingHybrid_v6_1_Fett(IStrategy): INTERFACE_VERSION = 3 timeframe = "15m" informative_timeframe = "1h" can_short = True # Aggressiveres ROI fuer besseres RR minimal_roi = { "0": 0.085, "45": 0.045, "90": 0.02, "180": 0, } # Failsafe stoploss — custom_stoploss() ist die eigentliche SL-Logik. # 9% Notaus, falls custom_stoploss() versagt. stoploss = -0.09 use_custom_stoploss = True trailing_stop = True trailing_stop_positive = 0.01 trailing_stop_positive_offset = 0.02 trailing_only_offset_is_reached = True startup_candle_count = 500 # ---- FleetGuard v1 entry safety (conservative limits) ---- _fleetguard = FleetGuard(FleetGuardConfig( max_open_trades=3, max_open_shorts=2, max_open_longs=2, )) @property def protections(self): return [ {"method": "CooldownPeriod", "stop_duration_candles": 5}, {"method": "StoplossGuard", "lookback_period_candles": 60, "trade_limit": 3, "stop_duration_candles": 60, "only_per_pair": False, "only_per_side": True}, {"method": "MaxDrawdown", "lookback_period_candles": 480, "trade_limit": 20, "stop_duration_candles": 96, "max_allowed_drawdown": 0.06}, {"method": "LowProfitPairs", "lookback_period_candles": 1440, "trade_limit": 2, "stop_duration_candles": 60, "required_profit": -0.01}, ] # Hyperoptable parameters (Buy space) adx_rel_threshold = DecimalParameter(0.8, 1.4, default=1.0, space="buy") rsi_oversold = IntParameter(20, 40, default=25, space="buy") # Fixed parameters rsi_overbought = 68 # ATR Multipliers (Sell space) — enger als v6 fuer besseres RR atr_sl_trend = 3.5 atr_sl_range = DecimalParameter(2.0, 4.0, default=2.8, space="sell", optimize=True) atr_tp_trend = DecimalParameter(1.0, 2.5, default=1.8, space="sell", optimize=True) def informative_pairs(self): pairs = self.dp.current_whitelist() return [(pair, self.informative_timeframe) for pair in pairs] def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: if not self.dp: return dataframe informative = self.dp.get_pair_dataframe( pair=metadata['pair'], timeframe=self.informative_timeframe ) informative['ema200'] = ta.EMA(informative, timeperiod=200) informative['adx'] = ta.ADX(informative) informative['rsi'] = ta.RSI(informative) dataframe = merge_informative_pair( dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True ) # Local indicators dataframe['adx'] = ta.ADX(dataframe) dataframe['adx_sma'] = dataframe['adx'].rolling(window=50).mean() dataframe['adx_rel'] = dataframe['adx'] / dataframe['adx_sma'] dataframe['rsi'] = ta.RSI(dataframe) dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200) # ATR calculation for dynamic stops dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) dataframe['atr_pct'] = dataframe['atr'] / dataframe['close'] 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['volume_mean'] = dataframe['volume'].rolling(window=30).mean() return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: ema200_htf = dataframe[f'ema200_{self.informative_timeframe}'] pair = metadata.get("pair") long_gate = primo_gate_allows(pair, "long") short_gate = primo_gate_allows(pair, "short") # --- LONG ENTRIES --- # Trend Regime Pullback (Long) trend_long = ( (dataframe['adx_rel'] > self.adx_rel_threshold.value) & (dataframe['close'] > ema200_htf) & (dataframe['close'] > dataframe['ema200']) & (dataframe['close'] < dataframe['ema50']) & (dataframe['rsi'] < 50) & (dataframe['volume'] > dataframe['volume_mean']) & long_gate ) # Range Regime Reversion (Long) range_long = ( (dataframe['adx_rel'] <= self.adx_rel_threshold.value) & (dataframe['rsi'] < self.rsi_oversold.value) & (dataframe['close'] < dataframe['bb_lowerband']) & (dataframe['volume'] > dataframe['volume_mean']) & long_gate ) # Combine long entries — trend prioritized over range via mask long_entries = trend_long | range_long dataframe.loc[long_entries, 'enter_long'] = 1 # enter_tag: trend wins when both match (set range first, then overwrite with trend) dataframe.loc[long_entries, 'enter_tag'] = 'range_reversion_long' dataframe.loc[trend_long, 'enter_tag'] = 'trend_pullback_long' # --- SHORT ENTRIES --- # Trend Regime Pullback (Short) trend_short = ( (dataframe['adx_rel'] > self.adx_rel_threshold.value) & (dataframe['close'] < ema200_htf) & (dataframe['close'] < dataframe['ema200']) & (dataframe['close'] > dataframe['ema50']) & (dataframe['rsi'] > 50) & (dataframe['volume'] > dataframe['volume_mean']) & short_gate ) # Range Regime Reversion (Short) range_short = ( (dataframe['adx_rel'] <= self.adx_rel_threshold.value) & (dataframe['rsi'] > 75) & (dataframe['close'] > dataframe['bb_upperband']) & (dataframe['volume'] > dataframe['volume_mean']) & short_gate ) # Combine short entries — trend prioritized over range via mask short_entries = trend_short | range_short dataframe.loc[short_entries, 'enter_short'] = 1 dataframe.loc[short_entries, 'enter_tag'] = 'range_reversion_short' dataframe.loc[trend_short, 'enter_tag'] = 'trend_pullback_short' return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Long exits dataframe.loc[ (dataframe['rsi'] > self.rsi_overbought) | (dataframe['close'] > dataframe['bb_upperband']), 'exit_long' ] = 1 # Short exits dataframe.loc[ (dataframe['rsi'] < 30) | (dataframe['close'] < dataframe['bb_lowerband']), 'exit_short' ] = 1 return dataframe def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], side: str, **kwargs) -> bool: """FleetGuard entry safety check with real trade data.""" open_trades = [] recent_closed = [] current_drawdown = 0.0 try: from freqtrade.persistence import Trade for t in Trade.get_trades_proxy(is_open=True): open_trades.append({"pair": t.pair, "is_short": t.is_short}) cutoff = current_time - timedelta(hours=24) for t in Trade.get_trades_proxy(is_open=False): if t.close_date and t.close_date >= cutoff: recent_closed.append({ "pair": t.pair, "is_short": t.is_short, "close_profit": t.close_profit or 0.0, }) total_profit = Trade.get_total_closed_profit() starting_balance = (self.wallets.get_starting_balance() if hasattr(self, 'wallets') and self.wallets else 1000.0) if starting_balance > 0: current_drawdown = abs(min(0, total_profit / starting_balance)) except Exception as e: logger.warning(f"FleetGuard data gathering fallback: {e}") try: from freqtrade.persistence import Trade for t in Trade.get_trades_proxy(is_open=True): open_trades.append({"pair": t.pair, "is_short": t.is_short}) except Exception: pass allowed, reason = self._fleetguard.check_entry( pair=pair, side=side, open_trades=open_trades, recent_closed_trades=recent_closed, current_drawdown_pct=current_drawdown ) if not allowed: logger.info(f"FleetGuard REJECT: {pair} {side} — {reason}") return False return True def custom_stoploss(self, pair: str, trade, current_time, current_rate, current_profit: float, **kwargs) -> float: """ATR-based dynamic stoploss with regime-aware trailing.""" dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe.empty: return self.stoploss last = dataframe.iloc[-1] atr_pct = last['atr_pct'] adx_rel = last.get('adx_rel', 1.0) is_trend = adx_rel > self.adx_rel_threshold.value if is_trend: sl_distance = atr_pct * self.atr_sl_trend tp_trigger = atr_pct * self.atr_tp_trend.value if current_profit > tp_trigger: # Trailing: SL folgt dem Gewinn, aber nie weiter als sl_distance zurueck return max(-sl_distance, current_profit - sl_distance) return -sl_distance else: # Range-Regime: engerer Stop sl_distance = atr_pct * self.atr_sl_range.value return -sl_distance