ADX DI 策略
该策略关注+DI与-DI的交叉以及ADX的强度。+DI上穿-DI且ADX走强时做多,反向交叉则做空;ADX减弱或出现相反交叉时离场。通过要求ADX确认,可避免在每次DI交叉时交易,旨在捕捉可持续的趋势。
测试表明年均收益约为 103%,该策略在股票市场表现最佳。
详情
- 入场条件: 基于 ADX、ATR 的信号
- 多空方向: 双向
- 退出条件: 反向信号或止损
- 止损: 是
- 默认值:
AdxPeriod= 14AdxThreshold= 25mAtrMultiplier= 2mCandleType= TimeSpan.FromMinutes(5)
- 过滤器:
- 类型: 趋势
- 方向: 双向
- 指标: ADX, ATR
- 止损: 是
- 复杂度: 基础
- 时间框架: 日内 (5m)
- 季节性: 无
- 神经网络: 无
- 背离: 无
- 风险等级: 中
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Strategy based on ADX and Directional Movement indicators.
/// Trades strict DI crossings with rising strong ADX, exits on weakening/opposite crosses,
/// and protects actual fills with entry-frozen native ATR distances.
/// </summary>
public class AdxDiStrategy : Strategy
{
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private decimal _prevPlusDi, _prevMinusDi, _prevAdx;
private bool _hasPrevValues;
private Order _pendingOrder, _entryOrder;
private Unit _stopDistance;
private decimal _requestedDistance;
private bool _protectionStarted;
/// <summary>
/// ADX period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// ADX threshold for trend confirmation.
/// </summary>
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
/// <summary>
/// Initializes a new instance of the <see cref="AdxDiStrategy"/>.
/// </summary>
public AdxDiStrategy()
{
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetDisplay("ADX Period", "Period for ADX calculation", "Indicators")
.SetOptimize(10, 20, 2);
_adxThreshold = Param(nameof(AdxThreshold), 25m)
.SetDisplay("ADX Threshold", "ADX level to confirm trend", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_atrPeriod = Param(nameof(AtrPeriod), 14).SetGreaterThanZero()
.SetDisplay("ATR Period", "Wilder ATR lookback for entry-frozen protection.", "Protection");
_atrMultiplier = Param(nameof(AtrMultiplier), 2m).SetNotNegative()
.SetDisplay("ATR Multiplier", "Signal ATR distance; zero disables the stop.", "Protection");
OrderRegistering += order => _pendingOrder = order;
Trades.TradeAdded += ProcessEntryFill;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Security, DataType.Level1)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevPlusDi = _prevMinusDi = _prevAdx = 0m;
_hasPrevValues = default;
_pendingOrder = _entryOrder = null;
_stopDistance = null;
_requestedDistance = 0m;
_protectionStarted = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var adx = new AverageDirectionalIndex { Length = AdxPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quotes = new Subscription(DataType.Level1, Security);
quotes.MarketData.BuildField = field;
SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
}
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(adx, atr, ProcessCandle, false)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, adx);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ObserveProtectionQuote(Level1ChangeMessage quote)
{
// The high-level handler activates native protection before the callback, including between bars.
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var adx = (IAverageDirectionalIndexValue)adxValue;
if (adx.MovingAverage is not decimal adxMain || adx.Dx.Plus is not decimal plusDi || adx.Dx.Minus is not decimal minusDi)
return;
if (!_hasPrevValues)
{
_hasPrevValues = true;
SavePrevious(plusDi, minusDi, adxMain);
return;
}
if (_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
{
SavePrevious(plusDi, minusDi, adxMain);
return;
}
// Equality alone is not a bearish crossing.
var upCross = _prevPlusDi <= _prevMinusDi && plusDi > minusDi;
var downCross = _prevMinusDi <= _prevPlusDi && minusDi > plusDi;
var confirmed = adxMain >= AdxThreshold && adxMain > _prevAdx;
if (confirmed && upCross && Position <= 0m)
Enter(Sides.Buy, atrValue.GetValue<decimal>());
else if (confirmed && downCross && Position >= 0m)
Enter(Sides.Sell, atrValue.GetValue<decimal>());
else if (Position > 0m && (adxMain < _prevAdx || downCross))
SellMarket(Position);
else if (Position < 0m && (adxMain < _prevAdx || upCross))
BuyMarket(Math.Abs(Position));
SavePrevious(plusDi, minusDi, adxMain);
}
private void SavePrevious(decimal plus, decimal minus, decimal adx)
{
_prevPlusDi = plus;
_prevMinusDi = minus;
_prevAdx = adx;
}
private void Enter(Sides side, decimal atr)
{
_requestedDistance = atr * AtrMultiplier;
_stopDistance ??= new Unit(_requestedDistance);
if (Position == 0m) _stopDistance.Value = _requestedDistance;
if (!_protectionStarted && _requestedDistance > 0m)
{
StartProtection(new Unit(), _stopDistance, useMarketOrders: true, isLocalStop: true);
_protectionStarted = true;
}
_entryOrder = new Order
{
Security = Security,
Portfolio = Portfolio,
Type = OrderTypes.Market,
Side = side,
Volume = Volume + Math.Abs(Position),
Comment = "ADX signal",
};
RegisterOrder(_entryOrder);
}
private void ProcessEntryFill(MyTrade trade)
{
// Keep the old native distance until a partial reversal actually changes position direction.
if (trade.Order == _entryOrder && Position != 0m && (Position > 0m) == (trade.Order.Side == Sides.Buy))
_stopDistance.Value = _requestedDistance;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, OrderStates, OrderTypes, Sides, Level1Fields
from StockSharp.BusinessEntities import Subscription, Order
from StockSharp.Algo.Indicators import AverageDirectionalIndex, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class adx_di_strategy(Strategy):
"""Strict DI crossings with rising ADX, weakening/opposite exits and actual-fill ATR protection."""
def __init__(self):
super(adx_di_strategy, self).__init__()
self._adx_period = self.Param("AdxPeriod", 14) \
.SetDisplay("ADX Period", "Period for ADX calculation", "Indicators")
self._adx_threshold = self.Param("AdxThreshold", 25.0) \
.SetDisplay("ADX Threshold", "ADX level to confirm trend", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._atr_period = self.Param("AtrPeriod", 14).SetGreaterThanZero() \
.SetDisplay("ATR Period", "Wilder ATR lookback for entry-frozen protection.", "Protection")
self._atr_multiplier = self.Param("AtrMultiplier", 2.0).SetNotNegative() \
.SetDisplay("ATR Multiplier", "Signal ATR distance; zero disables the stop.", "Protection")
self._reset_state()
self.OrderRegistering += self._track_pending
self.Trades.TradeAdded += self._process_entry_fill
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, DataType.Level1)]
def _track_pending(self, order):
self._pending_order = order
def _reset_state(self):
self._prev_plus_di = self._prev_minus_di = self._prev_adx = Decimal.Zero
self._has_prev_values = False
self._pending_order = self._entry_order = None
self._stop_distance = None
self._requested_distance = Decimal.Zero
self._protection_started = False
@property
def AdxPeriod(self):
return self._adx_period.Value
@AdxPeriod.setter
def AdxPeriod(self, value):
self._adx_period.Value = value
@property
def AdxThreshold(self):
return self._adx_threshold.Value
@AdxThreshold.setter
def AdxThreshold(self, value):
self._adx_threshold.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(adx_di_strategy, self).OnStarted2(time)
adx = AverageDirectionalIndex()
adx.Length = self.AdxPeriod
atr = AverageTrueRange()
atr.Length = self._atr_period.Value
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_protection_quote).Start()
self.SubscribeCandles(self.CandleType) \
.BindEx(adx, atr, self.ProcessCandle, False) \
.Start()
def _observe_protection_quote(self, quote):
# Native protection runs before this callback, including between signal bars.
pass
def ProcessCandle(self, candle, adx_value, atr_value):
if candle.State != CandleStates.Finished or not self.IsFormedAndOnlineAndAllowTrading():
return
if adx_value.IsEmpty:
return
adx_main = adx_value.MovingAverage
plus_di = adx_value.Dx.Plus
minus_di = adx_value.Dx.Minus
if adx_main is None or plus_di is None or minus_di is None:
return
if not self._has_prev_values:
self._has_prev_values = True
self._save_previous(plus_di, minus_di, adx_main)
return
if self._pending_order is not None and self._pending_order.State not in (OrderStates.Done, OrderStates.Failed):
self._save_previous(plus_di, minus_di, adx_main)
return
up_cross = self._prev_plus_di <= self._prev_minus_di and plus_di > minus_di
down_cross = self._prev_minus_di <= self._prev_plus_di and minus_di > plus_di
confirmed = adx_main >= Decimal(self.AdxThreshold) and adx_main > self._prev_adx
if confirmed and up_cross and self.Position <= 0:
self._enter(Sides.Buy, atr_value.GetValue[Decimal](None))
elif confirmed and down_cross and self.Position >= 0:
self._enter(Sides.Sell, atr_value.GetValue[Decimal](None))
elif self.Position > 0 and (adx_main < self._prev_adx or down_cross):
self.SellMarket(self.Position)
elif self.Position < 0 and (adx_main < self._prev_adx or up_cross):
self.BuyMarket(Math.Abs(self.Position))
self._save_previous(plus_di, minus_di, adx_main)
def _save_previous(self, plus, minus, adx):
self._prev_plus_di = plus
self._prev_minus_di = minus
self._prev_adx = adx
def _enter(self, side, atr):
self._requested_distance = atr * Decimal(self._atr_multiplier.Value)
if self._stop_distance is None:
self._stop_distance = Unit(self._requested_distance)
if self.Position == 0:
self._stop_distance.Value = self._requested_distance
if not self._protection_started and self._requested_distance > 0:
self.StartProtection(Unit(), self._stop_distance, useMarketOrders=True, isLocalStop=True)
self._protection_started = True
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = self.Volume + Math.Abs(self.Position)
order.Comment = "ADX signal"
self._entry_order = order
self.RegisterOrder(order)
def _process_entry_fill(self, trade):
# Preserve the old distance until an actual reversal fill changes direction.
if (trade.Order == self._entry_order and self.Position != 0
and (self.Position > 0) == (trade.Order.Side == Sides.Buy)):
self._stop_distance.Value = self._requested_distance
def OnReseted(self):
super(adx_di_strategy, self).OnReseted()
self._reset_state()
def CreateClone(self):
return adx_di_strategy()