💬 Forum

Strategy14_ATR_Breakout

🏆 League #1550 / 1922

Nhebi23/halal-algo-trading-strategies/user_data/strategies/Strategy14_ATR_Breakout.py · first seen 2026-07-16 · repo updated 2026-05-07 · ⬇ 1 download

Basics mode: spot timeframe: 4h interface version: 3
Settings stoploss: -0.1 has minimal roi custom stoploss startup candle count: 60 hyperopt hyperopt params: 6
Indicators ATR EMA RSI pandas_ta
Concepts breakout mean_reversion trend_following
15 related strategies ( identical code, similar name)

Each tile is a different kind of check — from an instant code lint to full sandboxed backtests and forward tests on recent data. Not sure what a check actually proves? See the FAQ →

Source

Download Raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401

"""
Strategy 14: ATR Volatility Breakout
======================================
Author      : Halal Algo Trading Suite
Timeframe   : 4h
Asset class : Crypto SPOT only — no shorting, no margin, no futures

Overview
--------
The Average True Range (ATR) is the most widely used measure of market
volatility. Rather than using a fixed percentage stoploss, this strategy
dynamically sizes its risk exposure in proportion to the current market
volatility — a technique championed by trend-following funds.

Core Logic
----------
A breakout is defined as the close exceeding the highest high of the
previous 20 bars. This "Donchian-style" breakout avoids minor intraday
wicks and focuses on confirmed closing-price strength.

  Entry conditions (ALL must be true)
  ────────────────────────────────────
  • Close > highest high of the prior 20 bars (confirmed breakout)
  • Volume >= 1.5× the 20-bar average (institutional participation)
  • RSI > 50 (momentum regime, not just mean reversion bounce)
  • Close > 50-period EMA (trend aligned — avoids counter-trend fades)

ATR-Based Dynamic Stoploss
--------------------------
The custom_stoploss() override replaces the static -10 % fallback with
an ATR-proportional stop that adapts to live volatility:

  Phase         Condition              Stop distance
  ─────────     ─────────────────────  ──────────────
  Initial       Any open trade         ATR × 1.5
  Profitable    Profit > 0             ATR × 1.5 (trailing from current rate)
  Well in       Profit > ATR×2 / entry ATR × 0.8  (tightened to protect gains)

This means:
  • In a highly volatile market (large ATR), the stop is wider — reducing
    the chance of being shaken out by normal fluctuations.
  • In a calm market (small ATR), the stop is tighter — appropriate risk.
  • Once a trade is well in profit (> 2× ATR), the stop tightens
    significantly to lock in a larger portion of the gain.

Position Sizing Note (for manual risk management)
--------------------------------------------------
The ATR-based stop enables precise position sizing:

    Position Size (units) = Account Risk Amount / (ATR × multiplier)

  Example: $10 000 account, 1 % risk = $100 risk budget
           ATR = $500, multiplier = 1.5 → stop distance = $750
           Position = $100 / $750 = 0.133 units of the asset

This ensures consistent percentage-of-account risk regardless of volatility.

Risk Parameters
---------------
  Stoploss (fallback)  : -10 % (overridden by custom_stoploss in practice)
  use_custom_stoploss  : True
  ROI schedule         : 12 % immediately, 6 % after 8h, 3 % after 16h

Halal Compliance
----------------
  ✓ SPOT trading only
  ✓ No leverage or margin
  ✓ No short selling
  ✓ No interest-bearing instruments
"""

from datetime import datetime, timedelta
from typing import Optional

import numpy as np
import pandas as pd
import pandas_ta as ta

from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
from freqtrade.persistence import Trade


# ---------------------------------------------------------------------------
# Helper utilities
# ---------------------------------------------------------------------------

def crossed_above(s1: pd.Series, s2: pd.Series) -> pd.Series:
    """Return True on bars where s1 crosses above s2."""
    return (s1 > s2) & (s1.shift(1) <= s2.shift(1))


def crossed_below(s1: pd.Series, s2: pd.Series) -> pd.Series:
    """Return True on bars where s1 crosses below s2."""
    return (s1 < s2) & (s1.shift(1) >= s2.shift(1))


# ---------------------------------------------------------------------------
# Strategy
# ---------------------------------------------------------------------------

class Strategy14_ATR_Breakout(IStrategy):
    """
    ATR Volatility Breakout — Donchian breakout filtered by volume, RSI, and
    EMA, with ATR-proportional dynamic stoploss.

    See module docstring for full description.
    """

    INTERFACE_VERSION = 3

    # ------------------------------------------------------------------
    # Metadata
    # ------------------------------------------------------------------
    timeframe = "4h"
    can_short = False  # SPOT only

    # ------------------------------------------------------------------
    # ROI — trend trades need time; allow multi-day holds
    # ------------------------------------------------------------------
    minimal_roi = {
        "0":   0.12,  # 12 % immediately (large move target)
        "120": 0.06,  # 6 % after 8h (2 candles × 4h)
        "240": 0.03,  # 3 % after 16h (4 candles × 4h)
    }

    # ------------------------------------------------------------------
    # Stoploss — large fallback; actual stop set by custom_stoploss
    # ------------------------------------------------------------------
    stoploss = -0.10
    trailing_stop = False
    use_custom_stoploss = True

    # ------------------------------------------------------------------
    # Startup candle count — need 50+ bars for EMA and Donchian lookback
    # ------------------------------------------------------------------
    startup_candle_count = 60

    # ------------------------------------------------------------------
    # Hyper-opt parameters
    # ------------------------------------------------------------------
    # ATR period
    atr_period = IntParameter(10, 20, default=14, space="buy", optimize=True)
    # Donchian breakout lookback
    donchian_period = IntParameter(10, 30, default=20, space="buy", optimize=True)
    # Volume multiplier for entry confirmation
    vol_mult = DecimalParameter(1.2, 2.5, default=1.5, decimals=1, space="buy", optimize=True)
    # EMA length for trend filter
    ema_length = IntParameter(30, 100, default=50, space="buy", optimize=True)
    # ATR multiplier for initial stop
    atr_initial_mult = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="sell", optimize=True)
    # ATR multiplier when trade is well in profit
    atr_tight_mult = DecimalParameter(0.5, 1.5, default=0.8, decimals=1, space="sell", optimize=True)

    # ------------------------------------------------------------------
    # Indicator population
    # ------------------------------------------------------------------

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Compute all breakout and trend indicators.

        Columns added
        -------------
        atr             : 14-period Average True Range
        ema50           : 50-period EMA (trend filter)
        highest_high    : Rolling max of high over last 20 bars (shifted by 1)
        vol_ma          : 20-bar volume SMA
        vol_ratio       : Current volume / vol_ma
        rsi             : 14-period RSI
        breakout        : True when close > prior 20-bar highest high
        """
        atr_p = self.atr_period.value
        don_p = self.donchian_period.value

        # -- ATR -----------------------------------------------------------
        dataframe["atr"] = ta.atr(
            dataframe["high"], dataframe["low"], dataframe["close"],
            length=atr_p
        )

        # -- Trend filter --------------------------------------------------
        dataframe["ema50"] = ta.ema(dataframe["close"], length=self.ema_length.value)

        # -- Donchian highest high (exclude current bar) -------------------
        # .shift(1) ensures we use data UP TO (not including) the current bar,
        # preventing lookahead bias on the breakout condition.
        dataframe["highest_high"] = (
            dataframe["high"].shift(1).rolling(window=don_p).max()
        )

        # -- Volume analysis -----------------------------------------------
        dataframe["vol_ma"]    = dataframe["volume"].rolling(window=20).mean()
        dataframe["vol_ratio"] = dataframe["volume"] / dataframe["vol_ma"]

        # -- RSI -----------------------------------------------------------
        dataframe["rsi"] = ta.rsi(dataframe["close"], length=14)

        # -- Breakout flag -------------------------------------------------
        dataframe["breakout"] = dataframe["close"] > dataframe["highest_high"]

        # --- NaN Safety: convert any None to NaN so pandas comparisons work ---
        for col in dataframe.columns:
            if col not in ['open', 'high', 'low', 'close', 'volume', 'date']:
                try:
                    dataframe[col] = pd.to_numeric(dataframe[col], errors='coerce')
                except (ValueError, TypeError):
                    pass

        return dataframe

    # ------------------------------------------------------------------
    # Entry signals
    # ------------------------------------------------------------------

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Entry requires ALL four conditions simultaneously:

        1. Confirmed close-above-high breakout (Donchian 20)
        2. Volume >= vol_mult × 20-bar average (institutional participation)
        3. RSI > 50 (momentum regime)
        4. Close > 50-period EMA (trend aligned, avoid counter-trend)

        A single entry per breakout event is natural since the stoploss
        and ROI schedule manage the trade thereafter.
        """
        dataframe.loc[
            (
                dataframe["breakout"]
                & (dataframe["vol_ratio"] >= self.vol_mult.value)
                & (dataframe["rsi"] > 50)
                & (dataframe["close"] > dataframe["ema50"])
                & dataframe["atr"].notna()
                & dataframe["ema50"].notna()
                & dataframe["highest_high"].notna()
            ),
            "enter_long"
        ] = 1

        return dataframe

    # ------------------------------------------------------------------
    # Exit signals
    # ------------------------------------------------------------------

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Signal-based exits complement the custom stoploss:

        • Price falls back below the 50 EMA — trend is broken.
        • RSI drops below 40 — momentum has reversed.
        • Price closes back inside the Donchian channel (false breakout).

        The custom_stoploss handles the primary ATR-based exit, and
        the ROI schedule captures profit targets.
        """
        dataframe.loc[
            (
                # Trend filter broken
                crossed_below(dataframe["close"], dataframe["ema50"])
            )
            | (
                # Momentum reversed
                (dataframe["rsi"] < 40)
            )
            | (
                # Breakout failed — price closed back below prior high
                (dataframe["close"] < dataframe["highest_high"])
                & (~dataframe["breakout"])
            ),
            "exit_long"
        ] = 1

        return dataframe

    # ------------------------------------------------------------------
    # Custom stoploss — ATR-proportional, adaptive to profit phase
    # ------------------------------------------------------------------

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        """
        Replace the static stoploss with an ATR-proportional stop.

        Logic
        -----
        Phase 1 — Initial (any profit state):
            Stop distance = ATR × 1.5 / current_rate

        Phase 2 — Well profitable (profit > 2 × ATR / entry_rate):
            Stop distance = ATR × 0.8 / current_rate  (tightened)

        The returned value is a negative fraction of current_rate.
        Freqtrade interprets it as a relative distance from current price.

        Returns the most conservative (widest) of:
          • ATR-based calculation
          • The strategy's static stoploss (-10 %)
        to prevent runaway losses if ATR data is unavailable.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)

        if dataframe is None or dataframe.empty:
            return self.stoploss  # fallback to static

        last_candle = dataframe.iloc[-1]
        atr = last_candle.get("atr", None)

        if atr is None or np.isnan(atr) or atr <= 0 or current_rate <= 0:
            return self.stoploss  # fallback to static

        initial_mult = self.atr_initial_mult.value
        tight_mult   = self.atr_tight_mult.value

        # Threshold: profit exceeds 2 ATR relative to entry
        profit_threshold = (atr * 2.0) / trade.open_rate

        if current_profit > profit_threshold:
            # Well in profit — tighten to protect gains
            sl_distance = atr * tight_mult / current_rate
        else:
            # Standard ATR stop
            sl_distance = atr * initial_mult / current_rate

        # Return negative (stoploss below current price)
        # Cap at the static fallback to avoid excessively wide stops
        return max(-sl_distance, self.stoploss)