ADX Expert 戦略
概要
ADX Expert 戦略は、元の MetaTrader 4 エキスパートアドバイザー「ADX Expert」(MQL スクリプト 20315)の直接変換です。このエキスパートは、Average Directional Index(ADX)が指定された閾値を下回っている間、正の Directional Index(+DI)と負の Directional Index(-DI)のラインのクロスを探し、市場がレンジ相場であることを示します。ソースエキスパートと同様に、一度に開けるポジションは 1 つだけです。
トレードロジック
- 戦略は選択されたローソク足シリーズ(デフォルトは 15 分ローソク足)を購読し、設定された期間で Average Directional Index を計算します。
- 以下の場合に買い注文が出されます:
- +DI ラインが -DI ラインを上回るようにクロスする。
- ADX 値が定義された閾値(デフォルト 20)を下回り、弱いトレンドを示している。
- 現在のスプレッドが
MaxSpreadPointsフィルターを下回っている。 - 現在ポジションがない。
- 以下の場合に売り注文が出されます:
- +DI ラインが -DI ラインを下回るようにクロスする。
- ADX 値がまだ許容閾値より低い。
- スプレッド要件とフラットポジション条件が満たされている。
- 保護的なストップロスとテイクプロフィットレベルは
StartProtectionを通じて割り当てられ、MQL バージョンの固定ストップとターゲットを反映しています。価格ポイント(価格ステップ)で表現され、値をゼロに設定することで無効にできます。
この戦略は単一ポジションのワークフローに基づいています:現在のポジションがその保護注文によって閉じられるまで、新しいシグナルは無視されます。
パラメーター
| パラメーター | 説明 | デフォルト |
|---|---|---|
TradeVolume |
すべての成行注文に使用される注文サイズ。 | 0.1 |
AdxPeriod |
ADX 計算の期間。 | 14 |
AdxThreshold |
まだトレードを許可する最大 ADX 値。 | 20 |
MaxSpreadPoints |
価格ポイントでの最大許容スプレッド。フィルターを無効にするには 0 に設定。 | 20 |
StopLossPoints |
価格ポイントでのストップロス距離。 | 200 |
TakeProfitPoints |
価格ポイントでのテイクプロフィット距離。 | 400 |
CandleType |
指標計算のためのローソク足タイプ(デフォルトは 15 分ローソク足)。 | 15 分タイムフレーム |
追加注意事項
- スプレッドフィルターは、最良の買値と売値を読み取るためにオーダーブックの更新が必要です。データプロバイダーがこの情報を供給していることを確認してください。
- すべてのコメントとログは明確さのために英語で書かれており、リポジトリのガイドラインに準拠しています。
- この戦略は教育目的のためのものです。ライブ取引に展開する前に、シミュレーション環境で徹底的にテストしてください。
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>
/// ADX crossover strategy translated from the original MQL expert.
/// Opens a single position when DI lines cross while ADX remains weak.
/// </summary>
public class AdxExpertStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxThreshold;
private readonly StrategyParam<decimal> _maxSpreadPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private AverageDirectionalIndex _adx = null!;
private decimal _previousPlusDi;
private decimal _previousMinusDi;
private bool _hasPreviousDi;
/// <summary>
/// Trading volume for every market order.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// ADX calculation period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// Maximum ADX level that still allows new trades.
/// </summary>
public decimal AdxThreshold
{
get => _adxThreshold.Value;
set => _adxThreshold.Value = value;
}
/// <summary>
/// Maximum allowed bid-ask spread measured in price points.
/// </summary>
public decimal MaxSpreadPoints
{
get => _maxSpreadPoints.Value;
set => _maxSpreadPoints.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in price points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance expressed in price points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="AdxExpertStrategy"/>.
/// </summary>
public AdxExpertStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade volume", "Order volume used for entries", "Risk management")
.SetOptimize(0.1m, 1m, 0.1m);
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ADX period", "Smoothing length for the ADX indicator", "Indicators")
.SetOptimize(7, 28, 7);
_adxThreshold = Param(nameof(AdxThreshold), 20m)
.SetGreaterThanZero()
.SetDisplay("ADX threshold", "Upper ADX limit that allows trades", "Signals")
.SetOptimize(15m, 35m, 5m);
_maxSpreadPoints = Param(nameof(MaxSpreadPoints), 20m)
.SetNotNegative()
.SetDisplay("Max spread (points)", "Maximum allowed bid-ask spread in points", "Risk management")
.SetOptimize(5m, 40m, 5m);
_stopLossPoints = Param(nameof(StopLossPoints), 200m)
.SetNotNegative()
.SetDisplay("Stop loss (points)", "Protective stop distance in price points", "Risk management")
.SetOptimize(100m, 400m, 50m);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400m)
.SetNotNegative()
.SetDisplay("Take profit (points)", "Target distance in price points", "Risk management")
.SetOptimize(200m, 600m, 100m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle type", "Type of candles used for ADX", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousPlusDi = 0m;
_previousMinusDi = 0m;
_hasPreviousDi = false;
_entryPrice = 0m;
}
private decimal _entryPrice;
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_adx = new AverageDirectionalIndex { Length = AdxPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
IIndicatorValue adxResult;
try
{
adxResult = _adx.Process(candle);
}
catch (IndexOutOfRangeException)
{
return;
}
if (adxResult.IsEmpty || !_adx.IsFormed)
return;
if (adxResult is not AverageDirectionalIndexValue adxData)
return;
var plusDi = adxData.Dx.Plus ?? 0m;
var minusDi = adxData.Dx.Minus ?? 0m;
if (adxData.MovingAverage is not decimal currentAdx)
{
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
_hasPreviousDi = true;
return;
}
if (!_hasPreviousDi)
{
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
_hasPreviousDi = true;
return;
}
// Manage open position SL/TP
if (Position != 0)
{
var step = Security?.PriceStep ?? 1m;
if (Position > 0)
{
if (StopLossPoints > 0m && candle.LowPrice <= _entryPrice - StopLossPoints * step)
{
SellMarket(Position);
goto updateDi;
}
if (TakeProfitPoints > 0m && candle.HighPrice >= _entryPrice + TakeProfitPoints * step)
{
SellMarket(Position);
goto updateDi;
}
}
else
{
var vol = Math.Abs(Position);
if (StopLossPoints > 0m && candle.HighPrice >= _entryPrice + StopLossPoints * step)
{
BuyMarket(vol);
goto updateDi;
}
if (TakeProfitPoints > 0m && candle.LowPrice <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket(vol);
goto updateDi;
}
}
}
var bullishCross = _previousPlusDi <= _previousMinusDi && plusDi > minusDi;
var bearishCross = _previousPlusDi >= _previousMinusDi && plusDi < minusDi;
if (currentAdx < AdxThreshold && Position == 0)
{
if (bullishCross)
{
BuyMarket(TradeVolume);
_entryPrice = candle.ClosePrice;
}
else if (bearishCross)
{
SellMarket(TradeVolume);
_entryPrice = candle.ClosePrice;
}
}
updateDi:
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import AverageDirectionalIndex, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan
class adx_expert_strategy(Strategy):
def __init__(self):
super(adx_expert_strategy, self).__init__()
self._trade_volume = self.Param("TradeVolume", 0.1)
self._adx_period = self.Param("AdxPeriod", 14)
self._adx_threshold = self.Param("AdxThreshold", 20.0)
self._stop_loss_points = self.Param("StopLossPoints", 200.0)
self._take_profit_points = self.Param("TakeProfitPoints", 400.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2)))
self._adx = None
self._prev_plus_di = 0.0
self._prev_minus_di = 0.0
self._has_prev_di = False
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(adx_expert_strategy, self).OnStarted2(time)
self._adx = AverageDirectionalIndex()
self._adx.Length = self._adx_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
try:
civ = CandleIndicatorValue(self._adx, candle)
civ.IsFinal = True
adx_result = self._adx.Process(civ)
except Exception:
return
if adx_result.IsEmpty or not self._adx.IsFormed:
return
plus_di = float(adx_result.Dx.Plus) if adx_result.Dx.Plus is not None else 0.0
minus_di = float(adx_result.Dx.Minus) if adx_result.Dx.Minus is not None else 0.0
current_adx_val = adx_result.MovingAverage
if current_adx_val is None:
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
self._has_prev_di = True
return
current_adx = float(current_adx_val)
if not self._has_prev_di:
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
self._has_prev_di = True
return
# Manage SL/TP
if self.Position != 0:
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self.Position > 0:
if self._stop_loss_points.Value > 0 and float(candle.LowPrice) <= self._entry_price - self._stop_loss_points.Value * step:
self.SellMarket(self.Position)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
if self._take_profit_points.Value > 0 and float(candle.HighPrice) >= self._entry_price + self._take_profit_points.Value * step:
self.SellMarket(self.Position)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
else:
vol = abs(self.Position)
if self._stop_loss_points.Value > 0 and float(candle.HighPrice) >= self._entry_price + self._stop_loss_points.Value * step:
self.BuyMarket(vol)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
if self._take_profit_points.Value > 0 and float(candle.LowPrice) <= self._entry_price - self._take_profit_points.Value * step:
self.BuyMarket(vol)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
return
bullish_cross = self._prev_plus_di <= self._prev_minus_di and plus_di > minus_di
bearish_cross = self._prev_plus_di >= self._prev_minus_di and plus_di < minus_di
if current_adx < self._adx_threshold.Value and self.Position == 0:
if bullish_cross:
self.BuyMarket(self._trade_volume.Value)
self._entry_price = float(candle.ClosePrice)
elif bearish_cross:
self.SellMarket(self._trade_volume.Value)
self._entry_price = float(candle.ClosePrice)
self._prev_plus_di = plus_di
self._prev_minus_di = minus_di
def OnReseted(self):
super(adx_expert_strategy, self).OnReseted()
self._prev_plus_di = 0.0
self._prev_minus_di = 0.0
self._has_prev_di = False
self._entry_price = 0.0
def CreateClone(self):
return adx_expert_strategy()