from datetime import datetime from functools import reduce import numpy as np import pandas as pd import talib.abstract as ta from pandas import DataFrame from freqtrade.persistence import Trade from freqtrade.strategy import ( IStrategy, merge_informative_pair, stoploss_from_open, ) class ProTrend15m(IStrategy): """ ProTrend15m (v5 Elite Confluence): 1. Capitalizes on the 70.6% Win Rate 15m_fvg_vwap setup (1.74 Profit Factor). 2. Filters out low-volume breakout traps with 1.5x volume expansion. 3. Runs clean 1h macro trend alignment. """ INTERFACE_VERSION = 3 timeframe = "15m" informative_timeframe = "1h" can_short: bool = False # Pro Trader ROI Ladder: Targets real +3.8% to +5.5% expansions minimal_roi = { "0": 0.055, # 5.5% immediate momentum target "60": 0.038, # 3.8% target after 1 hour (4 candles) "180": 0.025, # 2.5% target after 3 hours "360": 0.016 # 1.6% floor target after 6 hours } # Baseline Hard Stop-Loss (-1.8%) stoploss = -0.018 trailing_stop = False use_custom_stoploss = True order_types = { "entry": "limit", "exit": "limit", "stoploss": "market", "stoploss_on_exchange": False } startup_candle_count: int = 150 @property def protections(self): return [ { "method": "CooldownPeriod", "stop_duration_candles": 4 # Wait 1 hour (4 x 15m) after exiting }, { "method": "StoplossGuard", "lookback_period_candles": 32, "trade_limit": 2, "stop_duration_candles": 16, "only_per_pair": False } ] def informative_pairs(self): pairs = self.dp.current_whitelist() informative = [(pair, self.informative_timeframe) for pair in pairs] informative.append(("BTC/USDT", self.informative_timeframe)) return informative def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ Asymmetric Profit Lock: - Up +2.2% -> Move stop to +0.8%. - Up +3.5% -> Lock +2.0%. - Up +5.0% -> Lock +3.5%. """ if current_profit >= 0.050: return stoploss_from_open(0.035, current_profit) if current_profit >= 0.035: return stoploss_from_open(0.020, current_profit) if current_profit >= 0.022: return stoploss_from_open(0.008, current_profit) return 1 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # ------------------------------------------------------------- # 1. 1h Macro Trend Filter # ------------------------------------------------------------- pair_1h = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe=self.informative_timeframe) if not pair_1h.empty: pair_1h["ema_50"] = ta.EMA(pair_1h, timeperiod=50) pair_1h["ema_200"] = ta.EMA(pair_1h, timeperiod=200) pair_1h["macro_bull"] = (pair_1h["close"] > pair_1h["ema_50"]) & (pair_1h["ema_50"] > pair_1h["ema_200"]) dataframe = merge_informative_pair( dataframe, pair_1h, self.timeframe, self.informative_timeframe, ffill=True ) btc_1h = self.dp.get_pair_dataframe(pair="BTC/USDT", timeframe=self.informative_timeframe) if not btc_1h.empty: btc_1h["btc_ema_50"] = ta.EMA(btc_1h, timeperiod=50) btc_1h["btc_bull"] = btc_1h["close"] > btc_1h["btc_ema_50"] dataframe = merge_informative_pair( dataframe, btc_1h, self.timeframe, self.informative_timeframe, ffill=True ) # ------------------------------------------------------------- # 2. Base 15m Indicators # ------------------------------------------------------------- dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20) dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["volume_sma"] = dataframe["volume"].rolling(window=20).mean() # Session VWAP typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3.0 pv = typical_price * dataframe["volume"] window_15m = 96 # 24 hours of 15m candles cum_pv = pv.rolling(window=window_15m, min_periods=1).sum() cum_vol = dataframe["volume"].rolling(window=window_15m, min_periods=1).sum() dataframe["vwap"] = cum_pv / np.where(cum_vol == 0, 1, cum_vol) dataframe["vwap_upper"] = dataframe["vwap"] + (1.5 * dataframe["atr"]) # Fair Value Gap (FVG) fvg_gap = dataframe["low"] - dataframe["high"].shift(2) dataframe["bullish_fvg"] = ( (fvg_gap > 0) & (dataframe["close"].shift(1) > dataframe["open"].shift(1)) ).astype(int) dataframe["fvg_active"] = dataframe["bullish_fvg"].rolling(window=4).max() dataframe["is_green"] = dataframe["close"] > dataframe["open"] return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, "enter_long"] = 0 dataframe.loc[:, "enter_tag"] = "" # Global Macro Alignment (1h Trend & BTC Anchor) macro_btc = dataframe.get("btc_bull_1h", pd.Series(True, index=dataframe.index)) == True macro_asset = dataframe.get("macro_bull_1h", pd.Series(True, index=dataframe.index)) == True base_filters = macro_btc & macro_asset # Setup 1: Institutional FVG Pullback (The 1.74 Profit Factor Engine) fvg_pullback = ( base_filters & (dataframe["adx"] > 20) & (dataframe["fvg_active"] == 1) & (dataframe["close"] >= dataframe["vwap"]) & (dataframe["close"] > dataframe["ema_20"]) & (dataframe["rsi"].between(48, 62)) & (dataframe["volume"] > dataframe["volume_sma"] * 1.15) & dataframe["is_green"] ) dataframe.loc[fvg_pullback, "enter_long"] = 1 dataframe.loc[fvg_pullback, "enter_tag"] = "15m_fvg_vwap" # Setup 2: True Institutional Breakout (Requires Heavy 1.5x Volume Expansion) trend_breakout = ( base_filters & (dataframe["close"] > dataframe["ema_20"]) & (dataframe["ema_20"] > dataframe["ema_50"]) & (dataframe["adx"] > 24) & (dataframe["rsi"].between(54, 66)) & (dataframe["volume"] > dataframe["volume_sma"] * 1.50) & dataframe["is_green"] ) dataframe.loc[trend_breakout, "enter_long"] = 1 dataframe.loc[trend_breakout, "enter_tag"] = "15m_trend_breakout" return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # Technical Exhaustion Exit conditions.append(dataframe["close"] >= dataframe["vwap_upper"]) conditions.append(dataframe["rsi"] > 74) if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), "exit_long" ] = 1 return dataframe