MACD Volume Cluster
The MACD Volume Cluster strategy is built around MACD Volume Cluster.
Signals trigger when its indicators confirms trend changes on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like FastMacdPeriod, SlowMacdPeriod. 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:
FastMacdPeriod = 12SlowMacdPeriod = 26MacdSignalPeriod = 9VolumePeriod = 20VolumeDeviationFactor = 2.0mCandleType = TimeSpan.FromMinutes(5).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: multiple indicators
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (5m)
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// MACD with Volume Cluster strategy.
/// Enters positions when MACD signal coincides with abnormal volume spike.
/// </summary>
public class MacdVolumeClusterStrategy : Strategy
{
private readonly StrategyParam<int> _fastMacdPeriod;
private readonly StrategyParam<int> _slowMacdPeriod;
private readonly StrategyParam<int> _macdSignalPeriod;
private readonly StrategyParam<int> _volumePeriod;
private readonly StrategyParam<decimal> _volumeDeviationFactor;
private readonly StrategyParam<DataType> _candleType;
private decimal _avgVolume;
private decimal _volumeStdDev;
private int _processedCandles;
/// <summary>
/// Fast MACD EMA period.
/// </summary>
public int FastMacdPeriod
{
get => _fastMacdPeriod.Value;
set => _fastMacdPeriod.Value = value;
}
/// <summary>
/// Slow MACD EMA period.
/// </summary>
public int SlowMacdPeriod
{
get => _slowMacdPeriod.Value;
set => _slowMacdPeriod.Value = value;
}
/// <summary>
/// MACD signal line period.
/// </summary>
public int MacdSignalPeriod
{
get => _macdSignalPeriod.Value;
set => _macdSignalPeriod.Value = value;
}
/// <summary>
/// Period for volume average calculation.
/// </summary>
public int VolumePeriod
{
get => _volumePeriod.Value;
set => _volumePeriod.Value = value;
}
/// <summary>
/// Volume deviation factor for volume spike detection.
/// </summary>
public decimal VolumeDeviationFactor
{
get => _volumeDeviationFactor.Value;
set => _volumeDeviationFactor.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize strategy.
/// </summary>
public MacdVolumeClusterStrategy()
{
_fastMacdPeriod = Param(nameof(FastMacdPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("Fast MACD Period", "Period for fast EMA in MACD calculation", "MACD Settings")
.SetOptimize(8, 16, 2);
_slowMacdPeriod = Param(nameof(SlowMacdPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("Slow MACD Period", "Period for slow EMA in MACD calculation", "MACD Settings")
.SetOptimize(20, 30, 2);
_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal Period", "Period for signal line in MACD calculation", "MACD Settings")
.SetOptimize(7, 12, 1);
_volumePeriod = Param(nameof(VolumePeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Volume Period", "Period for volume moving average calculation", "Volume Settings")
.SetOptimize(10, 30, 5);
_volumeDeviationFactor = Param(nameof(VolumeDeviationFactor), 2.0m)
.SetGreaterThanZero()
.SetDisplay("Volume Deviation Factor", "Factor multiplied by standard deviation to detect volume spikes", "Volume Settings")
.SetOptimize(1.5m, 3.0m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_avgVolume = 0;
_volumeStdDev = 0;
_processedCandles = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create MACD indicator
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = FastMacdPeriod },
LongMa = { Length = SlowMacdPeriod },
},
SignalMa = { Length = MacdSignalPeriod }
};
// Create volume-based indicators
var smaVolume = new SMA
{
Length = VolumePeriod
};
var stdDevVolume = new StandardDeviation
{
Length = VolumePeriod
};
// Create subscription for candles
var subscription = SubscribeCandles(CandleType);
// Bind MACD and process volume separately
subscription
.BindEx(macd, ProcessMacdAndVolume)
.Start();
// Start position protection
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, macd);
DrawOwnTrades(area);
}
}
private void ProcessMacdAndVolume(ICandleMessage candle, IIndicatorValue macdValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Calculate volume statistics
_processedCandles++;
// Using exponential moving average approach for volume statistics
// to avoid keeping large arrays of historical volumes
if (_processedCandles == 1)
{
_avgVolume = candle.TotalVolume;
_volumeStdDev = 0;
}
else
{
// Update average volume with smoothing factor
decimal alpha = 2.0m / (VolumePeriod + 1);
decimal oldAvg = _avgVolume;
_avgVolume = alpha * candle.TotalVolume + (1 - alpha) * _avgVolume;
// Update standard deviation (simplified approach)
decimal volumeDev = Math.Abs(candle.TotalVolume - oldAvg);
_volumeStdDev = alpha * volumeDev + (1 - alpha) * _volumeStdDev;
}
// Check if strategy is ready to trade
if (!IsFormedAndOnlineAndAllowTrading())
return;
var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdValue;
var macd = macdTyped.Macd;
var signal = macdTyped.Signal;
// Determine if we have a volume spike
bool isVolumeSpike = candle.TotalVolume > (_avgVolume + VolumeDeviationFactor * _volumeStdDev);
// Log the values
LogInfo($"MACD: {macd}, Signal: {signal}, Volume: {candle.TotalVolume}, " +
$"Avg Volume: {_avgVolume}, StdDev: {_volumeStdDev}, Volume Spike: {isVolumeSpike}");
// Trading logic
if (isVolumeSpike)
{
// Buy signal: MACD line crosses above signal line with volume spike
if (macd > signal && Position <= 0)
{
// Close any existing short position
if (Position < 0)
BuyMarket(Math.Abs(Position));
// Open long position
BuyMarket(Volume);
LogInfo($"Buy signal: MACD ({macd}) > Signal ({signal}) with volume spike ({candle.TotalVolume})");
}
// Sell signal: MACD line crosses below signal line with volume spike
else if (macd < signal && Position >= 0)
{
// Close any existing long position
if (Position > 0)
SellMarket(Math.Abs(Position));
// Open short position
SellMarket(Volume);
LogInfo($"Sell signal: MACD ({macd}) < Signal ({signal}) with volume spike ({candle.TotalVolume})");
}
}
// Exit logic: MACD crosses back
if ((Position > 0 && macd < signal) ||
(Position < 0 && macd > signal))
{
ClosePosition();
LogInfo($"Exit signal: MACD and Signal crossed. Position closed at {candle.ClosePrice}");
}
}
}
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, UnitTypes, Unit
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
from StockSharp.Algo.Strategies import Strategy
class macd_volume_cluster_strategy(Strategy):
"""
MACD with Volume Cluster strategy.
Enters positions when MACD signal coincides with abnormal volume spike.
"""
def __init__(self):
super(macd_volume_cluster_strategy, self).__init__()
self._fast_macd_period = self.Param("FastMacdPeriod", 12) \
.SetGreaterThanZero() \
.SetDisplay("Fast MACD Period", "Period for fast EMA in MACD calculation", "MACD Settings") \
.SetCanOptimize(True) \
.SetOptimize(8, 16, 2)
self._slow_macd_period = self.Param("SlowMacdPeriod", 26) \
.SetGreaterThanZero() \
.SetDisplay("Slow MACD Period", "Period for slow EMA in MACD calculation", "MACD Settings") \
.SetCanOptimize(True) \
.SetOptimize(20, 30, 2)
self._macd_signal_period = self.Param("MacdSignalPeriod", 9) \
.SetGreaterThanZero() \
.SetDisplay("MACD Signal Period", "Period for signal line in MACD calculation", "MACD Settings") \
.SetCanOptimize(True) \
.SetOptimize(7, 12, 1)
self._volume_period = self.Param("VolumePeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Volume Period", "Period for volume moving average calculation", "Volume Settings") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 5)
self._volume_deviation_factor = self.Param("VolumeDeviationFactor", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Volume Deviation Factor", "Factor multiplied by standard deviation to detect volume spikes", "Volume Settings") \
.SetCanOptimize(True) \
.SetOptimize(1.5, 3.0, 0.5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._avg_volume = 0.0
self._volume_std_dev = 0.0
self._processed_candles = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(macd_volume_cluster_strategy, self).OnReseted()
self._avg_volume = 0.0
self._volume_std_dev = 0.0
self._processed_candles = 0
def OnStarted2(self, time):
super(macd_volume_cluster_strategy, self).OnStarted2(time)
macd = MovingAverageConvergenceDivergenceSignal()
macd.Macd.ShortMa.Length = int(self._fast_macd_period.Value)
macd.Macd.LongMa.Length = int(self._slow_macd_period.Value)
macd.SignalMa.Length = int(self._macd_signal_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(macd, self.ProcessMacdAndVolume).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent)
)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, macd)
self.DrawOwnTrades(area)
def ProcessMacdAndVolume(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
self._processed_candles += 1
volume = float(candle.TotalVolume)
vol_period = int(self._volume_period.Value)
vol_factor = float(self._volume_deviation_factor.Value)
if self._processed_candles == 1:
self._avg_volume = volume
self._volume_std_dev = 0.0
else:
alpha = 2.0 / (vol_period + 1)
old_avg = self._avg_volume
self._avg_volume = alpha * volume + (1.0 - alpha) * self._avg_volume
volume_dev = abs(volume - old_avg)
self._volume_std_dev = alpha * volume_dev + (1.0 - alpha) * self._volume_std_dev
if not self.IsFormedAndOnlineAndAllowTrading():
return
macd_line = macd_value.Macd
signal_line = macd_value.Signal
if macd_line is None or signal_line is None:
return
macd_line = float(macd_line)
signal_line = float(signal_line)
is_volume_spike = volume > (self._avg_volume + vol_factor * self._volume_std_dev)
if is_volume_spike:
if macd_line > signal_line and self.Position <= 0:
if self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.BuyMarket(self.Volume)
elif macd_line < signal_line and self.Position >= 0:
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.SellMarket(self.Volume)
if (self.Position > 0 and macd_line < signal_line) or \
(self.Position < 0 and macd_line > signal_line):
if self.Position > 0:
self.SellMarket(self.Position)
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
def CreateClone(self):
return macd_volume_cluster_strategy()