Heikin Ashi Color-Change + EMA Trend Filter SPOT only — halal algo trading, no leverage.
Timeframe
4h
Direction
Long Only
Stoploss
-8.0%
Trailing Stop
Yes
ROI
0m: 5.0%, 60m: 3.0%, 120m: 2.0%, 240m: 1.0%
Interface Version
3
Startup Candles
N/A
Indicators
2
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
"""
Strategy06_HeikinAshi
=====================
A Heikin Ashi candle trend-following strategy for SPOT crypto trading.
Halal-compliant: SPOT only — no shorting, no margin, no futures.
Concept
-------
Heikin Ashi (HA) candles are a Japanese charting technique that smooths
price noise by averaging OHLC values across consecutive bars. Unlike
standard candlesticks, HA candles reduce the visual impact of short-term
volatility and make trend direction far easier to read:
• A strong bull trend shows consecutive green HA candles with no lower wick.
• A trend reversal shows a color change from red → green (or green → red).
This strategy exploits that structure by:
1. Detecting the FIRST green HA candle following one or more red HA candles
(color change = momentum shift).
2. Requiring the green candle to have NO (or negligible) lower wick, which
signals strong bullish conviction — price never revisited the open.
3. Confirming we are in an uptrend via the 50 EMA (price above it).
4. Using the 200 EMA as a macro bull filter.
Exit fires on the first HA candle that turns red (color change green → red),
capturing the earliest sign of trend exhaustion.
IMPORTANT — Stop-loss uses REAL (non-HA) prices
-------------------------------------------------
Stop-loss percentages are applied to the actual entry price (real OHLCV data)
by freqtrade, not to HA prices. This is correct behavior: HA prices are only
used for signal generation.
HA Open Calculation Note
--------------------------
The iterative formula for ha_open is mathematically exact:
ha_open[i] = (ha_open[i-1] + ha_close[i-1]) / 2
The simpler pandas shift-based approximation (ha_open ≈ (open + close).shift(1) / 2)
diverges increasingly from the exact value and is NOT used here.
Risk Parameters
---------------
stoploss : -8 %
trailing_stop : True
trailing_stop_positive : 2 % (locks in profit once ≥ 2 % gain)
ROI table : 5 % (0 min) → 3 % (60 min) → 2 % (120 min) → 1 % (240 min)
Timeframe : 4h
Author : Halal Algo Batch 2
"""
from freqtrade.strategy import IStrategy
import pandas as pd
import numpy as np
import pandas_ta as ta
from pandas import DataFrame
# ---------------------------------------------------------------------------
# Utility helpers
# ---------------------------------------------------------------------------
def crossed_above(s1: pd.Series, s2: pd.Series) -> pd.Series:
"""Return True on the bar where s1 crosses above s2."""
return (s1 > s2) & (s1.shift(1) <= s2.shift(1))
def crossed_below(s1: pd.Series, s2: pd.Series) -> pd.Series:
"""Return True on the bar where s1 crosses below s2."""
return (s1 < s2) & (s1.shift(1) >= s2.shift(1))
# ---------------------------------------------------------------------------
# Strategy
# ---------------------------------------------------------------------------
class Strategy06_HeikinAshi(IStrategy):
"""
Heikin Ashi Color-Change + EMA Trend Filter
SPOT only — halal algo trading, no leverage.
"""
# ------------------------------------------------------------------
# Freqtrade metadata
# ------------------------------------------------------------------
INTERFACE_VERSION = 3
timeframe = "4h"
can_short = False # SPOT only
# 200 EMA needs 200 candles minimum; add buffer for HA warm-up.
startup_candle_count: int = 220
# ------------------------------------------------------------------
# ROI
# ------------------------------------------------------------------
minimal_roi = {
"0": 0.05, # 5 % immediately
"60": 0.03, # 3 % after 60 min (1 x 4h bar ≈ open of next)
"120": 0.02, # 2 % after 120 min
"240": 0.01, # 1 % after 240 min
}
# ------------------------------------------------------------------
# Stop-loss & trailing stop
# ------------------------------------------------------------------
stoploss = -0.08 # -8 % hard stop (real price basis)
trailing_stop = True
trailing_stop_positive = 0.02 # activate trailing stop once +2 % profit
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
# ------------------------------------------------------------------
# Misc
# ------------------------------------------------------------------
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# ------------------------------------------------------------------
# Indicator calculation
# ------------------------------------------------------------------
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Compute Heikin Ashi candles (exact iterative formula) and EMA filters.
Heikin Ashi formulas
--------------------
ha_close = (open + high + low + close) / 4
ha_open[0] = (open[0] + close[0]) / 2
ha_open[i] = (ha_open[i-1] + ha_close[i-1]) / 2 (iterative)
ha_high = max(high, ha_open, ha_close)
ha_low = min(low, ha_open, ha_close)
Derived signals
---------------
ha_green : ha_close > ha_open (bullish bar)
ha_no_low_wick: ha_low == ha_open (open == low → no lower shadow)
ha_color_up : current green, previous red (momentum shift bullish)
ha_color_down : current red, previous green (momentum shift bearish)
"""
# ── HA close (no iteration needed) ─────────────────────────────
dataframe["ha_close"] = (
dataframe["open"] + dataframe["high"] +
dataframe["low"] + dataframe["close"]
) / 4.0
# ── HA open (exact iterative formula) ──────────────────────────
ha_open_vals = np.empty(len(dataframe), dtype=float)
ha_open_vals[0] = (dataframe["open"].iloc[0] + dataframe["close"].iloc[0]) / 2.0
ha_close_arr = dataframe["ha_close"].values
for i in range(1, len(dataframe)):
ha_open_vals[i] = (ha_open_vals[i - 1] + ha_close_arr[i - 1]) / 2.0
dataframe["ha_open"] = ha_open_vals
# ── HA high / low ───────────────────────────────────────────────
dataframe["ha_high"] = dataframe[["high", "ha_open", "ha_close"]].max(axis=1)
dataframe["ha_low"] = dataframe[["low", "ha_open", "ha_close"]].min(axis=1)
# ── Candle color booleans ───────────────────────────────────────
dataframe["ha_green"] = dataframe["ha_close"] > dataframe["ha_open"]
dataframe["ha_red"] = dataframe["ha_close"] < dataframe["ha_open"]
# ── No lower wick: ha_low == ha_open (with float tolerance) ────
# A negligible lower wick is tolerated (≤ 0.05 % of ha_open).
tolerance = dataframe["ha_open"] * 0.0005
dataframe["ha_no_low_wick"] = (
(dataframe["ha_open"] - dataframe["ha_low"]) <= tolerance
)
# ── Color change signals ────────────────────────────────────────
# Color change UP : current green AND previous red
dataframe["ha_color_up"] = (
dataframe["ha_green"] & dataframe["ha_red"].shift(1).fillna(False)
)
# Color change DOWN: current red AND previous green
dataframe["ha_color_down"] = (
dataframe["ha_red"] & dataframe["ha_green"].shift(1).fillna(False)
)
# ── EMA trend filters ───────────────────────────────────────────
dataframe["ema50"] = ta.ema(dataframe["close"], length=50)
dataframe["ema200"] = ta.ema(dataframe["close"], length=200)
# --- 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:
"""
Long entry conditions (ALL must be true simultaneously):
1. HA color change: current bar is green, previous bar was red.
This marks the first bullish bar after a bearish sequence —
the earliest signal of a momentum shift.
2. No (or negligible) lower wick on the current HA candle.
If ha_low == ha_open, the price never traded below the open,
indicating strong sustained buying pressure throughout the bar.
3. Real close price is above the 50 EMA.
Confirms we are in a short-to-medium term uptrend.
4. Real close price is above the 200 EMA.
Macro bull filter — avoids buying into long-term bear markets.
5. Volume > 0 (data quality guard).
"""
conditions = (
dataframe["ha_color_up"] &
dataframe["ha_no_low_wick"] &
(dataframe["close"] > dataframe["ema50"]) &
(dataframe["close"] > dataframe["ema200"]) &
(dataframe["volume"] > 0)
)
dataframe.loc[conditions, "enter_long"] = 1
return dataframe
# ------------------------------------------------------------------
# Exit signal
# ------------------------------------------------------------------
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Long exit condition:
HA color change DOWN — the current HA candle is red and the
previous one was green. This is the first bearish bar after the
bullish run and signals trend exhaustion.
Freqtrade will apply the hard stop-loss (-8 %) and trailing stop
independently using REAL prices, protecting against sharp drops
that bypass the HA signal.
"""
conditions = (
dataframe["ha_color_down"] &
(dataframe["volume"] > 0)
)
dataframe.loc[conditions, "exit_long"] = 1
return dataframe