Стратегия средневозвратная на основе ADX
Здесь индекс среднего направленного движения (ADX) измеряет общую силу тренда. Когда ADX низок, рынок не имеет выраженного направления, и цены колеблются вокруг среднего значения. Стратегия использует это поведение, торгуя отклонения ADX от его скользящей средней.
Тестирование показывает среднегодичную доходность около 70%. Стратегию лучше запускать на фондовом рынке.
Длинная позиция открывается, когда ADX падает ниже среднего минус DeviationMultiplier, умноженный на стандартное отклонение, и цена ниже средней. Короткая позиция открывается при всплеске ADX выше верхней границы и цене выше средней. Позиции закрываются, когда ADX возвращается к своему среднему значению.
Система подходит трейдерам, ищущим возможности в условиях слабого тренда. Стоп‑лосс не дает небольшим сделкам на возврат к среднему перерасти в крупные убытки при возникновении нового тренда.
Подробности
- Условия входа:
- Лонг: ADX < Avg - DeviationMultiplier * StdDev && Close < MA
- Шорт: ADX > Avg + DeviationMultiplier * StdDev && Close > MA
- Длинные/короткие: обе стороны.
- Условия выхода:
- Лонг: выход при ADX > Avg
- Шорт: выход при ADX < Avg
- Стопы: да, процентный стоп‑лосс.
- Значения по умолчанию:
AdxPeriod= 14AveragePeriod= 20DeviationMultiplier= 2mCandleType= TimeSpan.FromMinutes(5)
- Фильтры:
- Категория: Mean Reversion
- Направление: оба
- Индикаторы: ADX
- Стопы: да
- Сложность: средняя
- Таймфрейм: внутридневной
- Сезонность: нет
- Нейросети: нет
- Дивергенция: нет
- Уровень риска: средний
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Candles;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// ADX Mean Reversion strategy.
/// This strategy enters positions when ADX is significantly below or above its average value.
/// </summary>
public class AdxMeanReversionStrategy : Strategy
{
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<int> _averagePeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _stopLossPercent;
private decimal _prevAdx;
private decimal _avgAdx;
private decimal _stdDevAdx;
private decimal _sumAdx;
private decimal _sumSquaresAdx;
private int _count;
private readonly Queue<decimal> _adxValues = [];
/// <summary>
/// ADX Period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// Period for calculating mean and standard deviation of ADX.
/// </summary>
public int AveragePeriod
{
get => _averagePeriod.Value;
set => _averagePeriod.Value = value;
}
/// <summary>
/// Deviation multiplier for entry signals.
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public AdxMeanReversionStrategy()
{
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetGreaterThanZero()
.SetOptimize(10, 20, 5)
.SetDisplay("ADX Period", "Period for ADX indicator", "Indicators");
_averagePeriod = Param(nameof(AveragePeriod), 20)
.SetGreaterThanZero()
.SetOptimize(10, 50, 10)
.SetDisplay("Average Period", "Period for calculating ADX average and standard deviation", "Settings");
_deviationMultiplier = Param(nameof(DeviationMultiplier), 2m)
.SetGreaterThanZero()
.SetOptimize(1.5m, 3m, 0.5m)
.SetDisplay("Deviation Multiplier", "Multiplier for standard deviation", "Settings");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetOptimize(1m, 3m, 0.5m)
.SetDisplay("Stop Loss %", "Stop loss as percentage of entry price", "Risk Management");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevAdx = 0;
_avgAdx = 0;
_stdDevAdx = 0;
_sumAdx = 0;
_sumSquaresAdx = 0;
_count = 0;
_adxValues.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
// Create ADX indicator
var adx = new AverageDirectionalIndex { Length = AdxPeriod };
// Create subscription and bind indicator
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(adx, ProcessCandle)
.Start();
// Setup chart visualization
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, adx);
DrawOwnTrades(area);
}
// Enable position protection
StartProtection(
takeProfit: new Unit(0m), // We'll manage exits ourselves based on ADX
stopLoss: new Unit(StopLossPercent, UnitTypes.Percent)
);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Check if strategy is ready to trade
if (!IsFormedAndOnlineAndAllowTrading())
return;
var adxTyped = (AverageDirectionalIndexValue)adxValue;
if (adxTyped.MovingAverage is not decimal currentAdx)
return;
var dx = adxTyped.Dx;
if (dx.Plus is not decimal plusDi || dx.Minus is not decimal minusDi)
return;
// Update ADX statistics
UpdateAdxStatistics(currentAdx);
// Save current ADX for next iteration
_prevAdx = currentAdx;
// If we don't have enough data yet for statistics
if (_count < AveragePeriod)
return;
// Check for entry conditions
if (Position == 0)
{
// Positive trend strength should correspond to price direction for entry
var direction = plusDi > minusDi ? Sides.Buy : Sides.Sell;
// ADX is significantly below its average - mean reversion expects it to rise
// This could indicate a period of low trend strength that might change
if (currentAdx < _avgAdx - DeviationMultiplier * _stdDevAdx)
{
if (direction == Sides.Buy)
{
BuyMarket(Volume);
LogInfo($"Long entry: ADX = {currentAdx}, Avg = {_avgAdx}, StdDev = {_stdDevAdx}, +DI > -DI");
}
else
{
SellMarket(Volume);
LogInfo($"Short entry: ADX = {currentAdx}, Avg = {_avgAdx}, StdDev = {_stdDevAdx}, +DI < -DI");
}
}
// ADX is significantly above its average - mean reversion expects it to fall
// This could indicate a period of high trend strength that might weaken
else if (currentAdx > _avgAdx + DeviationMultiplier * _stdDevAdx)
{
// For high ADX values, we're more cautious and might want to go against the direction
// as extremely high ADX may indicate trend exhaustion
if (direction == Sides.Sell)
{
BuyMarket(Volume);
LogInfo($"Long entry (trend strength exhaustion): ADX = {currentAdx}, Avg = {_avgAdx}, StdDev = {_stdDevAdx}");
}
else
{
SellMarket(Volume);
LogInfo($"Short entry (trend strength exhaustion): ADX = {currentAdx}, Avg = {_avgAdx}, StdDev = {_stdDevAdx}");
}
}
}
// Check for exit conditions
else if (Position > 0) // Long position
{
if (currentAdx > _avgAdx)
{
ClosePosition();
LogInfo($"Long exit: ADX = {currentAdx}, Avg = {_avgAdx}");
}
}
else if (Position < 0) // Short position
{
if (currentAdx < _avgAdx)
{
ClosePosition();
LogInfo($"Short exit: ADX = {currentAdx}, Avg = {_avgAdx}");
}
}
}
private void UpdateAdxStatistics(decimal currentAdx)
{
// Add current value to the queue
_adxValues.Enqueue(currentAdx);
_sumAdx += currentAdx;
_sumSquaresAdx += currentAdx * currentAdx;
_count++;
// If queue is larger than period, remove oldest value
if (_adxValues.Count > AveragePeriod)
{
var oldestAdx = _adxValues.Dequeue();
_sumAdx -= oldestAdx;
_sumSquaresAdx -= oldestAdx * oldestAdx;
_count--;
}
// Calculate average and standard deviation
if (_count > 0)
{
_avgAdx = _sumAdx / _count;
if (_count > 1)
{
var variance = (_sumSquaresAdx - (_sumAdx * _sumAdx) / _count) / (_count - 1);
_stdDevAdx = variance <= 0 ? 0 : (decimal)Math.Sqrt((double)variance);
}
else
{
_stdDevAdx = 0;
}
}
}
}
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, CandleStates, Unit, UnitTypes, Sides
from StockSharp.Algo.Indicators import AverageDirectionalIndex
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
class adx_mean_reversion_strategy(Strategy):
"""
ADX Mean Reversion strategy. This strategy enters positions when ADX is
significantly below or above its average value.
"""
def __init__(self):
super(adx_mean_reversion_strategy, self).__init__()
# Initialize strategy parameters
self._adx_period = self.Param("AdxPeriod", 14) \
.SetGreaterThanZero() \
.SetCanOptimize(True) \
.SetOptimize(10, 20, 5) \
.SetDisplay("ADX Period", "Period for ADX indicator", "Indicators")
self._average_period = self.Param("AveragePeriod", 20) \
.SetGreaterThanZero() \
.SetCanOptimize(True) \
.SetOptimize(10, 50, 10) \
.SetDisplay("Average Period", "Period for calculating ADX average and standard deviation", "Settings")
self._deviation_multiplier = self.Param("DeviationMultiplier", 2.0) \
.SetGreaterThanZero() \
.SetCanOptimize(True) \
.SetOptimize(1.5, 3.0, 0.5) \
.SetDisplay("Deviation Multiplier", "Multiplier for standard deviation", "Settings")
self._candle_type = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5) \
.SetDisplay("Stop Loss %", "Stop loss as percentage of entry price", "Risk Management")
# Internal state variables
self._prev_adx = 0.0
self._avg_adx = 0.0
self._std_dev_adx = 0.0
self._sum_adx = 0.0
self._sum_squares_adx = 0.0
self._count = 0
self._adx_values = []
@property
def AdxPeriod(self):
return self._adx_period.Value
@AdxPeriod.setter
def AdxPeriod(self, value):
self._adx_period.Value = value
@property
def AveragePeriod(self):
return self._average_period.Value
@AveragePeriod.setter
def AveragePeriod(self, value):
self._average_period.Value = value
@property
def DeviationMultiplier(self):
return self._deviation_multiplier.Value
@DeviationMultiplier.setter
def DeviationMultiplier(self, value):
self._deviation_multiplier.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StopLossPercent(self):
return self._stop_loss_percent.Value
@StopLossPercent.setter
def StopLossPercent(self, value):
self._stop_loss_percent.Value = value
def OnReseted(self):
super(adx_mean_reversion_strategy, self).OnReseted()
self._prev_adx = 0.0
self._avg_adx = 0.0
self._std_dev_adx = 0.0
self._sum_adx = 0.0
self._sum_squares_adx = 0.0
self._count = 0
self._adx_values.clear()
def OnStarted2(self, time):
"""
Called when the strategy starts. Resets statistics, creates indicators,
and sets up charting.
"""
super(adx_mean_reversion_strategy, self).OnStarted2(time)
# Create ADX indicator
adx = AverageDirectionalIndex()
adx.Length = self.AdxPeriod
# Create subscription and bind indicator
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(adx, self.ProcessCandle).Start()
# Setup chart visualization
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, adx)
self.DrawOwnTrades(area)
# Enable position protection
self.StartProtection(
takeProfit=None,
stopLoss=Unit(self.StopLossPercent, UnitTypes.Percent)
)
def ProcessCandle(self, candle, adx_value):
"""
Process candle with ADX indicator value.
"""
# Skip unfinished candles
if candle.State != CandleStates.Finished:
return
# Check if strategy is ready to trade
if adx_value.MovingAverage is None:
return
current_adx = float(adx_value.MovingAverage)
if adx_value.Dx is None or adx_value.Dx.Plus is None or adx_value.Dx.Minus is None:
return
dx = adx_value.Dx
plus_di = float(dx.Plus)
minus_di = float(dx.Minus)
# Update ADX statistics
self.UpdateAdxStatistics(current_adx)
# Save current ADX for next iteration
self._prev_adx = current_adx
# If we don't have enough data yet for statistics
if self._count < self.AveragePeriod:
return
if self.Position == 0:
# Positive trend strength should correspond to price direction for entry
direction = Sides.Buy if plus_di > minus_di else Sides.Sell
# ADX significantly below average - expect rise
if current_adx < self._avg_adx - self.DeviationMultiplier * self._std_dev_adx:
if direction == Sides.Buy:
self.BuyMarket(self.Volume)
self.LogInfo(
f"Long entry: ADX = {current_adx}, Avg = {self._avg_adx}, StdDev = {self._std_dev_adx}, +DI > -DI")
else:
self.SellMarket(self.Volume)
self.LogInfo(
f"Short entry: ADX = {current_adx}, Avg = {self._avg_adx}, StdDev = {self._std_dev_adx}, +DI < -DI")
# ADX significantly above average - expect fall (trend exhaustion)
elif current_adx > self._avg_adx + self.DeviationMultiplier * self._std_dev_adx:
if direction == Sides.Sell:
self.BuyMarket(self.Volume)
self.LogInfo(
f"Long entry (trend strength exhaustion): ADX = {current_adx}, Avg = {self._avg_adx}, StdDev = {self._std_dev_adx}")
else:
self.SellMarket(self.Volume)
self.LogInfo(
f"Short entry (trend strength exhaustion): ADX = {current_adx}, Avg = {self._avg_adx}, StdDev = {self._std_dev_adx}")
elif self.Position > 0:
# Long position exit condition
if current_adx > self._avg_adx:
self.ClosePosition()
self.LogInfo(f"Long exit: ADX = {current_adx}, Avg = {self._avg_adx}")
elif self.Position < 0:
# Short position exit condition
if current_adx < self._avg_adx:
self.ClosePosition()
self.LogInfo(f"Short exit: ADX = {current_adx}, Avg = {self._avg_adx}")
def UpdateAdxStatistics(self, current_adx):
"""Update running mean and standard deviation of ADX."""
self._adx_values.append(current_adx)
self._sum_adx += current_adx
self._sum_squares_adx += current_adx * current_adx
self._count += 1
if len(self._adx_values) > self.AveragePeriod:
oldest_adx = self._adx_values.pop(0)
self._sum_adx -= oldest_adx
self._sum_squares_adx -= oldest_adx * oldest_adx
self._count -= 1
if self._count > 0:
self._avg_adx = self._sum_adx / self._count
if self._count > 1:
variance = (self._sum_squares_adx - (self._sum_adx * self._sum_adx) / self._count) / (self._count - 1)
self._std_dev_adx = 0 if variance <= 0 else Math.Sqrt(float(variance))
else:
self._std_dev_adx = 0
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return adx_mean_reversion_strategy()