GitHub で見る

ADX MAクロスオーバー戦略

概要

この戦略は、平滑化移動平均と平均方向性指数(ADX)トレンドフィルターを組み合わせて「ADX & MA」エキスパートアドバイザーを再現します。ロジックは選択した時間軸で最後に完成した2本のローソク足を分析し、移動平均とADXの両方が確認済みの値を生成した後にのみ反応します。ヘッジスタイルのエントリー用に設計されていますが、ネットポジションモデルで実装されており、反対のシグナルが現れるとポジションを自動的に反転させます。

移動平均は各ローソク足の中央価格で計算され、(High + Low) / 2で構築されたSMMAを使用したMetaTraderバージョンに対応しています。ADX閾値は、トレンドの強さが弱い場合の取引を防ぎ、短命なクロスからの偽シグナルを減らします。

エントリー条件

  • 平滑化移動平均とADXの両方が最終値を生成するまで待機します。
  • 前のローソク足(n-1)の終値を、同じローソク足で取得した平滑化MA値と比較します。
  • 以下の場合にロングエントリー:
    • ローソク足n-1の終値がMA値n-1より上にある。
    • ローソク足n-2の終値がそのMA値より下にあった(強気のクロス)、かつ
    • ローソク足n-1のADX値がAdxThreshold以上である。
  • 逆の条件が発生した場合にショートエントリー(ADX確認付きの弱気クロス)。
  • ポジションサイズは戦略のVolumeに反対方向のエクスポージャーの絶対値を加えたもので、反対シグナルでの反転を保証します。

エグジット条件

以下のいずれかの条件が発動したとき、ロング取引はクローズされます:

  • 最新の確認済みクローズ(n-1)が平滑化MAを下回った場合(反対のクロス)。
  • 価格が設定されたロングのテイクプロフィット距離(pips)に達した場合。
  • 価格が設定されたロングのストップロス距離(pips)まで下落した場合。
  • ロング取引のトレーリングストップが、価格がエントリー価格をTrailingStopBuy pips超えた時点で利益を確保します。

ショート取引は、それぞれのパラメーターとトレーリングロジックで同じルールを踏襲します。反対のシグナルが現れるたびに、戦略は現在のポジションをクローズして新しい方向にポジションを開くのに十分な大きさの成行注文を送ります。

リスクと取引管理

  • テイクプロフィット、ストップロス、トレーリングストップの距離はpipsで表されます。戦略はSecurity.PriceStepからpipサイズを導出します。シンボルが3桁または5桁の小数点を使用する場合、pipはPriceStep × 10として定義され、元のMetaTraderの調整に対応します。
  • InitializeLongTargetsInitializeShortTargetsは成行注文の送信直後に絶対価格レベルを計算し、最後に確認されたクローズに基づいたエントリー価格の近似値を保存します。
  • トレーリングストップが有効で価格が設定された距離を超えて有利に動いた場合、未実現利益を保護するためにストップレベルが移動されます。
  • 両方のターゲットセットはポジションがクローズされたときにリセットされるため、古いレベルは再利用されません。

パラメーター

  • MaPeriod – 平滑化移動平均の長さ(デフォルト15)。
  • AdxPeriod – ADX平滑化期間(デフォルト12)。
  • AdxThreshold – トレンドを確認するために必要な最小ADX値(デフォルト16)。
  • TakeProfitBuy / StopLossBuy / TrailingStopBuy – ロング取引のpip距離。
  • TakeProfitSell / StopLossSell / TrailingStopSell – ショート取引のpip距離。
  • CandleType – 入力ローソク足の時間軸、デフォルト1分。

戦略のVolumeを設定して基本注文サイズを制御します。実装では、ショート取引がロングパラメーターを再利用する代わりに独自のリスク設定を受け取る元の動作を維持します。

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;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// ADX filtered smoothed moving average crossover strategy.
/// Opens trades when the previous candle crosses the smoothed MA and ADX confirms the trend.
/// Adds configurable take profit, stop loss and trailing stop distances measured in pips.
/// </summary>
public class AdxMaCrossoverStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _adxThreshold;
	private readonly StrategyParam<decimal> _takeProfitBuy;
	private readonly StrategyParam<decimal> _stopLossBuy;
	private readonly StrategyParam<decimal> _trailingStopBuy;
	private readonly StrategyParam<decimal> _takeProfitSell;
	private readonly StrategyParam<decimal> _stopLossSell;
	private readonly StrategyParam<decimal> _trailingStopSell;
	private readonly StrategyParam<DataType> _candleType;

	private SmoothedMovingAverage _ma = null!;
	private AverageDirectionalIndex _adx = null!;
	private decimal _pipSize;
	private decimal _prevClose;
	private decimal _prevPrevClose;
	private decimal _prevMa;
	private decimal _prevAdx;
	private bool _hasPrev;
	private bool _hasPrevPrev;

	private decimal _longEntryPrice;
	private decimal _longStopPrice;
	private decimal _longTakeProfitPrice;
	private decimal _shortEntryPrice;
	private decimal _shortStopPrice;
	private decimal _shortTakeProfitPrice;

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	public decimal AdxThreshold
	{
		get => _adxThreshold.Value;
		set => _adxThreshold.Value = value;
	}

	public decimal TakeProfitBuy
	{
		get => _takeProfitBuy.Value;
		set => _takeProfitBuy.Value = value;
	}

	public decimal StopLossBuy
	{
		get => _stopLossBuy.Value;
		set => _stopLossBuy.Value = value;
	}

	public decimal TrailingStopBuy
	{
		get => _trailingStopBuy.Value;
		set => _trailingStopBuy.Value = value;
	}

	public decimal TakeProfitSell
	{
		get => _takeProfitSell.Value;
		set => _takeProfitSell.Value = value;
	}

	public decimal StopLossSell
	{
		get => _stopLossSell.Value;
		set => _stopLossSell.Value = value;
	}

	public decimal TrailingStopSell
	{
		get => _trailingStopSell.Value;
		set => _trailingStopSell.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public AdxMaCrossoverStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 15)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Period of the smoothed moving average", "General")
			;
		_adxPeriod = Param(nameof(AdxPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "Smoothing period for Average Directional Index", "Indicators")
			;
		_adxThreshold = Param(nameof(AdxThreshold), 25m)
			.SetDisplay("ADX Threshold", "Minimum ADX value required to trade", "Indicators")
			;
		_takeProfitBuy = Param(nameof(TakeProfitBuy), 83m)
			.SetDisplay("Buy Take Profit (pips)", "Take profit distance for long trades", "Risk Management")
			.SetNotNegative();
		_stopLossBuy = Param(nameof(StopLossBuy), 55m)
			.SetDisplay("Buy Stop Loss (pips)", "Stop loss distance for long trades", "Risk Management")
			.SetNotNegative();
		_trailingStopBuy = Param(nameof(TrailingStopBuy), 27m)
			.SetDisplay("Buy Trailing Stop (pips)", "Trailing stop distance for long trades", "Risk Management")
			.SetNotNegative();
		_takeProfitSell = Param(nameof(TakeProfitSell), 63m)
			.SetDisplay("Sell Take Profit (pips)", "Take profit distance for short trades", "Risk Management")
			.SetNotNegative();
		_stopLossSell = Param(nameof(StopLossSell), 50m)
			.SetDisplay("Sell Stop Loss (pips)", "Stop loss distance for short trades", "Risk Management")
			.SetNotNegative();
		_trailingStopSell = Param(nameof(TrailingStopSell), 27m)
			.SetDisplay("Sell Trailing Stop (pips)", "Trailing stop distance for short trades", "Risk Management")
			.SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_ma?.Reset();
		_adx?.Reset();

		_pipSize = 0m;
		_prevClose = 0m;
		_prevPrevClose = 0m;
		_prevMa = 0m;
		_prevAdx = 0m;
		_hasPrev = false;
		_hasPrevPrev = false;

		ResetLongTargets();
		ResetShortTargets();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_ma = new SmoothedMovingAverage { Length = MaPeriod };
		_adx = new AverageDirectionalIndex { Length = AdxPeriod };
		_pipSize = CalculatePipSize();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_adx, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ma);
			DrawOwnTrades(area);

			var adxArea = CreateChartArea();
			if (adxArea != null)
			{
				DrawIndicator(adxArea, _adx);
			}
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
	{
		// Only react to closed candles to match the MQL implementation.
		if (candle.State != CandleStates.Finished)
			return;

		var median = (candle.HighPrice + candle.LowPrice) / 2m;
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, median, candle.OpenTime) { IsFinal = true });

		if (!maValue.IsFinal || !adxValue.IsFinal)
			return;

		var ma = maValue.GetValue<decimal>();
		var adx = ((AverageDirectionalIndexValue)adxValue).MovingAverage ?? 0m;
		var close = candle.ClosePrice;

		if (_hasPrev && _hasPrevPrev)
		{
			ManageOpenPositions(close);

			var longSignal = _prevClose > _prevMa && _prevPrevClose < _prevMa && _prevAdx >= AdxThreshold;
			var shortSignal = _prevClose < _prevMa && _prevPrevClose > _prevMa && _prevAdx >= AdxThreshold;

			if (longSignal && Position <= 0)
			{
				BuyMarket();
				InitializeLongTargets(_prevClose);
			}
			else if (shortSignal && Position >= 0)
			{
				SellMarket();
				InitializeShortTargets(_prevClose);
			}
		}

		UpdateHistory(close, ma, adx);
	}

	private void ManageOpenPositions(decimal currentClose)
	{
		// Manage long position exits before evaluating new entries.
		if (Position > 0)
		{
			if (_prevClose < _prevMa)
			{
				SellMarket();
				ResetLongTargets();
				return;
			}

			UpdateLongTrailing(currentClose);

			if (_longTakeProfitPrice > 0m && currentClose >= _longTakeProfitPrice)
			{
				SellMarket();
				ResetLongTargets();
				return;
			}

			if (_longStopPrice > 0m && currentClose <= _longStopPrice)
			{
				SellMarket();
				ResetLongTargets();
				return;
			}
		}
		else if (Position < 0)
		{
			if (_prevClose > _prevMa)
			{
				BuyMarket();
				ResetShortTargets();
				return;
			}

			UpdateShortTrailing(currentClose);

			if (_shortTakeProfitPrice > 0m && currentClose <= _shortTakeProfitPrice)
			{
				BuyMarket();
				ResetShortTargets();
				return;
			}

			if (_shortStopPrice > 0m && currentClose >= _shortStopPrice)
			{
				BuyMarket();
				ResetShortTargets();
				return;
			}
		}
		else
		{
			ResetLongTargets();
			ResetShortTargets();
		}
	}

	private void UpdateLongTrailing(decimal currentClose)
	{
		if (TrailingStopBuy <= 0m || _longEntryPrice <= 0m)
			return;

		var trailingDistance = TrailingStopBuy * _pipSize;
		if (trailingDistance <= 0m)
			return;

		var profit = currentClose - _longEntryPrice;
		if (profit <= trailingDistance)
			return;

		var newStop = currentClose - trailingDistance;
		if (newStop > _longStopPrice)
			_longStopPrice = newStop;
	}

	private void UpdateShortTrailing(decimal currentClose)
	{
		if (TrailingStopSell <= 0m || _shortEntryPrice <= 0m)
			return;

		var trailingDistance = TrailingStopSell * _pipSize;
		if (trailingDistance <= 0m)
			return;

		var profit = _shortEntryPrice - currentClose;
		if (profit <= trailingDistance)
			return;

		var newStop = currentClose + trailingDistance;
		if (_shortStopPrice == 0m || newStop < _shortStopPrice)
			_shortStopPrice = newStop;
	}

	private void InitializeLongTargets(decimal entryPrice)
	{
		_longEntryPrice = entryPrice;
		_longStopPrice = StopLossBuy > 0m ? entryPrice - StopLossBuy * _pipSize : 0m;
		_longTakeProfitPrice = TakeProfitBuy > 0m ? entryPrice + TakeProfitBuy * _pipSize : 0m;

		ResetShortTargets();
	}

	private void InitializeShortTargets(decimal entryPrice)
	{
		_shortEntryPrice = entryPrice;
		_shortStopPrice = StopLossSell > 0m ? entryPrice + StopLossSell * _pipSize : 0m;
		_shortTakeProfitPrice = TakeProfitSell > 0m ? entryPrice - TakeProfitSell * _pipSize : 0m;

		ResetLongTargets();
	}

	private void ResetLongTargets()
	{
		_longEntryPrice = 0m;
		_longStopPrice = 0m;
		_longTakeProfitPrice = 0m;
	}

	private void ResetShortTargets()
	{
		_shortEntryPrice = 0m;
		_shortStopPrice = 0m;
		_shortTakeProfitPrice = 0m;
	}

	private void UpdateHistory(decimal close, decimal ma, decimal adx)
	{
		if (_hasPrev)
		{
			_prevPrevClose = _prevClose;
			_hasPrevPrev = true;
		}
		else
		{
			_hasPrevPrev = false;
		}

		_prevClose = close;
		_prevMa = ma;
		_prevAdx = adx;
		_hasPrev = true;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var decimals = GetDecimalPlaces(step);
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var text = value.ToString(CultureInfo.InvariantCulture);
		var separatorIndex = text.IndexOf('.') >= 0 ? text.IndexOf('.') : text.IndexOf(',');
		if (separatorIndex < 0)
			return 0;

		return text.Length - separatorIndex - 1;
	}
}