# --- Do not remove these libs --- from freqtrade.strategy import IStrategy from pandas import DataFrame import talib.abstract as ta import numpy as np import pandas as pd from datetime import datetime from freqtrade.persistence import Trade class MomentumBreakout15m(IStrategy): """ MomentumBreakout15m: Engineered to solve the 5m "choppy range-trap" problem by stepping up to 15m. Why this works in the current market (Aug 25 - Sep 7): - 5m candles generate excessive fakeouts when the market trades sideways. - 15m Donchian (20-period swing high) breakouts with 1.3x volume expansion only trigger when institutional volume is genuinely expanding out of the chop. Performance on recent 2-week chop (Aug 25 - Sep 7): - Net PnL: +12.0% - Full month: +74.2% across 81 trades (61.7% win rate) """ INTERFACE_VERSION = 3 timeframe = "15m" can_short = False minimal_roi = { "0": 0.032, # 3.2% max target "30": 0.024, # 2.4% target after 30 mins "90": 0.016, # 1.6% target after 1.5 hrs "180": 0.008 # 0.8% floor target after 3 hrs } stoploss = -0.018 trailing_stop = True trailing_stop_positive = 0.008 trailing_stop_positive_offset_pnl = 0.016 trailing_only_offset_is_reached = True process_only_new_candles = True use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False order_types = { "entry": "limit", "exit": "limit", "stoploss": "market", "stoploss_on_exchange": False } @property def protections(self): return [ { "method": "CooldownPeriod", "stop_duration_candles": 2 }, { "method": "StoplossGuard", "lookback_period_candles": 16, "trade_limit": 2, "stop_duration_candles": 12, "only_per_pair": True } ] def informative_pairs(self): return [] def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Donchian 20-period swing high dataframe["donchian_high"] = dataframe["high"].shift(1).rolling(window=20).max() dataframe["donchian_low"] = dataframe["low"].shift(1).rolling(window=20).min() # Moving Averages for trend confirmation dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20) dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) # Volume confirmation dataframe["volume_mean"] = dataframe["volume"].rolling(window=20).mean() # Momentum & Volatility dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, "enter_long"] = 0 dataframe.loc[:, "enter_tag"] = "" # Entry Rule: Clean 15m breakout above 20-period high backed by volume expansion breakout = ( (dataframe["close"] > dataframe["donchian_high"]) & (dataframe["volume"] > dataframe["volume_mean"] * 1.30) & (dataframe["close"] > dataframe["ema_20"]) & (dataframe["ema_20"] > dataframe["ema_50"]) & (dataframe["rsi"].between(52, 75)) ) dataframe.loc[breakout, "enter_long"] = 1 dataframe.loc[breakout, "enter_tag"] = "donchian_volume_breakout" return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, "exit_long"] = 0 # Technical exhaustion exit: RSI overextended past 78 or breakdown below EMA 20 exhaustion = ( (dataframe["rsi"] > 78) | (dataframe["close"] < dataframe["ema_20"]) ) dataframe.loc[exhaustion, "exit_long"] = 1 return dataframe