""" HybridGateStrategy — freqtrade port of the Claude trading bot's hard gate logic. Maps the bot's 3-layer gate to native indicators: - Multi-Timeframe Alignment → EMA stack across 15m/1h/4h, RSI agreement - Multi-Agent Debate → composite of RSI + MACD + Bollinger position - Volume Confirmation → volume / 20-period SMA ≥ 1.5 This is a faithful approximation, not exact — the live bot uses TradingView analysis + Claude synthesis. Backtest result indicates *ballpark* edge of the same logic on historical OHLCV. Defaults mirror live bot: SL=3%, TP=5%, gate strict. """ from datetime import datetime from functools import reduce from typing import Optional import numpy as np import pandas as pd import talib.abstract as ta from pandas import DataFrame from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative class HybridGateStrategy(IStrategy): INTERFACE_VERSION = 3 timeframe = "15m" can_short = False # Mirror live bot risk settings minimal_roi = {"0": 0.05} # take profit 5% stoploss = -0.03 # stop loss 3% trailing_stop = False process_only_new_candles = True use_exit_signal = False exit_profit_only = False ignore_roi_if_entry_signal = False startup_candle_count: int = 200 # ---- TUNABLES (hyperopt-able) ---- buy_rsi_max = IntParameter(50, 75, default=70, space="buy") buy_rsi_min = IntParameter(35, 55, default=45, space="buy") vol_ratio_min = DecimalParameter(1.0, 2.5, default=1.5, space="buy", decimals=2) mtf_score_min = IntParameter(1, 4, default=3, space="buy") agent_min = IntParameter(5, 9, default=7, space="buy") adx_min = IntParameter(20, 40, default=25, space="buy") @informative("1h") def populate_indicators_1h(self, df: DataFrame, metadata: dict) -> DataFrame: df["ema_50"] = ta.EMA(df, timeperiod=50) df["ema_200"] = ta.EMA(df, timeperiod=200) df["rsi"] = ta.RSI(df, timeperiod=14) return df @informative("4h") def populate_indicators_4h(self, df: DataFrame, metadata: dict) -> DataFrame: df["ema_50"] = ta.EMA(df, timeperiod=50) df["ema_200"] = ta.EMA(df, timeperiod=200) df["rsi"] = ta.RSI(df, timeperiod=14) return df def populate_indicators(self, df: DataFrame, metadata: dict) -> DataFrame: # Base 15m indicators df["ema_20"] = ta.EMA(df, timeperiod=20) df["ema_50"] = ta.EMA(df, timeperiod=50) df["ema_200"] = ta.EMA(df, timeperiod=200) df["rsi"] = ta.RSI(df, timeperiod=14) macd = ta.MACD(df) df["macd"] = macd["macd"] df["macd_signal"] = macd["macdsignal"] df["macd_hist"] = macd["macdhist"] bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) df["bb_lower"] = bb["lowerband"] df["bb_middle"] = bb["middleband"] df["bb_upper"] = bb["upperband"] df["bb_pos"] = (df["close"] - df["bb_lower"]) / (df["bb_upper"] - df["bb_lower"]) df["adx"] = ta.ADX(df, timeperiod=14) # Volume confirmation: vol_ratio = current / 20-period mean df["vol_sma_20"] = ta.SMA(df["volume"], timeperiod=20) df["vol_ratio"] = df["volume"] / df["vol_sma_20"] # ---- MTF score: count bullish timeframes (15m, 1h, 4h) ---- # 15m bias df["bias_15m"] = ((df["close"] > df["ema_50"]) & (df["ema_50"] > df["ema_200"])).astype(int) df["bias_15m"] -= ((df["close"] < df["ema_50"]) & (df["ema_50"] < df["ema_200"])).astype(int) # 1h df["bias_1h"] = ((df["close"] > df["ema_50_1h"]) & (df["ema_50_1h"] > df["ema_200_1h"])).astype(int) df["bias_1h"] -= ((df["close"] < df["ema_50_1h"]) & (df["ema_50_1h"] < df["ema_200_1h"])).astype(int) # 4h df["bias_4h"] = ((df["close"] > df["ema_50_4h"]) & (df["ema_50_4h"] > df["ema_200_4h"])).astype(int) df["bias_4h"] -= ((df["close"] < df["ema_50_4h"]) & (df["ema_50_4h"] < df["ema_200_4h"])).astype(int) df["mtf_score"] = df["bias_15m"] + df["bias_1h"] + df["bias_4h"] # ---- Multi-agent composite ---- # Tech agent: MACD + EMA cross + RSI room df["tech_signal"] = ( (df["macd"] > df["macd_signal"]).astype(int) + (df["close"] > df["ema_20"]).astype(int) + ((df["rsi"] > 50) & (df["rsi"] < 70)).astype(int) ) # Sentiment agent: RSI direction + BB position momentum df["senti_signal"] = ( (df["rsi"].diff(3) > 0).astype(int) + ((df["bb_pos"] > 0.5) & (df["bb_pos"] < 0.95)).astype(int) + (df["close"].pct_change(5) > 0).astype(int) ) # Risk agent: ADX trending + not overbought + volume sane df["risk_signal"] = ( (df["adx"] > 20).astype(int) + (df["rsi"] < 75).astype(int) + (df["vol_ratio"] > 0.7).astype(int) ) df["agent_consensus"] = df["tech_signal"] + df["senti_signal"] + df["risk_signal"] # Max possible = 9. ≥6 = bullish consensus. return df def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: conditions = [ df["mtf_score"] >= self.mtf_score_min.value, # MTF aligned bullish df["agent_consensus"] >= self.agent_min.value, # 3-agent consensus bullish df["vol_ratio"] >= self.vol_ratio_min.value, # volume confirmation df["rsi"] < self.buy_rsi_max.value, # not overbought df["rsi"] > self.buy_rsi_min.value, # momentum present df["close"] > df["ema_50"], # short-term trend up df["adx"] > self.adx_min.value, # only trade trending markets df["volume"] > 0, # candle has volume ] df.loc[reduce(lambda a, b: a & b, conditions), "enter_long"] = 1 return df def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: # Exit when MTF turns bearish OR agents flip to bearish df.loc[ ( (df["mtf_score"] <= -1) | (df["agent_consensus"] <= 3) | ((df["rsi"] > 80) & (df["macd"] < df["macd_signal"])) ), "exit_long" ] = 1 return df