QQE Signals Strategy
Implements the Quantitative Qualitative Estimation technique on RSI. The indicator builds dynamic upper and lower bands around a smoothed RSI line and tracks band crosses to signal trend changes. When RSI crosses above the trailing band a long signal is generated; crosses below trigger exits.
By adapting the bands to volatility, QQE seeks to smooth noise while remaining responsive. The strategy focuses on long trades and relies on the engine's trade reversals to close positions.
Details
- Entry Criteria:
- Long: RSI smoothed line crosses above the trailing band.
- Exit Criteria:
- RSI falls below the opposite band or an opposite signal appears.
- Indicators:
- RSI (period 14, smoothing 5)
- QQE bands derived from ATR of RSI with factor 4.238
- Stops: None by default; relies on opposite signals.
- Default Values:
RsiPeriod= 14RsiSmoothing= 5QqeFactor= 4.238Threshold= 10
- Filters:
- Trend-following
- Single timeframe
- Indicators: RSI, QQE
- Stops: None
- Complexity: Moderate
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// QQE Signals Strategy.
/// RSI is smoothed by an EMA of RsiSmoothing bars. The bar-to-bar change of that line is double smoothed over
/// 2 * RsiPeriod - 1 bars and multiplied by QqeFactor to build long and short trailing bands. The trailing line follows
/// the long band in an up trend and the short band in a down trend. When the smoothed RSI moves above the trailing
/// line a long is opened; when it falls below it the long is closed. Threshold marks the 50 +/- Threshold zone on the chart.
/// </summary>
public class QqeSignalsStrategy : Strategy
{
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _rsiSmoothing;
private readonly StrategyParam<decimal> _qqeFactor;
private readonly StrategyParam<decimal> _threshold;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _rsiMa;
private ExponentialMovingAverage _atrRsiMa;
private ExponentialMovingAverage _darMa;
private decimal? _prevRsiMa;
private decimal? _longBand;
private decimal? _shortBand;
private int _trend;
private int _aboveCount;
private int _belowCount;
/// <summary>
/// RSI period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// EMA smoothing of RSI.
/// </summary>
public int RsiSmoothing
{
get => _rsiSmoothing.Value;
set => _rsiSmoothing.Value = value;
}
/// <summary>
/// Multiplier of the smoothed RSI volatility.
/// </summary>
public decimal QqeFactor
{
get => _qqeFactor.Value;
set => _qqeFactor.Value = value;
}
/// <summary>
/// Distance of the reference zone from the 50 level.
/// </summary>
public decimal Threshold
{
get => _threshold.Value;
set => _threshold.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public QqeSignalsStrategy()
{
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI period", "QQE");
_rsiSmoothing = Param(nameof(RsiSmoothing), 5)
.SetGreaterThanZero()
.SetDisplay("RSI Smoothing", "EMA smoothing of RSI", "QQE");
_qqeFactor = Param(nameof(QqeFactor), 4.238m)
.SetGreaterThanZero()
.SetDisplay("QQE Factor", "Multiplier of the smoothed RSI volatility", "QQE");
_threshold = Param(nameof(Threshold), 10m)
.SetNotNegative()
.SetDisplay("Threshold", "Distance of the reference zone from the 50 level", "QQE");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_prevRsiMa = null;
_longBand = null;
_shortBand = null;
_trend = 1;
_aboveCount = 0;
_belowCount = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var wildersPeriod = RsiPeriod * 2 - 1;
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
_rsiMa = new ExponentialMovingAverage { Length = RsiSmoothing };
_atrRsiMa = new ExponentialMovingAverage { Length = wildersPeriod };
_darMa = new ExponentialMovingAverage { Length = wildersPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(rsi, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
var oscillators = CreateChartArea();
if (oscillators != null)
DrawIndicator(oscillators, rsi);
}
}
private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
var rsiMaValue = _rsiMa.Process(rsiValue, candle.OpenTime, true);
if (!_rsiMa.IsFormed)
return;
var rsiMa = rsiMaValue.ToDecimal();
var prevRsiMa = _prevRsiMa;
_prevRsiMa = rsiMa;
if (prevRsiMa is not decimal previous)
return;
var atrRsi = Math.Abs(previous - rsiMa);
var maAtrRsi = _atrRsiMa.Process(atrRsi, candle.OpenTime, true);
if (!_atrRsiMa.IsFormed)
return;
var dar = _darMa.Process(maAtrRsi.ToDecimal(), candle.OpenTime, true);
if (!_darMa.IsFormed)
return;
var delta = dar.ToDecimal() * QqeFactor;
var newLongBand = rsiMa - delta;
var newShortBand = rsiMa + delta;
var prevLongBand = _longBand;
var prevShortBand = _shortBand;
var longBand = prevLongBand is decimal pl && previous > pl && rsiMa > pl ? Math.Max(pl, newLongBand) : newLongBand;
var shortBand = prevShortBand is decimal ps && previous < ps && rsiMa < ps ? Math.Min(ps, newShortBand) : newShortBand;
if (prevShortBand is decimal s && previous <= s && rsiMa > s)
_trend = 1;
else if (prevLongBand is decimal l && previous >= l && rsiMa < l)
_trend = -1;
_longBand = longBand;
_shortBand = shortBand;
var trailingLine = _trend == 1 ? longBand : shortBand;
_aboveCount = trailingLine < rsiMa ? _aboveCount + 1 : 0;
_belowCount = trailingLine > rsiMa ? _belowCount + 1 : 0;
if (prevLongBand is null || prevShortBand is null)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_aboveCount == 1 && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (_belowCount == 1 && Position > 0)
SellMarket(Position);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import RelativeStrengthIndex, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class qqe_signals_strategy(Strategy):
"""
QQE Signals Strategy.
RSI is smoothed by an EMA of RsiSmoothing bars. The bar-to-bar change of that line is double smoothed over
2 * RsiPeriod - 1 bars and multiplied by QqeFactor to build long and short trailing bands. The trailing line follows
the long band in an up trend and the short band in a down trend. When the smoothed RSI moves above the trailing
line a long is opened; when it falls below it the long is closed. Threshold marks the 50 +/- Threshold zone on the chart.
"""
def __init__(self):
super(qqe_signals_strategy, self).__init__()
self._rsi_period = self.Param("RsiPeriod", 14).SetGreaterThanZero().SetDisplay("RSI Period", "RSI period", "QQE")
self._rsi_smoothing = self.Param("RsiSmoothing", 5).SetGreaterThanZero().SetDisplay("RSI Smoothing", "EMA smoothing of RSI", "QQE")
self._qqe_factor = self.Param("QqeFactor", 4.238).SetGreaterThanZero().SetDisplay("QQE Factor", "Multiplier of the smoothed RSI volatility", "QQE")
self._threshold = self.Param("Threshold", 10.0).SetNotNegative().SetDisplay("Threshold", "Distance of the reference zone from the 50 level", "QQE")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._rsi_ma = None
self._atr_rsi_ma = None
self._dar_ma = None
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._prev_rsi_ma = None
self._long_band = None
self._short_band = None
self._trend = 1
self._above_count = 0
self._below_count = 0
def OnReseted(self):
super(qqe_signals_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(qqe_signals_strategy, self).OnStarted2(time)
self._reset_state()
wilders_period = self._rsi_period.Value * 2 - 1
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
self._rsi_ma = ExponentialMovingAverage()
self._rsi_ma.Length = self._rsi_smoothing.Value
self._atr_rsi_ma = ExponentialMovingAverage()
self._atr_rsi_ma.Length = wilders_period
self._dar_ma = ExponentialMovingAverage()
self._dar_ma.Length = wilders_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(rsi, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
oscillators = self.CreateChartArea()
if oscillators is not None:
self.DrawIndicator(oscillators, rsi)
def _process_candle(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
rsi_ma_result = process_float(self._rsi_ma, rsi_value, candle.OpenTime, True)
if not self._rsi_ma.IsFormed:
return
rsi_ma = float(rsi_ma_result.GetValue[Decimal](None))
previous = self._prev_rsi_ma
self._prev_rsi_ma = rsi_ma
if previous is None:
return
atr_rsi = abs(previous - rsi_ma)
ma_atr_rsi = process_float(self._atr_rsi_ma, atr_rsi, candle.OpenTime, True)
if not self._atr_rsi_ma.IsFormed:
return
dar = process_float(self._dar_ma, ma_atr_rsi.GetValue[Decimal](None), candle.OpenTime, True)
if not self._dar_ma.IsFormed:
return
delta = float(dar.GetValue[Decimal](None)) * float(self._qqe_factor.Value)
new_long_band = rsi_ma - delta
new_short_band = rsi_ma + delta
prev_long_band = self._long_band
prev_short_band = self._short_band
if prev_long_band is not None and previous > prev_long_band and rsi_ma > prev_long_band:
long_band = max(prev_long_band, new_long_band)
else:
long_band = new_long_band
if prev_short_band is not None and previous < prev_short_band and rsi_ma < prev_short_band:
short_band = min(prev_short_band, new_short_band)
else:
short_band = new_short_band
if prev_short_band is not None and previous <= prev_short_band and rsi_ma > prev_short_band:
self._trend = 1
elif prev_long_band is not None and previous >= prev_long_band and rsi_ma < prev_long_band:
self._trend = -1
self._long_band = long_band
self._short_band = short_band
trailing_line = long_band if self._trend == 1 else short_band
self._above_count = self._above_count + 1 if trailing_line < rsi_ma else 0
self._below_count = self._below_count + 1 if trailing_line > rsi_ma else 0
if prev_long_band is None or prev_short_band is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._above_count == 1 and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif self._below_count == 1 and self.Position > 0:
self.SellMarket(self.Position)
def CreateClone(self):
return qqe_signals_strategy()