Gaussian Detrended Reversion
Gaussian Detrended Reversion is a mean-reversion strategy that uses a detrended price oscillator smoothed with an Arnaud Legoux Moving Average (ALMA). Long positions open when the smoothed oscillator crosses above its lagged version while below zero; shorts open on downward crosses above zero. Positions exit on opposite crosses or when the oscillator crosses the zero line.
Details
- Data: Price candles.
- Entry Criteria:
- Long: ALMA-smoothed DPO crosses above its lag line and is below zero.
- Short: ALMA-smoothed DPO crosses below its lag line and is above zero.
- Exit Criteria: Opposite lag cross or zero-line cross.
- Stops: None.
- Default Values:
PriceLength= 52SmoothingLength= 52LagLength= 26
- Filters:
- Category: Mean reversion
- Direction: Long & Short
- Indicators: EMA, ALMA
- Complexity: Low
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Gaussian detrended reversion strategy.
/// The detrended price oscillator is the close minus the PriceLength EMA from PriceLength / 2 + 1 candles ago. It is smoothed with an
/// ALMA of SmoothingLength, and the lag line is that value LagLength candles ago. The smoothed oscillator crossing above the lag line
/// below zero goes long, crossing below it above zero goes short. A long closes on a cross below the lag line or above zero, a short
/// on a cross above the lag line or below zero.
/// </summary>
public class GaussianDetrendedReversionStrategy : Strategy
{
private readonly StrategyParam<int> _priceLength;
private readonly StrategyParam<int> _smoothingLength;
private readonly StrategyParam<int> _lagLength;
private readonly StrategyParam<DataType> _candleType;
private ArnaudLegouxMovingAverage _alma;
private readonly List<decimal> _emaHistory = [];
private readonly List<decimal> _smoothHistory = [];
/// <summary>
/// EMA length used to detrend the price.
/// </summary>
public int PriceLength
{
get => _priceLength.Value;
set => _priceLength.Value = value;
}
/// <summary>
/// ALMA smoothing length.
/// </summary>
public int SmoothingLength
{
get => _smoothingLength.Value;
set => _smoothingLength.Value = value;
}
/// <summary>
/// Candles back for the lag line.
/// </summary>
public int LagLength
{
get => _lagLength.Value;
set => _lagLength.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public GaussianDetrendedReversionStrategy()
{
_priceLength = Param(nameof(PriceLength), 52)
.SetGreaterThanZero()
.SetDisplay("Price Length", "EMA length used to detrend the price", "Indicators");
_smoothingLength = Param(nameof(SmoothingLength), 52)
.SetGreaterThanZero()
.SetDisplay("Smoothing Length", "ALMA smoothing length", "Indicators");
_lagLength = Param(nameof(LagLength), 26)
.SetGreaterThanZero()
.SetDisplay("Lag Length", "Candles back for the lag line", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).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();
_emaHistory.Clear();
_smoothHistory.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_emaHistory.Clear();
_smoothHistory.Clear();
var ema = new ExponentialMovingAverage { Length = PriceLength };
_alma = new ArnaudLegouxMovingAverage { Length = SmoothingLength };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal ema)
{
if (candle.State != CandleStates.Finished)
return;
var barsBack = PriceLength / 2 + 1;
_emaHistory.Add(ema);
if (_emaHistory.Count > barsBack + 1)
_emaHistory.RemoveAt(0);
if (_emaHistory.Count <= barsBack)
return;
var dpo = candle.ClosePrice - _emaHistory[0];
var almaValue = _alma.Process(new DecimalIndicatorValue(_alma, dpo, candle.OpenTime) { IsFinal = true });
if (!_alma.IsFormed)
return;
// The history keeps the current value, the previous one and the lag values for both.
_smoothHistory.Add(almaValue.GetValue<decimal>());
if (_smoothHistory.Count > LagLength + 2)
_smoothHistory.RemoveAt(0);
if (_smoothHistory.Count < LagLength + 2)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var count = _smoothHistory.Count;
var smooth = _smoothHistory[count - 1];
var prevSmooth = _smoothHistory[count - 2];
var lag = _smoothHistory[count - 1 - LagLength];
var prevLag = _smoothHistory[count - 2 - LagLength];
var crossUp = prevSmooth <= prevLag && smooth > lag;
var crossDown = prevSmooth >= prevLag && smooth < lag;
var zeroUp = prevSmooth <= 0 && smooth > 0;
var zeroDown = prevSmooth >= 0 && smooth < 0;
if (crossUp && smooth < 0 && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (crossDown && smooth > 0 && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
else if (Position > 0 && (crossDown || zeroUp))
SellMarket(Position);
else if (Position < 0 && (crossUp || zeroDown))
BuyMarket(Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, ArnaudLegouxMovingAverage, DecimalIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class gaussian_detrended_reversion_strategy(Strategy):
"""
Gaussian detrended reversion strategy.
The detrended price oscillator is the close minus the PriceLength EMA from PriceLength / 2 + 1 candles ago. It is smoothed with an
ALMA of SmoothingLength, and the lag line is that value LagLength candles ago. The smoothed oscillator crossing above the lag line
below zero goes long, crossing below it above zero goes short. A long closes on a cross below the lag line or above zero, a short
on a cross above the lag line or below zero.
"""
def __init__(self):
super(gaussian_detrended_reversion_strategy, self).__init__()
self._price_length = self.Param("PriceLength", 52).SetGreaterThanZero().SetDisplay("Price Length", "EMA length used to detrend the price", "Indicators")
self._smoothing_length = self.Param("SmoothingLength", 52).SetGreaterThanZero().SetDisplay("Smoothing Length", "ALMA smoothing length", "Indicators")
self._lag_length = self.Param("LagLength", 26).SetGreaterThanZero().SetDisplay("Lag Length", "Candles back for the lag line", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._alma = None
self._ema_history = []
self._smooth_history = []
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(gaussian_detrended_reversion_strategy, self).OnReseted()
self._ema_history = []
self._smooth_history = []
def OnStarted2(self, time):
super(gaussian_detrended_reversion_strategy, self).OnStarted2(time)
self._ema_history = []
self._smooth_history = []
ema = ExponentialMovingAverage()
ema.Length = self._price_length.Value
self._alma = ArnaudLegouxMovingAverage()
self._alma.Length = self._smoothing_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ema):
if candle.State != CandleStates.Finished:
return
bars_back = self._price_length.Value // 2 + 1
lag_length = self._lag_length.Value
self._ema_history.append(ema)
if len(self._ema_history) > bars_back + 1:
self._ema_history.pop(0)
if len(self._ema_history) <= bars_back:
return
dpo = candle.ClosePrice - self._ema_history[0]
alma_input = DecimalIndicatorValue(self._alma, dpo, candle.OpenTime)
alma_input.IsFinal = True
alma_value = self._alma.Process(alma_input)
if not self._alma.IsFormed:
return
# The history keeps the current value, the previous one and the lag values for both.
self._smooth_history.append(alma_value.GetValue[Decimal](None))
if len(self._smooth_history) > lag_length + 2:
self._smooth_history.pop(0)
if len(self._smooth_history) < lag_length + 2:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
count = len(self._smooth_history)
smooth = self._smooth_history[count - 1]
prev_smooth = self._smooth_history[count - 2]
lag = self._smooth_history[count - 1 - lag_length]
prev_lag = self._smooth_history[count - 2 - lag_length]
cross_up = prev_smooth <= prev_lag and smooth > lag
cross_down = prev_smooth >= prev_lag and smooth < lag
zero_up = prev_smooth <= 0 and smooth > 0
zero_down = prev_smooth >= 0 and smooth < 0
if cross_up and smooth < 0 and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif cross_down and smooth > 0 and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self.Position > 0 and (cross_down or zero_up):
self.SellMarket(self.Position)
elif self.Position < 0 and (cross_up or zero_down):
self.BuyMarket(abs(self.Position))
def CreateClone(self):
return gaussian_detrended_reversion_strategy()