Parabolic SAR Distance Mean Reversion
The Parabolic SAR Distance Mean Reversion strategy focuses on extreme readings of the Parabolic to exploit reversion. Wide departures from the normal level rarely last.
Trades trigger when the indicator swings far from its mean and then begins to reverse. Both long and short setups include a protective stop.
Suited for swing traders expecting oscillations, the strategy closes out once the Parabolic returns toward balance. Starting parameter AccelerationFactor = 0.02m.
Details
- Entry Criteria: Indicator crosses back toward mean.
- Long/Short: Both directions.
- Exit Criteria: Indicator reverts to average.
- Stops: Yes.
- Default Values:
AccelerationFactor= 0.02mAccelerationLimit= 0.2mLookbackPeriod= 20DeviationMultiplier= 2.0mCandleType= TimeSpan.FromMinutes(5)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: Parabolic
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Short-term
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Parabolic SAR distance mean reversion strategy.
/// Trades large deviations of price from a locally calculated Parabolic SAR level and exits when the distance returns to its recent average.
/// </summary>
public class ParabolicSarDistanceMeanReversionStrategy : Strategy
{
private readonly StrategyParam<decimal> _accelerationFactor;
private readonly StrategyParam<decimal> _accelerationLimit;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal[] _distanceHistory;
private int _currentIndex;
private int _filledCount;
private int _cooldown;
private bool _isInitialized;
private bool _isBullishTrend;
private decimal _sarValue;
private decimal _extremePoint;
private decimal _acceleration;
private decimal _previousHigh;
private decimal _previousLow;
/// <summary>
/// Acceleration factor for Parabolic SAR.
/// </summary>
public decimal AccelerationFactor
{
get => _accelerationFactor.Value;
set => _accelerationFactor.Value = value;
}
/// <summary>
/// Acceleration limit for Parabolic SAR.
/// </summary>
public decimal AccelerationLimit
{
get => _accelerationLimit.Value;
set => _accelerationLimit.Value = value;
}
/// <summary>
/// Lookback period for distance statistics.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Deviation multiplier for mean reversion detection.
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Cooldown bars between orders.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="ParabolicSarDistanceMeanReversionStrategy"/>.
/// </summary>
public ParabolicSarDistanceMeanReversionStrategy()
{
_accelerationFactor = Param(nameof(AccelerationFactor), 0.02m)
.SetGreaterThanZero()
.SetDisplay("Acceleration Factor", "Acceleration factor for Parabolic SAR", "Parabolic SAR")
.SetOptimize(0.01m, 0.05m, 0.01m);
_accelerationLimit = Param(nameof(AccelerationLimit), 0.2m)
.SetGreaterThanZero()
.SetDisplay("Acceleration Limit", "Acceleration limit for Parabolic SAR", "Parabolic SAR")
.SetOptimize(0.1m, 0.3m, 0.05m);
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Lookback period for distance statistics", "Strategy Parameters")
.SetOptimize(10, 50, 5);
_deviationMultiplier = Param(nameof(DeviationMultiplier), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Deviation Multiplier", "Deviation multiplier for mean reversion detection", "Strategy Parameters")
.SetOptimize(1m, 3m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management");
_cooldownBars = Param(nameof(CooldownBars), 1200)
.SetRange(1, 5000)
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type for strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_distanceHistory = new decimal[LookbackPeriod];
_currentIndex = default;
_filledCount = default;
_cooldown = default;
_isInitialized = default;
_isBullishTrend = default;
_sarValue = default;
_extremePoint = default;
_acceleration = default;
_previousHigh = default;
_previousLow = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_distanceHistory = new decimal[LookbackPeriod];
_currentIndex = 0;
_filledCount = 0;
_cooldown = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
StartProtection(new(), new Unit(StopLossPercent, UnitTypes.Percent));
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!_isInitialized)
{
InitializeState(candle);
return;
}
UpdateSar(candle);
var distance = Math.Abs(candle.ClosePrice - _sarValue);
_distanceHistory[_currentIndex] = distance;
_currentIndex = (_currentIndex + 1) % LookbackPeriod;
if (_filledCount < LookbackPeriod)
_filledCount++;
if (_filledCount < LookbackPeriod)
{
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
return;
}
var avgDistance = 0m;
var sumSq = 0m;
for (var i = 0; i < LookbackPeriod; i++)
avgDistance += _distanceHistory[i];
avgDistance /= LookbackPeriod;
for (var i = 0; i < LookbackPeriod; i++)
{
var diff = _distanceHistory[i] - avgDistance;
sumSq += diff * diff;
}
var stdDistance = (decimal)Math.Sqrt((double)(sumSq / LookbackPeriod));
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
return;
}
var extendedThreshold = avgDistance + stdDistance * DeviationMultiplier;
var priceAboveSar = candle.ClosePrice > _sarValue;
var priceBelowSar = candle.ClosePrice < _sarValue;
if (Position == 0)
{
if (distance > extendedThreshold)
{
if (priceAboveSar)
{
SellMarket();
_cooldown = CooldownBars;
}
else if (priceBelowSar)
{
BuyMarket();
_cooldown = CooldownBars;
}
}
}
else if (Position > 0 && (distance <= avgDistance || priceAboveSar))
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
else if (Position < 0 && (distance <= avgDistance || priceBelowSar))
{
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
}
private void InitializeState(ICandleMessage candle)
{
_isBullishTrend = candle.ClosePrice >= candle.OpenPrice;
_sarValue = _isBullishTrend ? candle.LowPrice : candle.HighPrice;
_extremePoint = _isBullishTrend ? candle.HighPrice : candle.LowPrice;
_acceleration = AccelerationFactor;
_previousHigh = candle.HighPrice;
_previousLow = candle.LowPrice;
_isInitialized = true;
}
private void UpdateSar(ICandleMessage candle)
{
_sarValue += _acceleration * (_extremePoint - _sarValue);
if (_isBullishTrend)
{
_sarValue = Math.Min(_sarValue, _previousLow);
if (candle.LowPrice <= _sarValue)
{
_isBullishTrend = false;
_sarValue = _extremePoint;
_extremePoint = candle.LowPrice;
_acceleration = AccelerationFactor;
}
else if (candle.HighPrice > _extremePoint)
{
_extremePoint = candle.HighPrice;
_acceleration = Math.Min(_acceleration + AccelerationFactor, AccelerationLimit);
}
}
else
{
_sarValue = Math.Max(_sarValue, _previousHigh);
if (candle.HighPrice >= _sarValue)
{
_isBullishTrend = true;
_sarValue = _extremePoint;
_extremePoint = candle.HighPrice;
_acceleration = AccelerationFactor;
}
else if (candle.LowPrice < _extremePoint)
{
_extremePoint = candle.LowPrice;
_acceleration = Math.Min(_acceleration + AccelerationFactor, AccelerationLimit);
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan, Math
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Strategies import Strategy
class parabolic_sar_distance_mean_reversion_strategy(Strategy):
"""
Parabolic SAR distance mean reversion strategy.
Trades large deviations of price from a locally calculated Parabolic SAR level
and exits when the distance returns to its recent average.
"""
def __init__(self):
super(parabolic_sar_distance_mean_reversion_strategy, self).__init__()
self._acceleration_factor = self.Param("AccelerationFactor", 0.02) \
.SetGreaterThanZero() \
.SetDisplay("Acceleration Factor", "Acceleration factor for Parabolic SAR", "Parabolic SAR")
self._acceleration_limit = self.Param("AccelerationLimit", 0.2) \
.SetGreaterThanZero() \
.SetDisplay("Acceleration Limit", "Acceleration limit for Parabolic SAR", "Parabolic SAR")
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Lookback period for distance statistics", "Strategy Parameters")
self._deviation_multiplier = self.Param("DeviationMultiplier", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Deviation Multiplier", "Deviation multiplier for mean reversion detection", "Strategy Parameters")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._cooldown_bars = self.Param("CooldownBars", 1200) \
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle type for strategy", "General")
self._distance_history = None
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._is_initialized = False
self._is_bullish_trend = False
self._sar_value = 0.0
self._extreme_point = 0.0
self._acceleration = 0.0
self._previous_high = 0.0
self._previous_low = 0.0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(parabolic_sar_distance_mean_reversion_strategy, self).OnReseted()
lb = int(self._lookback_period.Value)
self._distance_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
self._is_initialized = False
self._is_bullish_trend = False
self._sar_value = 0.0
self._extreme_point = 0.0
self._acceleration = 0.0
self._previous_high = 0.0
self._previous_low = 0.0
def OnStarted2(self, time):
super(parabolic_sar_distance_mean_reversion_strategy, self).OnStarted2(time)
lb = int(self._lookback_period.Value)
self._distance_history = [0.0] * lb
self._current_index = 0
self._filled_count = 0
self._cooldown = 0
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
self.StartProtection(Unit(), Unit(self._stop_loss_percent.Value, UnitTypes.Percent))
def _initialize_state(self, candle):
close_price = float(candle.ClosePrice)
open_price = float(candle.OpenPrice)
high_price = float(candle.HighPrice)
low_price = float(candle.LowPrice)
self._is_bullish_trend = close_price >= open_price
self._sar_value = low_price if self._is_bullish_trend else high_price
self._extreme_point = high_price if self._is_bullish_trend else low_price
self._acceleration = float(self._acceleration_factor.Value)
self._previous_high = high_price
self._previous_low = low_price
self._is_initialized = True
def _update_sar(self, candle):
high_price = float(candle.HighPrice)
low_price = float(candle.LowPrice)
af = float(self._acceleration_factor.Value)
al = float(self._acceleration_limit.Value)
self._sar_value += self._acceleration * (self._extreme_point - self._sar_value)
if self._is_bullish_trend:
self._sar_value = min(self._sar_value, self._previous_low)
if low_price <= self._sar_value:
self._is_bullish_trend = False
self._sar_value = self._extreme_point
self._extreme_point = low_price
self._acceleration = af
elif high_price > self._extreme_point:
self._extreme_point = high_price
self._acceleration = min(self._acceleration + af, al)
else:
self._sar_value = max(self._sar_value, self._previous_high)
if high_price >= self._sar_value:
self._is_bullish_trend = True
self._sar_value = self._extreme_point
self._extreme_point = high_price
self._acceleration = af
elif low_price < self._extreme_point:
self._extreme_point = low_price
self._acceleration = min(self._acceleration + af, al)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self._is_initialized:
self._initialize_state(candle)
return
self._update_sar(candle)
close_price = float(candle.ClosePrice)
distance = abs(close_price - self._sar_value)
lb = int(self._lookback_period.Value)
self._distance_history[self._current_index] = distance
self._current_index = (self._current_index + 1) % lb
if self._filled_count < lb:
self._filled_count += 1
if self._filled_count < lb:
self._previous_high = float(candle.HighPrice)
self._previous_low = float(candle.LowPrice)
return
avg_distance = 0.0
for i in range(lb):
avg_distance += self._distance_history[i]
avg_distance /= float(lb)
sum_sq = 0.0
for i in range(lb):
diff = self._distance_history[i] - avg_distance
sum_sq += diff * diff
std_distance = math.sqrt(sum_sq / float(lb))
if not self.IsFormedAndOnlineAndAllowTrading():
self._previous_high = float(candle.HighPrice)
self._previous_low = float(candle.LowPrice)
return
if self._cooldown > 0:
self._cooldown -= 1
self._previous_high = float(candle.HighPrice)
self._previous_low = float(candle.LowPrice)
return
dm = float(self._deviation_multiplier.Value)
extended_threshold = avg_distance + std_distance * dm
price_above_sar = close_price > self._sar_value
price_below_sar = close_price < self._sar_value
if self.Position == 0:
if distance > extended_threshold:
if price_above_sar:
self.SellMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif price_below_sar:
self.BuyMarket()
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position > 0 and (distance <= avg_distance or price_above_sar):
self.SellMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
elif self.Position < 0 and (distance <= avg_distance or price_below_sar):
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = int(self._cooldown_bars.Value)
self._previous_high = float(candle.HighPrice)
self._previous_low = float(candle.LowPrice)
def CreateClone(self):
return parabolic_sar_distance_mean_reversion_strategy()