Ver no GitHub

Estratégia ADX Expert

Visão geral

A Estratégia ADX Expert é uma conversão direta do expert advisor original do MetaTrader 4 "ADX Expert" (script MQL 20315). O expert procura cruzamentos entre as linhas do Directional Index positivo e negativo (+DI e -DI) enquanto o Average Directional Index (ADX) permanece abaixo de um limiar especificado, indicando que o mercado está em consolidação. Apenas uma posição pode estar aberta por vez, assim como no expert original.

Lógica de trading

  1. A estratégia se inscreve na série de candles selecionada (candles de 15 minutos por padrão) e calcula o Average Directional Index com o período configurado.
  2. Uma ordem de compra é colocada quando:
    • A linha +DI cruza acima da linha -DI.
    • O valor do ADX permanece abaixo do limiar definido (padrão 20), sinalizando uma tendência fraca.
    • O spread atual está abaixo do filtro MaxSpreadPoints.
    • Nenhuma posição está aberta atualmente.
  3. Uma ordem de venda é colocada quando:
    • A linha +DI cruza abaixo da linha -DI.
    • O valor do ADX ainda é menor que o limiar permitido.
    • O requisito de spread e a condição de posição flat são satisfeitos.
  4. Níveis de stop-loss e take-profit protetores são atribuídos através de StartProtection, espelhando o stop fixo e o alvo da versão MQL. Eles são expressos em pontos de preço (passos de preço) e podem ser desabilitados definindo os valores como zero.

A estratégia depende de um fluxo de trabalho de posição única: novos sinais são ignorados até que a posição atual seja fechada por suas ordens protetoras.

Parâmetros

Parâmetro Descrição Padrão
TradeVolume Tamanho de ordem usado para cada ordem de mercado. 0.1
AdxPeriod Período para o cálculo do ADX. 14
AdxThreshold Valor máximo do ADX que ainda permite uma operação. 20
MaxSpreadPoints Spread máximo permitido em pontos de preço. Definir como 0 para desabilitar o filtro. 20
StopLossPoints Distância de stop-loss em pontos de preço. 200
TakeProfitPoints Distância de take-profit em pontos de preço. 400
CandleType Tipo de candle para cálculos de indicadores (candles de 15 minutos por padrão). Período de 15 minutos

Notas adicionais

  • O filtro de spread requer atualizações do livro de ordens para ler os melhores preços de bid e ask. Certifique-se de que seu provedor de dados fornece essas informações.
  • Todos os comentários e registros são escritos em inglês para maior clareza, cumprindo com as diretrizes do repositório.
  • A estratégia é destinada a fins educacionais. Teste-a completamente em um ambiente simulado antes de implantá-la no trading ao vivo.
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;
	}
}