MACD Hidden Markov Model
The MACD Hidden Markov Model strategy is built around MACD Hidden Markov Model.
Testing indicates an average annual return of about 61%. It performs best in the crypto market.
Signals trigger when Markov confirms trend changes on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like MacdFast, MacdSlow. Adjust these defaults to balance risk and reward.
Details
- Entry Criteria: see implementation for indicator conditions.
- Long/Short: Both directions.
- Exit Criteria: opposite signal or stop logic.
- Stops: Yes, using indicator-based calculations.
- Default Values:
MacdFast = 12MacdSlow = 26MacdSignal = 9CandleType = TimeSpan.FromMinutes(5).TimeFrame()HmmHistoryLength = 100
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Markov
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: Yes
- Divergence: No
- Risk Level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MACD strategy with Hidden Markov Model for state detection.
/// </summary>
public class MacdHmmStrategy : Strategy
{
private readonly StrategyParam<int> _macdFast;
private readonly StrategyParam<int> _macdSlow;
private readonly StrategyParam<int> _macdSignal;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _hmmHistoryLength;
private readonly StrategyParam<int> _signalCooldownBars;
private MovingAverageConvergenceDivergenceSignal _macd;
// Hidden Markov Model states
private enum MarketStates
{
Bullish,
Neutral,
Bearish
}
private MarketStates _currentState = MarketStates.Neutral;
// Data for HMM calculations
private readonly List<decimal> _priceChanges = [];
private readonly List<decimal> _volumes = [];
private decimal _prevPrice;
private decimal? _prevMacd;
private decimal? _prevSignal;
private int _cooldownRemaining;
/// <summary>
/// MACD fast period.
/// </summary>
public int MacdFast
{
get => _macdFast.Value;
set => _macdFast.Value = value;
}
/// <summary>
/// MACD slow period.
/// </summary>
public int MacdSlow
{
get => _macdSlow.Value;
set => _macdSlow.Value = value;
}
/// <summary>
/// MACD signal period.
/// </summary>
public int MacdSignal
{
get => _macdSignal.Value;
set => _macdSignal.Value = value;
}
/// <summary>
/// Candle type to use for the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Length of history for Hidden Markov Model.
/// </summary>
public int HmmHistoryLength
{
get => _hmmHistoryLength.Value;
set => _hmmHistoryLength.Value = value;
}
/// <summary>
/// Bars to wait between trading actions.
/// </summary>
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="MacdHmmStrategy"/>.
/// </summary>
public MacdHmmStrategy()
{
_macdFast = Param(nameof(MacdFast), 12)
.SetDisplay("MACD Fast Period", "Fast EMA period for MACD", "Indicators")
.SetOptimize(8, 20, 2);
_macdSlow = Param(nameof(MacdSlow), 26)
.SetDisplay("MACD Slow Period", "Slow EMA period for MACD", "Indicators")
.SetOptimize(20, 40, 2);
_macdSignal = Param(nameof(MacdSignal), 9)
.SetDisplay("MACD Signal Period", "Signal EMA period for MACD", "Indicators")
.SetOptimize(7, 15, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_hmmHistoryLength = Param(nameof(HmmHistoryLength), 100)
.SetDisplay("HMM History Length", "Length of history for Hidden Markov Model", "HMM Parameters")
.SetOptimize(50, 200, 10);
_signalCooldownBars = Param(nameof(SignalCooldownBars), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between position changes", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_currentState = MarketStates.Neutral;
_prevPrice = 0;
_prevMacd = null;
_prevSignal = null;
_cooldownRemaining = 0;
_priceChanges.Clear();
_volumes.Clear();
_macd?.Reset();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create MACD indicator
_macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFast },
LongMa = { Length = MacdSlow },
},
SignalMa = { Length = MacdSignal }
};
// Create subscription and bind indicator
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_macd, ProcessCandle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _macd);
DrawOwnTrades(area);
}
// Setup position protection
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(2, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue macdValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Check if strategy is ready to trade
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Update HMM data
UpdateHmmData(candle);
// Determine market state using HMM
CalculateMarketState();
if (_cooldownRemaining > 0)
_cooldownRemaining--;
if (macdValue is not IMovingAverageConvergenceDivergenceSignalValue macdTyped ||
macdTyped.Macd is not decimal macd ||
macdTyped.Signal is not decimal signal)
return;
if (_prevMacd is not decimal previousMacd || _prevSignal is not decimal previousSignal)
{
_prevMacd = macd;
_prevSignal = signal;
return;
}
var crossUp = previousMacd <= previousSignal && macd > signal;
var crossDown = previousMacd >= previousSignal && macd < signal;
var longExit = Position > 0 && (_currentState == MarketStates.Bearish || crossDown);
var shortExit = Position < 0 && (_currentState == MarketStates.Bullish || crossUp);
// Generate trade signals based on MACD transitions and HMM state.
if (longExit)
{
SellMarket(Position);
_cooldownRemaining = SignalCooldownBars;
}
else if (shortExit)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && crossUp && _currentState == MarketStates.Bullish && Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
else if (_cooldownRemaining == 0 && crossDown && _currentState == MarketStates.Bearish && Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
_prevMacd = macd;
_prevSignal = signal;
}
private void UpdateHmmData(ICandleMessage candle)
{
// Calculate price change
if (_prevPrice > 0)
{
decimal priceChange = candle.ClosePrice - _prevPrice;
_priceChanges.Add(priceChange);
_volumes.Add(candle.TotalVolume);
// Maintain the desired history length
while (_priceChanges.Count > HmmHistoryLength)
{
_priceChanges.RemoveAt(0);
_volumes.RemoveAt(0);
}
}
_prevPrice = candle.ClosePrice;
}
private void CalculateMarketState()
{
// Only perform state calculation when we have enough data
if (_priceChanges.Count < 10)
return;
// Simple HMM approximation using recent price changes and volume patterns
// Note: This is a simplified implementation - a real HMM would use proper state transition probabilities
// Calculate statistics of recent price changes
var priceChanges = _priceChanges.ToArray();
var volumes = _volumes.ToArray();
var startIndex = Math.Max(0, priceChanges.Length - 10);
var positiveChanges = 0;
var negativeChanges = 0;
for (var i = startIndex; i < priceChanges.Length; i++)
{
if (priceChanges[i] > 0)
positiveChanges++;
else if (priceChanges[i] < 0)
negativeChanges++;
}
// Calculate average volume for up and down days
decimal upVolume = 0;
decimal downVolume = 0;
int upCount = 0;
int downCount = 0;
for (var i = startIndex; i < priceChanges.Length; i++)
{
if (priceChanges[i] > 0)
{
upVolume += volumes[i];
upCount++;
}
else if (priceChanges[i] < 0)
{
downVolume += volumes[i];
downCount++;
}
}
upVolume = upCount > 0 ? upVolume / upCount : 0;
downVolume = downCount > 0 ? downVolume / downCount : 0;
// Determine market state based on price change direction and volume
if (positiveChanges >= 7 || (positiveChanges >= 6 && upVolume > downVolume * 1.5m))
{
_currentState = MarketStates.Bullish;
}
else if (negativeChanges >= 7 || (negativeChanges >= 6 && downVolume > upVolume * 1.5m))
{
_currentState = MarketStates.Bearish;
}
else
{
_currentState = MarketStates.Neutral;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_hidden_markov_model_strategy(Strategy):
"""
MACD strategy with Hidden Markov Model for state detection.
"""
BULLISH = 0
NEUTRAL = 1
BEARISH = 2
def __init__(self):
super(macd_hidden_markov_model_strategy, self).__init__()
self._macd_fast = self.Param("MacdFast", 12) \
.SetDisplay("MACD Fast Period", "Fast EMA period for MACD", "Indicators")
self._macd_slow = self.Param("MacdSlow", 26) \
.SetDisplay("MACD Slow Period", "Slow EMA period for MACD", "Indicators")
self._macd_signal = self.Param("MacdSignal", 9) \
.SetDisplay("MACD Signal Period", "Signal EMA period for MACD", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._hmm_history_length = self.Param("HmmHistoryLength", 100) \
.SetDisplay("HMM History Length", "Length of history for Hidden Markov Model", "HMM Parameters")
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 12) \
.SetGreaterThanZero() \
.SetDisplay("Signal Cooldown", "Bars to wait between position changes", "Trading")
self._current_state = macd_hidden_markov_model_strategy.NEUTRAL
self._price_changes = []
self._volumes = []
self._prev_price = 0.0
self._prev_macd = None
self._prev_signal = None
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(macd_hidden_markov_model_strategy, self).OnReseted()
self._current_state = macd_hidden_markov_model_strategy.NEUTRAL
self._prev_price = 0.0
self._prev_macd = None
self._prev_signal = None
self._cooldown_remaining = 0
self._price_changes = []
self._volumes = []
def OnStarted2(self, time):
super(macd_hidden_markov_model_strategy, self).OnStarted2(time)
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = int(self._macd_fast.Value)
macd.Macd.LongMa.Length = int(self._macd_slow.Value)
macd.SignalMa.Length = int(self._macd_signal.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(macd, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, macd)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(2, UnitTypes.Percent)
)
def _process_candle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
self._update_hmm_data(candle)
self._calculate_market_state()
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
macd_val = macd_value.Macd
signal_val = macd_value.Signal
if macd_val is None or signal_val is None:
return
macd_f = float(macd_val)
signal_f = float(signal_val)
if self._prev_macd is None or self._prev_signal is None:
self._prev_macd = macd_f
self._prev_signal = signal_f
return
cross_up = self._prev_macd <= self._prev_signal and macd_f > signal_f
cross_down = self._prev_macd >= self._prev_signal and macd_f < signal_f
long_exit = self.Position > 0 and (self._current_state == macd_hidden_markov_model_strategy.BEARISH or cross_down)
short_exit = self.Position < 0 and (self._current_state == macd_hidden_markov_model_strategy.BULLISH or cross_up)
cd = int(self._signal_cooldown_bars.Value)
if long_exit:
self.SellMarket(self.Position)
self._cooldown_remaining = cd
elif short_exit:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cd
elif self._cooldown_remaining == 0 and cross_up and self._current_state == macd_hidden_markov_model_strategy.BULLISH and self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
self._cooldown_remaining = cd
elif self._cooldown_remaining == 0 and cross_down and self._current_state == macd_hidden_markov_model_strategy.BEARISH and self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._cooldown_remaining = cd
self._prev_macd = macd_f
self._prev_signal = signal_f
def _update_hmm_data(self, candle):
close_price = float(candle.ClosePrice)
if self._prev_price > 0:
price_change = close_price - self._prev_price
self._price_changes.append(price_change)
self._volumes.append(float(candle.TotalVolume))
hmm_len = int(self._hmm_history_length.Value)
while len(self._price_changes) > hmm_len:
self._price_changes.pop(0)
self._volumes.pop(0)
self._prev_price = close_price
def _calculate_market_state(self):
if len(self._price_changes) < 10:
return
start_index = max(0, len(self._price_changes) - 10)
positive_changes = 0
negative_changes = 0
for i in range(start_index, len(self._price_changes)):
if self._price_changes[i] > 0:
positive_changes += 1
elif self._price_changes[i] < 0:
negative_changes += 1
up_volume = 0.0
down_volume = 0.0
up_count = 0
down_count = 0
for i in range(start_index, len(self._price_changes)):
if self._price_changes[i] > 0:
up_volume += self._volumes[i]
up_count += 1
elif self._price_changes[i] < 0:
down_volume += self._volumes[i]
down_count += 1
up_volume = up_volume / up_count if up_count > 0 else 0.0
down_volume = down_volume / down_count if down_count > 0 else 0.0
if positive_changes >= 7 or (positive_changes >= 6 and up_volume > down_volume * 1.5):
self._current_state = macd_hidden_markov_model_strategy.BULLISH
elif negative_changes >= 7 or (negative_changes >= 6 and down_volume > up_volume * 1.5):
self._current_state = macd_hidden_markov_model_strategy.BEARISH
else:
self._current_state = macd_hidden_markov_model_strategy.NEUTRAL
def CreateClone(self):
return macd_hidden_markov_model_strategy()