"""A simple and popular Freqtrade strategy: EMA crossover with RSI filter. This strategy demonstrates basic structure expected by Freqtrade: - Adds a few technical indicators (EMA fast/slow, RSI) - Enters on bullish EMA crossover with RSI below an overbought threshold - Exits on bearish crossover or RSI overbought DISCLAIMER: For educational purposes only. Not optimized for live trading. """ from __future__ import annotations from typing import Dict, Any import pandas as pd try: # Graceful fallback if freqtrade libs not installed at dev time from freqtrade.strategy import IStrategy from freqtrade.vendor.qtpylib import indicator_extensions as qtpylib except ImportError: # pragma: no cover - only for environments without freqtrade class IStrategy: # type: ignore pass qtpylib = None # type: ignore class EMACrossRSIStrategy(IStrategy): """EMA crossover with RSI confirmation. Parameters / attributes: timeframe: Candle timeframe used for backtesting/live (string like "5m", "1h"). minimal_roi: Dict of minute offsets -> target ROI fraction; governs dynamic take-profit. stoploss: Hard stop loss as a negative fraction (e.g. -0.08 = -8%). trailing_stop: Enable/disable trailing stop mechanism (bool). ema_fast_length: Period length for fast EMA (short-term trend). ema_slow_length: Period length for slow EMA (long-term trend). rsi_length: Lookback period for RSI calculation. rsi_overbought: RSI threshold considered overbought; used to filter entries / force exits. startup_candle_count: Minimum candles required before signals (ensures indicators initialized). """ # --- Required class attributes --- # timeframe = "5m" # Trading timeframe for this strategy. # Minimal ROI table (example). Keys are minutes since trade open, values are min acceptable profit ratio. minimal_roi: Dict[str, float] = { "0": 0.02, # Immediately after entry aim for 2%. "30": 0.01, # After 30 minutes accept 1%. "120": 0.0, # After 120 minutes accept break-even (0%). } stoploss = -0.08 # Hard stop: exit if loss reaches -8%. trailing_stop = False # Trailing stop disabled for simplicity; set True to enable. # Indicator configuration (can be tuned / hyperoptimized). ema_fast_length = 12 # Fast EMA period (reacts quickly). ema_slow_length = 26 # Slow EMA period (smoother baseline). rsi_length = 14 # RSI lookback period (standard default). rsi_overbought = 70 # RSI threshold to deem market overbought. startup_candle_count = max(ema_slow_length, rsi_length) + 1 # Ensure enough candles to compute indicators. def populate_indicators(self, dataframe: pd.DataFrame, metadata: Dict[str, Any]) -> pd.DataFrame: # type: ignore[override] """Add EMA and RSI indicators to the dataframe. Freqtrade calls this once per candle batch. """ if dataframe.empty: return dataframe # Exponential moving averages dataframe["ema_fast"] = dataframe["close"].ewm(span=self.ema_fast_length, adjust=False).mean() dataframe["ema_slow"] = dataframe["close"].ewm(span=self.ema_slow_length, adjust=False).mean() # RSI calculation (manual implementation avoiding external dependency for portability) delta = dataframe["close"].diff() gain = (delta.where(delta > 0, 0)).ewm(alpha=1 / self.rsi_length, adjust=False).mean() loss = (-delta.where(delta < 0, 0)).ewm(alpha=1 / self.rsi_length, adjust=False).mean() rs = gain / loss.replace(0, 1e-10) dataframe["rsi"] = 100 - (100 / (1 + rs)) # Crossover helpers (qtpylib has a cross function; reimplement if not available) if qtpylib: dataframe["ema_cross_up"] = qtpylib.crossed_above(dataframe["ema_fast"], dataframe["ema_slow"]) # type: ignore[attr-defined] dataframe["ema_cross_down"] = qtpylib.crossed_below(dataframe["ema_fast"], dataframe["ema_slow"]) # type: ignore[attr-defined] else: # Fallback simple cross detection prev_fast = dataframe["ema_fast"].shift(1) prev_slow = dataframe["ema_slow"].shift(1) dataframe["ema_cross_up"] = (dataframe["ema_fast"] > dataframe["ema_slow"]) & (prev_fast <= prev_slow) dataframe["ema_cross_down"] = (dataframe["ema_fast"] < dataframe["ema_slow"]) & (prev_fast >= prev_slow) return dataframe def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: Dict[str, Any]) -> pd.DataFrame: # type: ignore[override] """Define entry (buy) signals. Long when EMA fast crosses above EMA slow and RSI below overbought threshold. """ dataframe.loc[ ( (dataframe["ema_cross_up"]) & (dataframe["rsi"] < self.rsi_overbought) & (dataframe["volume"] > 0) # Basic volume guard ensuring candle isn't empty. ), ["enter_long", "enter_tag"], ] = (1, "ema_cross_rsi") return dataframe def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: Dict[str, Any]) -> pd.DataFrame: # type: ignore[override] """Define exit (sell) signals. Exit when bearish crossover or RSI > overbought threshold. """ dataframe.loc[ ( (dataframe["ema_cross_down"]) | (dataframe["rsi"] > self.rsi_overbought) & (dataframe["volume"] > 0) ), ["exit_long", "exit_tag"], ] = (1, "exit_condition") return dataframe # (Optional) If you wanted to support shorting, you would implement enter_short/exit_short similarly. __all__ = ["EMACrossRSIStrategy"]