MACD Strategy — SPOT ONLY (Halal Algo Trading, Batch 1) ======================================================== Timeframe : 4h Author : Halal Algo Trading Version : 1.0
Timeframe
4h
Direction
Long Only
Stoploss
-8.0%
Trailing Stop
Yes
ROI
0m: 6.0%, 60m: 4.0%, 120m: 2.0%, 360m: 1.0%
Interface Version
3
Startup Candles
N/A
Indicators
2
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
# ---------------------------------------------------------------------------
# Strategy02_MACD
# ---------------------------------------------------------------------------
class Strategy02_MACD(IStrategy):
"""
MACD Strategy — SPOT ONLY (Halal Algo Trading, Batch 1)
========================================================
Timeframe : 4h
Author : Halal Algo Trading
Version : 1.0
Overview
--------
This strategy combines two MACD configurations with a CCI oversold/overbought
filter to improve entry quality and reduce false signals:
• Standard MACD — fast=12, slow=26, signal=9 (primary trigger)
• Alternative MACD — fast=8, slow=17, signal=9 (faster, used as context)
• CCI (25-period) — momentum oscillator to confirm oversold/overbought zones
The strategy is enhanced with a bearish divergence detector on exit:
when price makes a higher high but MACD makes a lower high, the momentum
is fading and the position is closed to protect profits.
Entry Conditions (ALL must be true)
------------------------------------
1. Standard MACD line crosses ABOVE the signal line → bullish crossover
2. MACD histogram turns positive (hist > 0) → confirmed momentum
3. CCI <= -50 → oversold, mean-reversion opportunity
STRONGER entry (optional, stricter):
• Crossover occurs BELOW the zero line (MACD < 0 at crossover)
Exit Conditions (ANY triggers exit)
------------------------------------
A. Standard MACD line crosses BELOW the signal line AND CCI >= 100
B. Bearish divergence detected:
• Price makes a higher high over the last 5 bars
• MACD makes a lower high over the same 5 bars
→ momentum weakening while price extends → exit
Risk Management
---------------
• Hard stoploss : -8 %
• Trailing stop : enabled
• Trailing stop positive: 2.5 %
• ROI ladder : 6 % @ open, 4 % @ 60 min, 2 % @ 120 min,
1 % @ 360 min
• SPOT only — no short, no margin, no futures
Backtest Reference
------------------
Sharpe 1.17 | Return 428 % | Max DD -33 % | PF 1.00
"""
# -------------------------------------------------------------------------
# Freqtrade metadata
# -------------------------------------------------------------------------
INTERFACE_VERSION = 3
strategy_name = "Strategy02_MACD"
timeframe = "4h"
process_only_new_candles = True
# Need enough bars for MACD slow=26 + signal smoothing; 60 is conservative
startup_candle_count: int = 60
# -------------------------------------------------------------------------
# ROI
# -------------------------------------------------------------------------
minimal_roi = {
"0": 0.06, # 6 % immediately
"60": 0.04, # 4 % after 60 min
"120": 0.02, # 2 % after 120 min
"360": 0.01, # 1 % after 360 min
}
# -------------------------------------------------------------------------
# Stoploss & Trailing Stop
# -------------------------------------------------------------------------
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.025
trailing_stop_positive_offset = 0.04
trailing_only_offset_is_reached = True
# -------------------------------------------------------------------------
# Misc
# -------------------------------------------------------------------------
can_short = False
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))
@staticmethod
def bearish_divergence(
price: pd.Series,
macd: pd.Series,
lookback: int = 5,
) -> pd.Series:
"""
Detect bearish MACD divergence over a rolling window.
A bearish divergence occurs when price records a higher high
while MACD records a lower high over the same `lookback` bars.
This signals waning momentum behind the price rally.
Parameters
----------
price : close price series
macd : MACD line series
lookback : number of bars to look back for the comparison high
Returns
-------
Boolean series — True where divergence is detected
"""
price_hh = price > price.shift(lookback).rolling(lookback).max()
macd_lh = macd < macd.shift(lookback).rolling(lookback).max()
return price_hh & macd_lh
# -------------------------------------------------------------------------
# Indicators
# -------------------------------------------------------------------------
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Compute all technical indicators.
Indicators added to `dataframe`
--------------------------------
macd — standard MACD line (12/26/9)
macd_signal — standard MACD signal line
macd_hist — standard MACD histogram
macd_alt — alternative MACD line (8/17/9)
macd_signal_alt — alternative MACD signal line
macd_hist_alt — alternative MACD histogram
cci — CCI with period 25
bearish_div — True where bearish MACD divergence is detected
"""
# --- Standard MACD (12, 26, 9) ---
macd_std = ta.macd(dataframe["close"], fast=12, slow=26, signal=9)
# pandas_ta returns a DataFrame; columns: MACD_12_26_9, MACDh_12_26_9, MACDs_12_26_9
dataframe["macd"] = macd_std["MACD_12_26_9"]
dataframe["macd_signal"] = macd_std["MACDs_12_26_9"]
dataframe["macd_hist"] = macd_std["MACDh_12_26_9"]
# --- Alternative MACD (8, 17, 9) — faster, used for context ---
macd_alt = ta.macd(dataframe["close"], fast=8, slow=17, signal=9)
dataframe["macd_alt"] = macd_alt["MACD_8_17_9"]
dataframe["macd_signal_alt"] = macd_alt["MACDs_8_17_9"]
dataframe["macd_hist_alt"] = macd_alt["MACDh_8_17_9"]
# --- CCI (25-period) ---
dataframe["cci"] = ta.cci(
dataframe["high"], dataframe["low"], dataframe["close"], length=25
)
# --- Bearish divergence flag ---
dataframe["bearish_div"] = self.bearish_divergence(
dataframe["close"], dataframe["macd"], lookback=5
)
# --- NaN Safety: convert any None to NaN so pandas comparisons work ---
for col in dataframe.columns:
if col not in ['open', 'high', 'low', 'close', 'volume', 'date']:
try:
dataframe[col] = pd.to_numeric(dataframe[col], errors='coerce')
except (ValueError, TypeError):
pass
return dataframe
# -------------------------------------------------------------------------
# Entry Signal
# -------------------------------------------------------------------------
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Generate LONG entry signals.
Standard Entry (all three conditions):
1. MACD line crosses above signal line
2. MACD histogram is positive (hist > 0) — momentum confirmed
3. CCI <= -50 — oversold mean-reversion opportunity
Stronger (stricter) variant is embedded inside the same condition:
The crossover check already covers both above-zero and below-zero
crossovers. A comment marks where to add the below-zero restriction
if desired.
"""
dataframe.loc[
(
# Condition 1 — MACD bullish crossover
self.crossed_above(dataframe["macd"], dataframe["macd_signal"])
# Condition 2 — Histogram positive (momentum confirmed)
& (dataframe["macd_hist"] > 0)
# Condition 3 — CCI oversold
& (dataframe["cci"] <= -50)
# [Optional stronger filter] Uncomment to require crossover below zero:
# & (dataframe["macd"] < 0)
# Safety: indicators populated
& dataframe["macd"].notna()
& dataframe["macd_signal"].notna()
& dataframe["cci"].notna()
),
"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. MACD line crosses below signal line AND CCI is overbought (>= 100)
B. Bearish MACD divergence detected (price higher high, MACD lower high)
"""
dataframe.loc[
(
# Condition A — MACD bearish crossover with overbought CCI
(
self.crossed_below(dataframe["macd"], dataframe["macd_signal"])
& (dataframe["cci"] >= 100)
)
# Condition B — Bearish divergence: momentum fading
| dataframe["bearish_div"]
)
& dataframe["macd"].notna()
& dataframe["macd_signal"].notna(),
"exit_long",
] = 1
return dataframe