MACD Sentiment Filter
The MACD Sentiment Filter strategy is built around MACD Sentiment Filter.
Signals trigger when its indicators confirms filtered entries on intraday (15m) 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 = 9Threshold = 0.5mStopLoss = 2mCandleType = TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: multiple indicators
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday (15m)
- 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 Sentiment Filter strategy.
/// Entry condition:
/// Long: MACD > Signal && Sentiment_Score > Threshold
/// Short: MACD < Signal && Sentiment_Score < -Threshold
/// Exit condition:
/// Long: MACD < Signal
/// Short: MACD > Signal
/// </summary>
public class MacdWithSentimentFilterStrategy : Strategy
{
private readonly StrategyParam<int> _macdFast;
private readonly StrategyParam<int> _macdSlow;
private readonly StrategyParam<int> _macdSignal;
private readonly StrategyParam<decimal> _threshold;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
// Sentiment score from external data source (simplified with simulation for this example)
private decimal _sentimentScore;
// Last MACD and Signal values stored from the previous candle
private decimal _prevMacd;
private decimal _prevSignal;
private bool _hasPreviousMacd;
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>
/// Sentiment threshold for entry signal.
/// </summary>
public decimal Threshold
{
get => _threshold.Value;
set => _threshold.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Bars to wait between position changes.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Type of candles to use.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor with default parameters.
/// </summary>
public MacdWithSentimentFilterStrategy()
{
_macdFast = Param(nameof(MacdFast), 12)
.SetGreaterThanZero()
.SetDisplay("MACD Fast", "Fast moving average period for MACD", "MACD Settings")
.SetOptimize(8, 20, 1);
_macdSlow = Param(nameof(MacdSlow), 26)
.SetGreaterThanZero()
.SetDisplay("MACD Slow", "Slow moving average period for MACD", "MACD Settings")
.SetOptimize(20, 34, 2);
_macdSignal = Param(nameof(MacdSignal), 9)
.SetGreaterThanZero()
.SetDisplay("MACD Signal", "Signal line period for MACD", "MACD Settings")
.SetOptimize(5, 13, 1);
_threshold = Param(nameof(Threshold), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Sentiment Threshold", "Threshold for sentiment filter", "Sentiment Settings")
.SetOptimize(0.2m, 0.8m, 0.1m);
_cooldownBars = Param(nameof(CooldownBars), 24)
.SetNotNegative()
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General");
_stopLoss = Param(nameof(StopLoss), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (%)", "Stop Loss percentage from entry price", "Risk Management")
.SetOptimize(1m, 3m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).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();
// reset stored values
_prevMacd = default;
_prevSignal = default;
_sentimentScore = default;
_hasPreviousMacd = default;
_cooldownRemaining = default;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Create MACD indicator
var macd = new MovingAverageConvergenceDivergenceSignal
{
Macd =
{
ShortMa = { Length = MacdFast },
LongMa = { Length = MacdSlow },
},
SignalMa = { Length = MacdSignal }
};
var _macdInd = macd;
// Subscribe to candles
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(c => ProcessCandle(c, _macdInd))
.Start();
// Create chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, macd);
DrawOwnTrades(area);
}
// Enable position protection with stop-loss
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(StopLoss, UnitTypes.Percent)
);
}
/// <summary>
/// Process each candle and MACD values.
/// </summary>
private void ProcessCandle(ICandleMessage candle, MovingAverageConvergenceDivergenceSignal macdInd)
{
if (candle.State != CandleStates.Finished)
return;
UpdateSentimentScore(candle);
var macdResult = macdInd.Process(candle);
if (!macdInd.IsFormed)
return;
var macdTyped = (MovingAverageConvergenceDivergenceSignalValue)macdResult;
if (macdTyped.Macd is not decimal macd || macdTyped.Signal is not decimal signal)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
if (!_hasPreviousMacd)
{
_prevMacd = macd;
_prevSignal = signal;
_hasPreviousMacd = true;
return;
}
// Store previous MACD values for state tracking
var prevMacdOverSignal = _prevMacd > _prevSignal;
var currMacdOverSignal = macd > signal;
// Entry conditions with sentiment filter
if (_cooldownRemaining == 0 && prevMacdOverSignal != currMacdOverSignal && Position == 0)
{
if (currMacdOverSignal && _sentimentScore > Threshold)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (!currMacdOverSignal && _sentimentScore < -Threshold)
{
SellMarket();
_cooldownRemaining = CooldownBars;
}
}
_prevMacd = macd;
_prevSignal = signal;
}
/// <summary>
/// Update sentiment score based on candle data (simulation).
/// In a real implementation, this would fetch data from an external source.
/// </summary>
private void UpdateSentimentScore(ICandleMessage candle)
{
var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
var totalSize = candle.HighPrice - candle.LowPrice;
if (totalSize == 0)
return;
var bodyRatio = bodySize / totalSize;
_sentimentScore *= 0.85m;
// Bullish candle with strong body
if (candle.ClosePrice > candle.OpenPrice && bodyRatio > 0.7m)
{
_sentimentScore = Math.Min(_sentimentScore + 0.25m, 1m);
}
// Bearish candle with strong body
else if (candle.ClosePrice < candle.OpenPrice && bodyRatio > 0.7m)
{
_sentimentScore = Math.Max(_sentimentScore - 0.25m, -1m);
}
LogInfo($"Updated sentiment score: {_sentimentScore}");
}
}
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, MovingAverageConvergenceDivergenceSignalValue, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class macd_with_sentiment_filter_strategy(Strategy):
"""
MACD with Sentiment Filter strategy.
Entry condition:
Long: MACD > Signal && Sentiment_Score > Threshold
Short: MACD < Signal && Sentiment_Score < -Threshold
"""
def __init__(self):
super(macd_with_sentiment_filter_strategy, self).__init__()
self._macd_fast = self.Param("MacdFast", 12) \
.SetGreaterThanZero() \
.SetDisplay("MACD Fast", "Fast moving average period for MACD", "MACD Settings")
self._macd_slow = self.Param("MacdSlow", 26) \
.SetGreaterThanZero() \
.SetDisplay("MACD Slow", "Slow moving average period for MACD", "MACD Settings")
self._macd_signal = self.Param("MacdSignal", 9) \
.SetGreaterThanZero() \
.SetDisplay("MACD Signal", "Signal line period for MACD", "MACD Settings")
self._threshold = self.Param("Threshold", 0.1) \
.SetGreaterThanZero() \
.SetDisplay("Sentiment Threshold", "Threshold for sentiment filter", "Sentiment Settings")
self._cooldown_bars = self.Param("CooldownBars", 24) \
.SetNotNegative() \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General")
self._stop_loss = self.Param("StopLoss", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (%)", "Stop Loss percentage from entry price", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._sentiment_score = 0.0
self._prev_macd = 0.0
self._prev_signal = 0.0
self._has_previous_macd = False
self._cooldown_remaining = 0
self._macd_ind = None
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(macd_with_sentiment_filter_strategy, self).OnReseted()
self._prev_macd = 0.0
self._prev_signal = 0.0
self._sentiment_score = 0.0
self._has_previous_macd = False
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(macd_with_sentiment_filter_strategy, self).OnStarted2(time)
self._macd_ind = MovingAverageConvergenceDivergenceSignal()
self._macd_ind.Macd.ShortMa.Length = int(self._macd_fast.Value)
self._macd_ind.Macd.LongMa.Length = int(self._macd_slow.Value)
self._macd_ind.SignalMa.Length = int(self._macd_signal.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._macd_ind)
self.DrawOwnTrades(area)
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self.UpdateSentimentScore(candle)
civ = CandleIndicatorValue(self._macd_ind, candle)
civ.IsFinal = True
macd_result = self._macd_ind.Process(civ)
if not self._macd_ind.IsFormed:
return
macd_typed = macd_result
macd_val = macd_typed.Macd
signal_val = macd_typed.Signal
if macd_val is None or signal_val is None:
return
macd = float(macd_val)
signal = float(signal_val)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
if not self._has_previous_macd:
self._prev_macd = macd
self._prev_signal = signal
self._has_previous_macd = True
return
prev_macd_over_signal = self._prev_macd > self._prev_signal
curr_macd_over_signal = macd > signal
threshold = float(self._threshold.Value)
cooldown = int(self._cooldown_bars.Value)
if self._cooldown_remaining == 0 and prev_macd_over_signal != curr_macd_over_signal and self.Position == 0:
if curr_macd_over_signal and self._sentiment_score > threshold:
self.BuyMarket()
self._cooldown_remaining = cooldown
elif not curr_macd_over_signal and self._sentiment_score < -threshold:
self.SellMarket()
self._cooldown_remaining = cooldown
self._prev_macd = macd
self._prev_signal = signal
def UpdateSentimentScore(self, candle):
body_size = float(abs(candle.ClosePrice - candle.OpenPrice))
total_size = float(candle.HighPrice - candle.LowPrice)
if total_size == 0:
return
body_ratio = body_size / total_size
self._sentiment_score *= 0.85
if candle.ClosePrice > candle.OpenPrice and body_ratio > 0.7:
self._sentiment_score = min(self._sentiment_score + 0.25, 1.0)
elif candle.ClosePrice < candle.OpenPrice and body_ratio > 0.7:
self._sentiment_score = max(self._sentiment_score - 0.25, -1.0)
self.LogInfo("Updated sentiment score: {0}".format(self._sentiment_score))
def CreateClone(self):
return macd_with_sentiment_filter_strategy()