ADX 均值回归策略
平均趋向指数(ADX)用于衡量市场趋势力度。当 ADX 值处于低位时,市场倾向于围绕均值震荡。本策略交易 ADX 相对其均值的偏离。
测试表明年均收益约为 70%,该策略在股票市场表现最佳。
当 ADX 低于平均值减 DeviationMultiplier 倍标准差且价格位于均线下方时做多;当 ADX 高于上轨并且价格在均线上方时做空。ADX 回归均值附近后平仓。
此系统适合在弱趋势环境中寻找机会的交易者,若出现新的趋势则由止损限制亏损。
详细信息
- 入场条件:
- 做多: ADX < Avg - DeviationMultiplier * StdDev && Close < MA
- 做空: ADX > Avg + DeviationMultiplier * StdDev && Close > MA
- 多空方向: 双向
- 退出条件:
- 做多: Exit when ADX > Avg
- 做空: Exit when ADX < Avg
- 止损: 是
- 默认值:
AdxPeriod= 14AveragePeriod= 20DeviationMultiplier= 2mCandleType= TimeSpan.FromMinutes(5)
- 筛选条件:
- 类别: 均值回归
- 方向: 双向
- 指标: 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()