ZScore Reversal Strategy
The ZScore Reversal strategy measures how far price deviates from a moving average in terms of standard deviations. The resulting Z-Score highlights statistically stretched conditions that may snap back toward the mean.
Testing indicates an average annual return of about 91%. It performs best in the stocks market.
A trade is opened long when the Z-Score falls below a negative threshold, signalling an oversold market. A short trade is taken when the Z-Score rises above the positive threshold. The position is closed once the Z-Score crosses back through zero, indicating price has normalized.
This technique is attractive for mean reversion traders who prefer objective entry levels. The stop-loss percentage keeps adverse moves manageable while waiting for the reversion.
Details
- Entry Criteria:
- Long: Z-Score < -Threshold
- Short: Z-Score > Threshold
- Long/Short: Both sides.
- Exit Criteria:
- Long: Exit when Z-Score crosses above 0
- Short: Exit when Z-Score crosses below 0
- Stops: Yes, percent stop-loss.
- Default Values:
LookbackPeriod= 20ZScoreThreshold= 2.0mStopLossPercent= 2mCandleType= TimeSpan.FromMinutes(10)
- Filters:
- Category: Mean Reversion
- Direction: Both
- Indicators: Z-Score
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Intraday
- 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>
/// Strategy that trades based on Z-Score (normalized price deviation from the mean).
/// Enters long when Z-Score is below a negative threshold (price significantly below mean).
/// Enters short when Z-Score is above a positive threshold (price significantly above mean).
/// Exits when Z-Score returns to zero (price returns to mean).
/// </summary>
public class ZScoreReversalStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _zScoreThreshold;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _ma;
private StandardDeviation _stdDev;
private decimal _lastZScore;
/// <summary>
/// Period for calculating mean and standard deviation.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Z-Score threshold for entry signals.
/// </summary>
public decimal ZScoreThreshold
{
get => _zScoreThreshold.Value;
set => _zScoreThreshold.Value = value;
}
/// <summary>
/// Stop-loss percentage parameter.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type parameter.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public ZScoreReversalStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Period for calculating mean and standard deviation", "Parameters")
.SetOptimize(10, 40, 5);
_zScoreThreshold = Param(nameof(ZScoreThreshold), 2.0m)
.SetGreaterThanZero()
.SetDisplay("Z-Score Threshold", "Z-Score threshold for entry signals", "Parameters")
.SetOptimize(1.5m, 3.0m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop-loss %", "Stop-loss as percentage of entry price", "Risk Management")
.SetOptimize(1m, 3m, 0.5m);
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(10).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();
_ma = null;
_stdDev = null;
_lastZScore = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Initialize indicators
_ma = new() { Length = LookbackPeriod };
_stdDev = new() { Length = LookbackPeriod };
// Create candles subscription
var subscription = SubscribeCandles(CandleType);
// Bind indicators to subscription
subscription
.Bind(_ma, _stdDev, ProcessCandle)
.Start();
// Enable position protection with stop-loss
StartProtection(
takeProfit: new Unit(0, UnitTypes.Absolute), // No take-profit
stopLoss: new Unit(StopLossPercent, UnitTypes.Percent) // Stop-loss as percentage
);
// Setup chart if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal stdDevValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Skip if strategy is not ready to trade
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Skip if standard deviation is zero (avoid division by zero)
if (stdDevValue == 0)
return;
// Calculate Z-Score: (Price - Mean) / StdDev
decimal zScore = (candle.ClosePrice - maValue) / stdDevValue;
LogInfo($"Current Z-Score: {zScore:F4}, Mean: {maValue:F4}, StdDev: {stdDevValue:F4}");
// Trading logic
if (zScore < -ZScoreThreshold)
{
// Long signal: Z-Score is below negative threshold
if (Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
LogInfo($"Long Entry: Z-Score({zScore:F4}) < -{ZScoreThreshold:F4}");
}
}
else if (zScore > ZScoreThreshold)
{
// Short signal: Z-Score is above positive threshold
if (Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
LogInfo($"Short Entry: Z-Score({zScore:F4}) > {ZScoreThreshold:F4}");
}
}
else if ((zScore > 0 && Position > 0) || (zScore < 0 && Position < 0))
{
// Exit signals: Z-Score crossed zero line
if (Position > 0 && _lastZScore < 0 && zScore > 0)
{
SellMarket(Math.Abs(Position));
LogInfo($"Exit Long: Z-Score crossed zero from negative to positive");
}
else if (Position < 0 && _lastZScore > 0 && zScore < 0)
{
BuyMarket(Math.Abs(Position));
LogInfo($"Exit Short: Z-Score crossed zero from positive to negative");
}
}
// Store current Z-Score for next calculation
_lastZScore = zScore;
}
}
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, UnitTypes, Unit, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
class z_score_reversal_strategy(Strategy):
"""
Strategy that trades based on Z-Score (normalized price deviation from the mean).
Enters long when Z-Score is below a negative threshold (price significantly below mean).
Enters short when Z-Score is above a positive threshold (price significantly above mean).
Exits when Z-Score returns to zero (price returns to mean).
"""
def __init__(self):
super(z_score_reversal_strategy, self).__init__()
# Constructor.
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Period for calculating mean and standard deviation", "Parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 40, 5)
self._z_score_threshold = self.Param("ZScoreThreshold", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Z-Score Threshold", "Z-Score threshold for entry signals", "Parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.5, 3.0, 0.5)
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop-loss %", "Stop-loss as percentage of entry price", "Risk Management") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
self._candle_type = self.Param("CandleType", tf(10)) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._ma = None
self._std_dev = None
self._last_z_score = 0.0
@property
def lookback_period(self):
"""Period for calculating mean and standard deviation."""
return self._lookback_period.Value
@lookback_period.setter
def lookback_period(self, value):
self._lookback_period.Value = value
@property
def z_score_threshold(self):
"""Z-Score threshold for entry signals."""
return self._z_score_threshold.Value
@z_score_threshold.setter
def z_score_threshold(self, value):
self._z_score_threshold.Value = value
@property
def stop_loss_percent(self):
"""Stop-loss percentage parameter."""
return self._stop_loss_percent.Value
@stop_loss_percent.setter
def stop_loss_percent(self, value):
self._stop_loss_percent.Value = value
@property
def candle_type(self):
"""Candle type parameter."""
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type)]
def OnReseted(self):
super(z_score_reversal_strategy, self).OnReseted()
self._ma = None
self._std_dev = None
self._last_z_score = 0.0
def OnStarted2(self, time):
super(z_score_reversal_strategy, self).OnStarted2(time)
# Initialize indicators
self._ma = SimpleMovingAverage()
self._ma.Length = self.lookback_period
self._std_dev = StandardDeviation()
self._std_dev.Length = self.lookback_period
# Create candles subscription
subscription = self.SubscribeCandles(self.candle_type)
# Bind indicators to subscription
subscription.Bind(self._ma, self._std_dev, self.ProcessCandle).Start()
# Enable position protection with stop-loss
self.StartProtection(
takeProfit=Unit(0, UnitTypes.Absolute),
stopLoss=Unit(self.stop_loss_percent, UnitTypes.Percent)
)
# Setup chart if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ma)
self.DrawOwnTrades(area)
def ProcessCandle(self, candle, ma_value, std_dev_value):
# Skip unfinished candles
if candle.State != CandleStates.Finished:
return
# Skip if strategy is not ready to trade
# Skip if standard deviation is zero (avoid division by zero)
if std_dev_value == 0:
return
# Calculate Z-Score: (Price - Mean) / StdDev
z_score = float((candle.ClosePrice - ma_value) / std_dev_value)
self.LogInfo(
"Current Z-Score: {0:.4f}, Mean: {1:.4f}, StdDev: {2:.4f}".format(
z_score, ma_value, std_dev_value))
# Trading logic
if z_score < -self.z_score_threshold:
# Long signal: Z-Score is below negative threshold
if self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
self.LogInfo(
"Long Entry: Z-Score({0:.4f}) < -{1:.4f}".format(
z_score, self.z_score_threshold))
elif z_score > self.z_score_threshold:
# Short signal: Z-Score is above positive threshold
if self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self.LogInfo(
"Short Entry: Z-Score({0:.4f}) > {1:.4f}".format(
z_score, self.z_score_threshold))
elif (z_score > 0 and self.Position > 0) or (z_score < 0 and self.Position < 0):
# Exit signals: Z-Score crossed zero line
if self.Position > 0 and self._last_z_score < 0 and z_score > 0:
self.SellMarket(Math.Abs(self.Position))
self.LogInfo("Exit Long: Z-Score crossed zero from negative to positive")
elif self.Position < 0 and self._last_z_score > 0 and z_score < 0:
self.BuyMarket(Math.Abs(self.Position))
self.LogInfo("Exit Short: Z-Score crossed zero from positive to negative")
# Store current Z-Score for next calculation
self._last_z_score = z_score
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return z_score_reversal_strategy()