ADX Sentiment Momentum
The ADX Sentiment Momentum strategy is built around ADX Sentiment Momentum.
Signals trigger when its indicators confirms momentum shifts on intraday (5m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like AdxPeriod, AdxThreshold. 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:
AdxPeriod = 14AdxThreshold = 25mSentimentPeriod = 5StopLoss = 2mCandleType = 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.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>
/// ADX trend strategy filtered by deterministic sentiment momentum.
/// </summary>
public class AdxSentimentMomentumStrategy : Strategy
{
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<int> _sentimentPeriod;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private ADX _adx = null!;
private decimal _prevSentiment;
private decimal _currentSentiment;
private decimal _sentimentMomentum;
private decimal? _prevDiPlus;
private decimal? _prevDiMinus;
private int _cooldownRemaining;
/// <summary>
/// ADX period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// ADX threshold for strong trend.
/// </summary>
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
/// <summary>
/// Period for sentiment momentum calculation.
/// </summary>
public int SentimentPeriod
{
get => _sentimentPeriod.Value;
set => _sentimentPeriod.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Closed candles to wait before another position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type for strategy calculation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize strategy.
/// </summary>
public AdxSentimentMomentumStrategy()
{
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetRange(5, 30)
.SetDisplay("ADX Period", "Period for ADX calculation", "Indicators");
_adxThreshold = Param(nameof(AdxThreshold), 25m)
.SetRange(15m, 35m)
.SetDisplay("ADX Threshold", "Threshold for strong trend identification", "Indicators");
_sentimentPeriod = Param(nameof(SentimentPeriod), 5)
.SetRange(3, 10)
.SetDisplay("Sentiment Period", "Period for sentiment momentum calculation", "Sentiment");
_stopLoss = Param(nameof(StopLoss), 2m)
.SetRange(1m, 5m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 24)
.SetNotNegative()
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General");
_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();
_adx?.Reset();
_adx = null!;
_prevSentiment = 0m;
_currentSentiment = 0m;
_sentimentMomentum = 0m;
_prevDiPlus = null;
_prevDiMinus = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_adx = new ADX
{
Length = AdxPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_adx, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _adx);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(StopLoss, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
{
if (candle.State != CandleStates.Finished)
return;
UpdateSentiment(candle);
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (adxValue is not AverageDirectionalIndexValue typedAdx ||
typedAdx.MovingAverage is not decimal adxMain ||
typedAdx.Dx.Plus is not decimal diPlus ||
typedAdx.Dx.Minus is not decimal diMinus)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var bullishCross = _prevDiPlus is decimal previousPlus && _prevDiMinus is decimal previousMinus &&
previousPlus <= previousMinus && diPlus > diMinus;
var bearishCross = _prevDiPlus is decimal previousPlus2 && _prevDiMinus is decimal previousMinus2 &&
previousPlus2 >= previousMinus2 && diMinus > diPlus;
var strongTrend = adxMain >= AdxThreshold;
if (_cooldownRemaining == 0 && strongTrend && bullishCross && _sentimentMomentum > 0 && Position <= 0)
{
BuyMarket(Volume + (Position < 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (_cooldownRemaining == 0 && strongTrend && bearishCross && _sentimentMomentum < 0 && Position >= 0)
{
SellMarket(Volume + (Position > 0 ? Math.Abs(Position) : 0m));
_cooldownRemaining = CooldownBars;
}
else if (Position > 0 && (adxMain < 20m || _sentimentMomentum < 0))
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && (adxMain < 20m || _sentimentMomentum > 0))
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_prevDiPlus = diPlus;
_prevDiMinus = diMinus;
}
private void UpdateSentiment(ICandleMessage candle)
{
_prevSentiment = _currentSentiment;
_currentSentiment = SimulateSentiment(candle);
_sentimentMomentum = _currentSentiment - _prevSentiment;
}
private decimal SimulateSentiment(ICandleMessage candle)
{
var range = Math.Max(candle.HighPrice - candle.LowPrice, 1m);
var body = candle.ClosePrice - candle.OpenPrice;
var bodyRatio = body / range;
var rangeRatio = range / Math.Max(candle.OpenPrice, 1m);
var trendFactor = Math.Min(0.3m, rangeRatio * SentimentPeriod);
return Math.Max(-1m, Math.Min(1m, bodyRatio + (Math.Sign(body) * trendFactor)));
}
}
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, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import AverageDirectionalIndex
from StockSharp.Algo.Strategies import Strategy
class adx_sentiment_momentum_strategy(Strategy):
"""ADX trend strategy filtered by deterministic sentiment momentum."""
def __init__(self):
super(adx_sentiment_momentum_strategy, self).__init__()
self._adx_period = self.Param("AdxPeriod", 14) \
.SetRange(5, 30) \
.SetDisplay("ADX Period", "Period for ADX calculation", "Indicators")
self._adx_threshold = self.Param("AdxThreshold", 25.0) \
.SetRange(15.0, 35.0) \
.SetDisplay("ADX Threshold", "Threshold for strong trend identification", "Indicators")
self._sentiment_period = self.Param("SentimentPeriod", 5) \
.SetRange(3, 10) \
.SetDisplay("Sentiment Period", "Period for sentiment momentum calculation", "Sentiment")
self._stop_loss = self.Param("StopLoss", 2.0) \
.SetRange(1.0, 5.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 24) \
.SetNotNegative() \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._adx = None
self._prev_sentiment = 0.0
self._current_sentiment = 0.0
self._sentiment_momentum = 0.0
self._prev_di_plus = None
self._prev_di_minus = None
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(adx_sentiment_momentum_strategy, self).OnReseted()
self._adx = None
self._prev_sentiment = 0.0
self._current_sentiment = 0.0
self._sentiment_momentum = 0.0
self._prev_di_plus = None
self._prev_di_minus = None
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(adx_sentiment_momentum_strategy, self).OnStarted2(time)
self._adx = AverageDirectionalIndex()
self._adx.Length = int(self._adx_period.Value)
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(self._adx, self.ProcessCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._adx)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessCandle(self, candle, adx_value):
if candle.State != CandleStates.Finished:
return
self.UpdateSentiment(candle)
if not self.IsFormedAndOnlineAndAllowTrading():
return
adx_main_val = adx_value.MovingAverage
di_plus_val = adx_value.Dx.Plus
di_minus_val = adx_value.Dx.Minus
if adx_main_val is None or di_plus_val is None or di_minus_val is None:
return
adx_main = float(adx_main_val)
di_plus = float(di_plus_val)
di_minus = float(di_minus_val)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
adx_threshold = float(self._adx_threshold.Value)
cooldown = int(self._cooldown_bars.Value)
bullish_cross = self._prev_di_plus is not None and self._prev_di_minus is not None and \
self._prev_di_plus <= self._prev_di_minus and di_plus > di_minus
bearish_cross = self._prev_di_plus is not None and self._prev_di_minus is not None and \
self._prev_di_plus >= self._prev_di_minus and di_minus > di_plus
strong_trend = adx_main >= adx_threshold
if self._cooldown_remaining == 0 and strong_trend and bullish_cross and self._sentiment_momentum > 0 and self.Position <= 0:
vol = self.Volume
if self.Position < 0:
vol = self.Volume + Math.Abs(self.Position)
self.BuyMarket(vol)
self._cooldown_remaining = cooldown
elif self._cooldown_remaining == 0 and strong_trend and bearish_cross and self._sentiment_momentum < 0 and self.Position >= 0:
vol = self.Volume
if self.Position > 0:
vol = self.Volume + Math.Abs(self.Position)
self.SellMarket(vol)
self._cooldown_remaining = cooldown
elif self.Position > 0 and (adx_main < 20.0 or self._sentiment_momentum < 0):
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
elif self.Position < 0 and (adx_main < 20.0 or self._sentiment_momentum > 0):
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._prev_di_plus = di_plus
self._prev_di_minus = di_minus
def UpdateSentiment(self, candle):
self._prev_sentiment = self._current_sentiment
self._current_sentiment = self.SimulateSentiment(candle)
self._sentiment_momentum = self._current_sentiment - self._prev_sentiment
def SimulateSentiment(self, candle):
range_val = max(float(candle.HighPrice - candle.LowPrice), 1.0)
body = float(candle.ClosePrice - candle.OpenPrice)
body_ratio = body / range_val
range_ratio = range_val / max(float(candle.OpenPrice), 1.0)
sentiment_period = int(self._sentiment_period.Value)
trend_factor = min(0.3, range_ratio * sentiment_period)
sign = 1 if body > 0 else (-1 if body < 0 else 0)
result = body_ratio + (sign * trend_factor)
return max(-1.0, min(1.0, result))
def CreateClone(self):
return adx_sentiment_momentum_strategy()