Momentum-breakout strategy active only in BREAKOUT regime.
Timeframe
4h
Direction
Long Only
Stoploss
-4.0%
Trailing Stop
Yes
ROI
0m: 20.0%, 48m: 10.0%
Interface Version
N/A
Startup Candles
200
Indicators
2
freqtrade/freqtrade-strategies
freqtrade/freqtrade-strategies
this is an example class, implementing a PSAR based trailing stop loss you are supposed to take the `custom_stoploss()` and `populate_indicators()` parts and adapt it to your own strategy
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
MomentumBreakout – Freqtrade strategy for BREAKOUT regimes.
Catches explosive moves on high volume with expanding ATR.
"""
from __future__ import annotations
import pandas as pd
try:
from freqtrade.strategy import IStrategy
_FREQTRADE_AVAILABLE = True
except ImportError:
_FREQTRADE_AVAILABLE = False
class IStrategy: # type: ignore[no-redef]
stoploss: float = -0.04
minimal_roi: dict = {"0": 0.20}
timeframe: str = "4h"
trailing_stop: bool = False
trailing_stop_positive: float | None = None
trailing_stop_positive_offset: float = 0.0
trailing_only_offset_is_reached: bool = False
process_only_new_candles: bool = True
use_exit_signal: bool = True
exit_profit_only: bool = False
can_short: bool = False
startup_candle_count: int = 200
def __init__(self, config: dict | None = None):
self.config = config or {}
def populate_indicators(self, dataframe, metadata): # pragma: no cover
return dataframe
def populate_entry_trend(self, dataframe, metadata): # pragma: no cover
return dataframe
def populate_exit_trend(self, dataframe, metadata): # pragma: no cover
return dataframe
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from strategies.helpers.indicators import (
atr,
rsi,
volume_sma,
)
from strategies.RegimeDetector import RegimeDetector
class MomentumBreakout(IStrategy):
"""
Momentum-breakout strategy active only in BREAKOUT regime.
Entry logic (all must be true):
1. Regime == BREAKOUT
2. Price breaks above 20-period high
3. Volume > 2.5x 20-period average
4. ATR expanding (current > max of previous 3 candles)
5. RSI between 55–75
Filter: Do NOT enter if price already moved >5 % from breakout level.
Exit logic:
- Hard stop: -4 %
- Target 1: +10 % (via minimal_roi entry "0": 0.10)
- Target 2: +20 % (via minimal_roi entry "0": 0.20 – kept at all time)
- Trailing stop follows after 10 % gain
"""
stoploss = -0.04
# Scaled ROI: keep half at 10%, rest rides with trailing stop
minimal_roi = {"0": 0.20, "48": 0.10}
timeframe = "4h"
trailing_stop = True
trailing_stop_positive = 0.10
trailing_stop_positive_offset = 0.12
trailing_only_offset_is_reached = True
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
can_short = False
startup_candle_count = 200
def __init__(self, config: dict | None = None):
if _FREQTRADE_AVAILABLE:
super().__init__(config) # type: ignore[call-arg]
else:
self.config = config or {}
self._regime_detector = RegimeDetector()
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
# Regime
dataframe = self._regime_detector.add_indicators(dataframe)
raw_regime = self._regime_detector.detect_regime(dataframe)
dataframe["regime"] = self._regime_detector.apply_hysteresis(raw_regime, candles=3)
# Strategy indicators
dataframe["rsi_14"] = rsi(dataframe["close"], 14)
dataframe["atr_14"] = atr(dataframe["high"], dataframe["low"], dataframe["close"], 14)
# 20-period high (breakout level)
dataframe["high_20"] = dataframe["high"].rolling(window=20).max()
# Use prior 20-period high (shift by 1 to avoid look-ahead)
dataframe["breakout_level"] = dataframe["high_20"].shift(1)
# ATR of previous 3 candles (max) for expansion check
dataframe["atr_prev_max"] = dataframe["atr_14"].shift(1).rolling(window=3).max()
# Volume metrics
dataframe["vol_avg_20"] = volume_sma(dataframe["volume"], 20)
return dataframe
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe["enter_long"] = 0
dataframe["enter_tag"] = ""
# Price must break above the prior 20-period high
price_breaks_out = dataframe["close"] > dataframe["breakout_level"]
# Filter: not more than 5 % beyond breakout level
within_5pct = (
(dataframe["close"] - dataframe["breakout_level"]) / dataframe["breakout_level"]
) <= 0.05
# ATR expanding
atr_expanding = dataframe["atr_14"] > dataframe["atr_prev_max"]
conditions = (
(dataframe["regime"] == "BREAKOUT")
& price_breaks_out
& within_5pct
& (dataframe["volume"] > dataframe["vol_avg_20"] * 2.5)
& atr_expanding
& (dataframe["rsi_14"] >= 55)
& (dataframe["rsi_14"] <= 75)
)
dataframe.loc[conditions, "enter_long"] = 1
dataframe.loc[conditions, "enter_tag"] = "momentum_breakout"
return dataframe
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe["exit_long"] = 0
dataframe["exit_tag"] = ""
# Exit on regime change away from BREAKOUT
regime_change = dataframe["regime"] != "BREAKOUT"
dataframe.loc[regime_change, "exit_long"] = 1
dataframe.loc[regime_change, "exit_tag"] = "regime_change"
return dataframe