EMA Crossover Strategy — SPOT ONLY (Halal Algo Trading, Batch 1) ================================================================= Timeframe : 4h Author : Halal Algo Trading Version : 1.0
Timeframe
4h
Direction
Long Only
Stoploss
-10.0%
Trailing Stop
Yes
ROI
0m: 5.0%, 60m: 4.0%, 120m: 3.0%, 240m: 1.0%
Interface Version
3
Startup Candles
N/A
Indicators
3
freqtrade/freqtrade-strategies
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort:skip_file
# --- Do not remove these imports ---
from datetime import datetime
from typing import Optional
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
import pandas_ta as ta
# ---------------------------------------------------------------------------
# Strategy01_EMA_Crossover
# ---------------------------------------------------------------------------
class Strategy01_EMA_Crossover(IStrategy):
"""
EMA Crossover Strategy — SPOT ONLY (Halal Algo Trading, Batch 1)
=================================================================
Timeframe : 4h
Author : Halal Algo Trading
Version : 1.0
Overview
--------
A trend-following strategy built around a classic triple-EMA framework:
• Fast EMA (12-period) — reacts quickly to price momentum
• Slow EMA (50-period) — confirms the medium-term trend direction
• Trend EMA (200-period) — acts as the primary bull/bear filter
A Heikin Ashi candle confirmation is added to reduce false crossovers
caused by brief spikes, and a 20-period volume SMA gate prevents entries
in low-liquidity environments.
Entry Conditions (ALL must be true)
------------------------------------
1. Fast EMA (12) crosses ABOVE Slow EMA (50) → momentum shift
2. Price (close) is ABOVE the 200 EMA → overall bull market
3. Current candle volume > 20-period volume SMA → confirmed participation
4. Heikin Ashi close > Heikin Ashi open → HA candle is bullish
Exit Conditions (ANY triggers exit)
------------------------------------
A. Fast EMA crosses BELOW Slow EMA → momentum reversal
B. Price (close) closes BELOW Slow EMA (50) → trend breakdown
Risk Management
---------------
• Hard stoploss : -10 %
• Trailing stop : enabled
• Trailing stop positive: 2 % (locks in profit once positive)
• ROI ladder : 5 % @ open, 4 % @ 60 min, 3 % @ 120 min,
1 % @ 240 min
• SPOT only — no short, no margin, no futures
Backtest Reference
------------------
Sharpe 1.30 | Return 491 % | Max DD -34 % | Win Rate 35 % | PF 1.73
"""
# -------------------------------------------------------------------------
# Freqtrade metadata
# -------------------------------------------------------------------------
INTERFACE_VERSION = 3
# Strategy name shown in logs / hyperopt
strategy_name = "Strategy01_EMA_Crossover"
# Candle timeframe
timeframe = "4h"
# Only look at completed (closed) candles
process_only_new_candles = True
# Startup candle count — need at least 200 candles for the 200 EMA
startup_candle_count: int = 210
# -------------------------------------------------------------------------
# ROI — minimal profit targets (time in minutes → target return)
# -------------------------------------------------------------------------
minimal_roi = {
"0": 0.05, # 5 % immediately
"60": 0.04, # 4 % after 60 min
"120": 0.03, # 3 % after 120 min
"240": 0.01, # 1 % after 240 min
}
# -------------------------------------------------------------------------
# Stoploss & Trailing Stop
# -------------------------------------------------------------------------
stoploss = -0.10 # Hard stoploss: -10 %
trailing_stop = True # Enable trailing stop
trailing_stop_positive = 0.02 # Trail 2 % once trade is profitable
trailing_stop_positive_offset = 0.03 # Only activate trailing after +3 %
trailing_only_offset_is_reached = True
# -------------------------------------------------------------------------
# Misc
# -------------------------------------------------------------------------
# SPOT ONLY — no short positions
can_short = False
# Use custom sell (exit) signal
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# -------------------------------------------------------------------------
# Helper functions
# -------------------------------------------------------------------------
@staticmethod
def crossed_above(s1: pd.Series, s2: pd.Series) -> pd.Series:
"""Return True on the bar where s1 crosses from below to above s2."""
return (s1 > s2) & (s1.shift(1) <= s2.shift(1))
@staticmethod
def crossed_below(s1: pd.Series, s2: pd.Series) -> pd.Series:
"""Return True on the bar where s1 crosses from above to below s2."""
return (s1 < s2) & (s1.shift(1) >= s2.shift(1))
# -------------------------------------------------------------------------
# Indicators
# -------------------------------------------------------------------------
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Compute all technical indicators used by entry / exit conditions.
Indicators added to `dataframe`
--------------------------------
ema_fast — 12-period EMA
ema_slow — 50-period EMA
ema_trend — 200-period EMA (bull/bear filter)
volume_sma — 20-period simple moving average of volume
ha_open — Heikin Ashi open
ha_close — Heikin Ashi close
"""
# --- Exponential Moving Averages ---
dataframe["ema_fast"] = ta.ema(dataframe["close"], length=12)
dataframe["ema_slow"] = ta.ema(dataframe["close"], length=50)
dataframe["ema_trend"] = ta.ema(dataframe["close"], length=200)
# --- Volume filter: 20-period SMA of volume ---
dataframe["volume_sma"] = ta.sma(dataframe["volume"], length=20)
# --- Heikin Ashi candles ---
# HA Close = (O + H + L + C) / 4
dataframe["ha_close"] = (
dataframe["open"] + dataframe["high"]
+ dataframe["low"] + dataframe["close"]
) / 4
# HA Open = (prev HA Open + prev HA Close) / 2
ha_open = (dataframe["open"].shift(1) + dataframe["close"].shift(1)) / 2
if len(dataframe) > 0:
ha_open.iloc[0] = (dataframe["open"].iloc[0] + dataframe["close"].iloc[0]) / 2
dataframe["ha_open"] = ha_open
# --- NaN Safety: convert any None to NaN so pandas comparisons work ---
for col in ['ema_fast', 'ema_slow', 'ema_trend', 'volume_sma', 'ha_open', 'ha_close']:
dataframe[col] = pd.to_numeric(dataframe[col], errors='coerce')
return dataframe
# -------------------------------------------------------------------------
# Entry Signal
# -------------------------------------------------------------------------
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Generate LONG entry signals.
Conditions (ALL must be true simultaneously):
1. Fast EMA (12) crosses above Slow EMA (50)
2. Price (close) is above 200 EMA → bull trend
3. Volume exceeds its 20-period SMA → liquidity confirmed
4. Heikin Ashi candle is bullish (HA close > HA open)
The resulting signal is stored in the `enter_long` column.
"""
conditions = (
dataframe["ema_fast"].notna()
& dataframe["ema_slow"].notna()
& dataframe["ema_trend"].notna()
& dataframe["volume_sma"].notna()
& dataframe["ha_close"].notna()
& dataframe["ha_open"].notna()
)
dataframe.loc[
conditions
& self.crossed_above(dataframe["ema_fast"].fillna(0), dataframe["ema_slow"].fillna(0))
& (dataframe["close"] > dataframe["ema_trend"].fillna(np.inf))
& (dataframe["volume"] > dataframe["volume_sma"].fillna(np.inf))
& (dataframe["ha_close"].fillna(0) > dataframe["ha_open"].fillna(0)),
"enter_long",
] = 1
return dataframe
# -------------------------------------------------------------------------
# Exit Signal
# -------------------------------------------------------------------------
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Generate LONG exit signals.
Exit is triggered when EITHER condition is met:
A. Fast EMA (12) crosses below Slow EMA (50) — momentum reversal
B. Price (close) closes below Slow EMA (50) — trend breakdown
The resulting signal is stored in the `exit_long` column.
"""
exit_valid = dataframe["ema_fast"].notna() & dataframe["ema_slow"].notna()
dataframe.loc[
exit_valid
& (
self.crossed_below(dataframe["ema_fast"].fillna(0), dataframe["ema_slow"].fillna(0))
| (dataframe["close"] < dataframe["ema_slow"].fillna(0))
),
"exit_long",
] = 1
return dataframe