Стратегия ZScore Reversal
Стратегия ZScore Reversal измеряет, насколько цена отклоняется от скользящей средней в стандартных отклонениях. Полученный Z‑Score выявляет статистически растянутые состояния, которые могут вернуться к среднему.
Тестирование показывает среднегодичную доходность около 91%. Стратегию лучше запускать на фондовом рынке.
Покупка совершается, когда Z‑Score падает ниже отрицательного порога, указывая на перепроданность. Короткая сделка открывается, когда Z‑Score превышает положительный порог. Позиция закрывается, когда Z‑Score снова пересекает ноль, что говорит о нормализации цены.
Эта техника привлекательна для трейдеров, ориентированных на возврат к среднему, которым нужны объективные уровни входа. Процентный стоп‑лосс помогает контролировать неблагоприятные движения, пока ждёшь возврата.
Детали
- Условия входа:
- Лонг: Z‑Score < -Порог
- Шорт: Z‑Score > Порог
- Лонг/Шорт: обе стороны.
- Условия выхода:
- Лонг: Выход при пересечении Z‑Score выше 0
- Шорт: Выход при пересечении Z‑Score ниже 0
- Стопы: да, процентный стоп‑лосс.
- Значения по умолчанию:
LookbackPeriod= 20ZScoreThreshold= 2.0mStopLossPercent= 2mCandleType= TimeSpan.FromMinutes(10)
- Фильтры:
- Категория: Mean Reversion
- Направление: Оба
- Индикаторы: Z‑Score
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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()