Trend-following strategy active only in BULL_TREND regime.
Timeframe
4h
Direction
Long Only
Stoploss
-5.0%
Trailing Stop
Yes
ROI
0m: 8.0%
Interface Version
N/A
Startup Candles
200
Indicators
3
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
TrendFollower – Freqtrade strategy for BULL_TREND regimes.
Buys pullbacks within confirmed bull trends.
"""
from __future__ import annotations
import sys
import pandas as pd
try:
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter
_FREQTRADE_AVAILABLE = True
except ImportError:
_FREQTRADE_AVAILABLE = False
class IStrategy: # type: ignore[no-redef]
"""Minimal stub so the module is importable without Freqtrade."""
stoploss: float = -0.05
minimal_roi: dict = {"0": 0.08}
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
class DecimalParameter: # type: ignore[no-redef]
def __init__(self, *args, **kwargs):
self.value = kwargs.get("default", args[1] if len(args) > 1 else 0)
class IntParameter: # type: ignore[no-redef]
def __init__(self, *args, **kwargs):
self.value = kwargs.get("default", args[1] if len(args) > 1 else 0)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from strategies.helpers.indicators import (
ema,
rsi,
macd,
volume_sma,
)
from strategies.RegimeDetector import RegimeDetector
class TrendFollower(IStrategy):
"""
Trend-following strategy active only in BULL_TREND regime.
Entry logic (all must be true):
1. Regime == BULL_TREND
2. RSI_14 crosses below 40 (pullback signal)
3. Price within 1 % of EMA_21
4. MACD histogram decreasing but MACD line above signal
5. Volume on pullback candle below 20-bar average
Exit logic:
- Hard stop: -5 %
- minimal_roi: 8 %
- Trailing stop activates after 4 % gain
"""
stoploss = -0.05
minimal_roi = {"0": 0.08}
timeframe = "4h"
trailing_stop = True
trailing_stop_positive = 0.04
trailing_stop_positive_offset = 0.05
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 indicators
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["ema_21"] = ema(dataframe["close"], 21)
dataframe["rsi_14"] = rsi(dataframe["close"], 14)
macd_line, signal_line, histogram = macd(dataframe["close"])
dataframe["macd"] = macd_line
dataframe["macd_signal"] = signal_line
dataframe["macd_hist"] = histogram
dataframe["macd_hist_prev"] = histogram.shift(1)
dataframe["vol_avg_20"] = volume_sma(dataframe["volume"], 20)
dataframe["rsi_prev"] = dataframe["rsi_14"].shift(1)
return dataframe
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe["enter_long"] = 0
dataframe["enter_tag"] = ""
# Distance from EMA_21 in percent
ema21_dist = ((dataframe["close"] - dataframe["ema_21"]) / dataframe["ema_21"]).abs()
conditions = (
(dataframe["regime"] == "BULL_TREND")
& (dataframe["rsi_14"] < 40) # RSI pulled back
& (dataframe["rsi_prev"] >= 40) # crossed below 40 (was above previous candle)
& (ema21_dist <= 0.01) # within 1 % of EMA_21
& (dataframe["macd"] > dataframe["macd_signal"]) # MACD line above signal
& (dataframe["macd_hist"] < dataframe["macd_hist_prev"]) # histogram decreasing
& (dataframe["volume"] < dataframe["vol_avg_20"]) # low volume pullback
)
dataframe.loc[conditions, "enter_long"] = 1
dataframe.loc[conditions, "enter_tag"] = "trend_pullback"
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 BULL_TREND
exit_conditions = dataframe["regime"] != "BULL_TREND"
dataframe.loc[exit_conditions, "exit_long"] = 1
dataframe.loc[exit_conditions, "exit_tag"] = "regime_change"
return dataframe