QQE Signals戦略
RSI上でQuantitative Qualitative Estimation技術を実装します。このインジケーターは平滑化されたRSIラインの周囲に動的な上部および下部バンドを構築し、バンドのクロスを追跡してトレンドの変化を示します。RSIがトレーリングバンドを上抜けするとロングシグナルが生成され、下抜けするとエグジットが発動します。
バンドをボラティリティに適応させることで、QQEはノイズを平滑化しながら反応性を維持しようとします。この戦略はロング取引に集中し、ポジションのクローズはエンジンのトレードリバーサルに依存します。
詳細
- エントリー条件:
- ロング: 平滑化されたRSIラインがトレーリングバンドを上抜けする。
- エグジット条件:
- RSIが反対側のバンドを下回るか、反対のシグナルが現れる。
- インジケーター:
- RSI (期間14、スムージング5)
- ファクター4.238のRSIのATRから導出されたQQEバンド
- ストップ: デフォルトでなし;反対のシグナルに依存。
- デフォルト値:
RsiPeriod= 14RsiSmoothing= 5QqeFactor= 4.238Threshold= 10
- フィルター:
- トレンドフォロー
- 単一時間軸
- インジケーター: RSI、QQE
- ストップ: なし
- 複雑さ: 中程度
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()