Timeframe
1h
Direction
Long Only
Stoploss
-3.0%
Trailing Stop
Yes
ROI
0m: 6.0%, 120m: 4.0%, 360m: 2.0%, 720m: 1.0%
Interface Version
3
Startup Candles
200
Indicators
2
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
BaselineRsiEmaStrategy — Phase 1.7
Changes from 1.6:
- RSI lookback extended to 8 candles (was 6): catches slower dip-and-
recover setups where RSI takes 5-7 candles to climb back to the entry
threshold. Same signal quality, more samples for hyperopt.
- Bullish candle filter removed: too restrictive; a neutral/slightly red
candle at the exact RSI crossover moment is still a valid entry in a
triple-EMA-aligned trend. Was cutting ~40 % of valid signals.
- These changes target 180-220 trades so hyperopt has reliable statistics.
Not financial advice. Do not use with real funds without independent review.
"""
from typing import Dict, List
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.optimize.space import SKDecimal
from freqtrade.strategy import IntParameter, IStrategy
class BaselineRsiEmaStrategy(IStrategy):
INTERFACE_VERSION = 3
can_short = False
timeframe = "1h"
startup_candle_count = 200
minimal_roi = {
"0": 0.06,
"120": 0.04,
"360": 0.02,
"720": 0.01,
}
stoploss = -0.03
# Trailing stop: activates once the trade is up +2 %, then trails 1 %
# below the running peak. Trades that "almost made it" exit at +1 %
# instead of waiting for the full ROI or falling to the hard stop.
trailing_stop = True
trailing_stop_positive = 0.01
trailing_stop_positive_offset = 0.02
trailing_only_offset_is_reached = True
process_only_new_candles = True
# ------------------------------------------------------------------ #
# Hyperopt parameters #
# ------------------------------------------------------------------ #
buy_rsi_entry = IntParameter(20, 55, default=40, space="buy", optimize=True)
sell_rsi_exit = IntParameter(60, 90, default=70, space="sell", optimize=True)
# ------------------------------------------------------------------ #
# Constrained Hyperopt spaces #
# ------------------------------------------------------------------ #
@staticmethod
def stoploss_space() -> List:
return [SKDecimal(-0.06, -0.02, decimals=3, name="stoploss")]
@staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]:
roi_table: Dict[int, float] = {}
roi_table[0] = params["roi_p1"] + params["roi_p2"] + params["roi_p3"]
roi_table[params["roi_t3"]] = params["roi_p1"] + params["roi_p2"]
roi_table[params["roi_t3"] + params["roi_t2"]] = params["roi_p1"]
roi_table[params["roi_t3"] + params["roi_t2"] + params["roi_t1"]] = 0.01
return roi_table
# ------------------------------------------------------------------ #
# Fixed indicator periods #
# ------------------------------------------------------------------ #
ema_long_period: int = 200
ema_medium_period: int = 50
ema_short_period: int = 20
rsi_period: int = 14
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=self.ema_long_period)
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=self.ema_medium_period)
dataframe["ema20"] = ta.EMA(dataframe, timeperiod=self.ema_short_period)
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=self.rsi_period)
return dataframe
# ------------------------------------------------------------------ #
# Entry — triple EMA alignment filters out weak uptrends #
# ------------------------------------------------------------------ #
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
# All three EMAs stacked bullish: short > medium > long
(dataframe["ema20"] > dataframe["ema50"])
& (dataframe["ema50"] > dataframe["ema200"])
& (dataframe["close"] > dataframe["ema200"])
# RSI must have been genuinely oversold within the last 8
# candles — filters out mid-range noise crossovers
& (dataframe["rsi"].rolling(8).min() < 35)
# RSI crossover signal
& (dataframe["rsi"] > self.buy_rsi_entry.value)
& (dataframe["rsi"].shift(1) <= self.buy_rsi_entry.value)
& (dataframe["volume"] > 0)
),
["enter_long", "enter_tag"],
] = [1, "triple_ema_rsi_dip"]
return dataframe
# ------------------------------------------------------------------ #
# Exit — RSI overbought only (EMA cross exit removed) #
# ------------------------------------------------------------------ #
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["rsi"] > self.sell_rsi_exit.value)
& (dataframe["volume"] > 0)
),
["exit_long", "exit_tag"],
] = [1, "rsi_overbought"]
return dataframe