Donchian Sentiment Spike
The Donchian Sentiment Spike strategy is built around Donchian Sentiment Spike.
Testing indicates an average annual return of about 115%. It performs best in the stocks market.
Signals trigger when Donchian confirms trend changes on intraday (15m) data. This makes the method suitable for active traders.
Stops rely on ATR multiples and factors like DonchianPeriod, SentimentPeriod. 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:
DonchianPeriod = 20SentimentPeriod = 20SentimentMultiplier = 2mStopLoss = 2mCandleType = TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Donchian, Spike
- 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>
/// Donchian with Sentiment Spike strategy.
/// Entry condition:
/// Long: Price > Max(High, N) && Sentiment_Score > Avg(Sentiment, M) + k*StdDev(Sentiment, M)
/// Short: Price < Min(Low, N) && Sentiment_Score < Avg(Sentiment, M) - k*StdDev(Sentiment, M)
/// Exit condition:
/// Long: Price < (Max(High, N) + Min(Low, N))/2
/// Short: Price > (Max(High, N) + Min(Low, N))/2
/// </summary>
public class DonchianWithSentimentSpikeStrategy : Strategy
{
private readonly StrategyParam<int> _donchianPeriod;
private readonly StrategyParam<int> _sentimentPeriod;
private readonly StrategyParam<decimal> _sentimentMultiplier;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _sentimentHistory = [];
private decimal _sentimentAverage;
private decimal _sentimentStdDev;
private decimal _currentSentiment;
private decimal _midChannel;
/// <summary>
/// Donchian channel period.
/// </summary>
public int DonchianPeriod
{
get => _donchianPeriod.Value;
set => _donchianPeriod.Value = value;
}
/// <summary>
/// Sentiment averaging period.
/// </summary>
public int SentimentPeriod
{
get => _sentimentPeriod.Value;
set => _sentimentPeriod.Value = value;
}
/// <summary>
/// Sentiment standard deviation multiplier.
/// </summary>
public decimal SentimentMultiplier
{
get => _sentimentMultiplier.Value;
set => _sentimentMultiplier.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.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 DonchianWithSentimentSpikeStrategy()
{
_donchianPeriod = Param(nameof(DonchianPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Donchian Period", "Donchian channel period", "Donchian Settings")
.SetOptimize(10, 30, 5);
_sentimentPeriod = Param(nameof(SentimentPeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Sentiment Period", "Sentiment averaging period", "Sentiment Settings")
.SetOptimize(10, 30, 5);
_sentimentMultiplier = Param(nameof(SentimentMultiplier), 0.5m)
.SetGreaterThanZero()
.SetDisplay("Sentiment StdDev Multiplier", "Multiplier for sentiment standard deviation", "Sentiment Settings")
.SetOptimize(1m, 3m, 0.5m);
_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();
_midChannel = _sentimentAverage = _sentimentStdDev = _currentSentiment = default;
_sentimentHistory.Clear();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var highest = new Highest { Length = DonchianPeriod };
var lowest = new Lowest { Length = DonchianPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(highest, lowest, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent)
);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal upper, decimal lower)
{
if (candle.State != CandleStates.Finished)
return;
UpdateSentiment(candle);
var price = candle.ClosePrice;
// Long entry: Price breaks above upper band with positive sentiment
if (price >= upper && _currentSentiment > 0 && Position == 0)
{
BuyMarket();
}
// Short entry: Price breaks below lower band with negative sentiment
else if (price <= lower && _currentSentiment < 0 && Position == 0)
{
SellMarket();
}
}
/// <summary>
/// Update sentiment score based on candle data (simulation).
/// In a real implementation, this would fetch data from an external source.
/// </summary>
private void UpdateSentiment(ICandleMessage candle)
{
// Simple sentiment simulation based on price action
// In reality, this would come from social media or news sentiment API
decimal sentiment;
// Base sentiment on candle pattern
var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
var totalSize = candle.HighPrice - candle.LowPrice;
if (totalSize == 0)
{
sentiment = 0;
}
else
{
var bodyRatio = bodySize / totalSize;
// Bullish candle with strong body
if (candle.ClosePrice > candle.OpenPrice)
{
sentiment = bodyRatio * 2; // 0 to 2 scale
}
// Bearish candle with strong body
else
{
sentiment = -bodyRatio * 2; // -2 to 0 scale
}
// Use body ratio directly without randomness
}
// Ensure sentiment is within -2 to 2 range
sentiment = Math.Max(Math.Min(sentiment, 2m), -2m);
_currentSentiment = sentiment;
// Add to history
_sentimentHistory.Add(_currentSentiment);
if (_sentimentHistory.Count > SentimentPeriod)
{
_sentimentHistory.RemoveAt(0);
}
// Calculate average
decimal sum = 0;
foreach (var value in _sentimentHistory)
{
sum += value;
}
_sentimentAverage = _sentimentHistory.Count > 0
? sum / _sentimentHistory.Count
: 0;
// Calculate standard deviation
if (_sentimentHistory.Count > 1)
{
decimal sumSquaredDiffs = 0;
foreach (var value in _sentimentHistory)
{
var diff = value - _sentimentAverage;
sumSquaredDiffs += diff * diff;
}
_sentimentStdDev = (decimal)Math.Sqrt((double)(sumSquaredDiffs / (_sentimentHistory.Count - 1)));
}
else
{
_sentimentStdDev = 0.5m; // Default value until we have enough data
}
LogInfo($"Sentiment: {_currentSentiment}, Avg: {_sentimentAverage}, StdDev: {_sentimentStdDev}");
}
}
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 Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class donchian_with_sentiment_spike_strategy(Strategy):
"""
Donchian with Sentiment Spike strategy.
"""
def __init__(self):
super(donchian_with_sentiment_spike_strategy, self).__init__()
self._donchian_period = self.Param("DonchianPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Donchian Period", "Donchian channel period", "Donchian Settings") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 5)
self._sentiment_period = self.Param("SentimentPeriod", 10) \
.SetGreaterThanZero() \
.SetDisplay("Sentiment Period", "Sentiment averaging period", "Sentiment Settings") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 5)
self._sentiment_multiplier = self.Param("SentimentMultiplier", 0.5) \
.SetGreaterThanZero() \
.SetDisplay("Sentiment StdDev Multiplier", "Multiplier for sentiment standard deviation", "Sentiment Settings") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
self._stop_loss = self.Param("StopLoss", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss (%)", "Stop Loss percentage from entry price", "Risk Management") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._sentiment_history = []
self._sentiment_average = 0.0
self._sentiment_std_dev = 0.0
self._current_sentiment = 0.0
@property
def DonchianPeriod(self):
return self._donchian_period.Value
@DonchianPeriod.setter
def DonchianPeriod(self, value):
self._donchian_period.Value = value
@property
def SentimentPeriod(self):
return self._sentiment_period.Value
@SentimentPeriod.setter
def SentimentPeriod(self, value):
self._sentiment_period.Value = value
@property
def SentimentMultiplier(self):
return self._sentiment_multiplier.Value
@SentimentMultiplier.setter
def SentimentMultiplier(self, value):
self._sentiment_multiplier.Value = value
@property
def StopLoss(self):
return self._stop_loss.Value
@StopLoss.setter
def StopLoss(self, value):
self._stop_loss.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def OnReseted(self):
super(donchian_with_sentiment_spike_strategy, self).OnReseted()
self._sentiment_history = []
self._sentiment_average = 0.0
self._sentiment_std_dev = 0.0
self._current_sentiment = 0.0
def OnStarted2(self, time):
super(donchian_with_sentiment_spike_strategy, self).OnStarted2(time)
highest = Highest()
highest.Length = self.DonchianPeriod
lowest = Lowest()
lowest.Length = self.DonchianPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(highest, lowest, self.ProcessCandle).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.DrawOwnTrades(area)
def ProcessCandle(self, candle, upper, lower):
if candle.State != CandleStates.Finished:
return
self.UpdateSentiment(candle)
price = float(candle.ClosePrice)
upper_val = float(upper)
lower_val = float(lower)
if self.Position != 0:
return
if price >= upper_val and self._current_sentiment > 0:
self.BuyMarket()
elif price <= lower_val and self._current_sentiment < 0:
self.SellMarket()
def UpdateSentiment(self, candle):
body_size = abs(float(candle.ClosePrice) - float(candle.OpenPrice))
total_size = float(candle.HighPrice) - float(candle.LowPrice)
if total_size == 0:
sentiment = 0.0
else:
body_ratio = body_size / total_size
if float(candle.ClosePrice) > float(candle.OpenPrice):
sentiment = body_ratio * 2.0
else:
sentiment = -body_ratio * 2.0
sentiment = max(min(sentiment, 2.0), -2.0)
self._current_sentiment = sentiment
self._sentiment_history.append(self._current_sentiment)
if len(self._sentiment_history) > self.SentimentPeriod:
self._sentiment_history.pop(0)
if len(self._sentiment_history) > 0:
self._sentiment_average = sum(self._sentiment_history) / len(self._sentiment_history)
else:
self._sentiment_average = 0.0
if len(self._sentiment_history) > 1:
sum_sq = 0.0
for v in self._sentiment_history:
diff = v - self._sentiment_average
sum_sq += diff * diff
self._sentiment_std_dev = Math.Sqrt(sum_sq / (len(self._sentiment_history) - 1))
else:
self._sentiment_std_dev = 0.5
def CreateClone(self):
return donchian_with_sentiment_spike_strategy()