Ver no GitHub

ErroEstratégia EA

Visão geral

A Estratégia ErrorEA é uma porta StockSharp do consultor MetaTrader errorEA.mq4. O especialista original comparou os componentes +DI e -DI do Índice Direcional Médio e continuou acumulando ordens de mercado na direção da tendência detectada enquanto aplicava um stop-loss de segurança muito grande e um take-profit de scalping rígido. Esta versão C# recria a mesma ideia com o API de alto nível do StockSharp, adiciona controles de parâmetros claros e documenta explicitamente o modelo de risco.

Lógica de negociação

  1. Assine o período configurado (CandleType) e alimente um indicador AverageDirectionalIndex com as velas recebidas.
  2. Espere até que a vela esteja totalmente fechada e o ADX produza um valor final para aquela barra.
  3. Compare as linhas +DI e -DI:
    • se +DI > -DI, a estratégia trata o mercado como altista;
    • se -DI > +DI, o mercado é considerado baixista;
    • valores iguais não geram novos sinais.
  4. Em um sinal de alta:
    • nivelar uma posição líquida curta existente (StockSharp usa contas de compensação, então hedges opostos são fechados);
    • se o número de negociações de longo escalonamento ainda estiver abaixo de MaxTrades, envie mais uma ordem de compra de mercado com o volume retornado pelo bloco de controle de risco.
  5. Em um sinal de baixa:
    • fechar uma posição longa existente;
    • se o número de tranches curtas for inferior a MaxTrades, envie uma ordem de venda a mercado com a mesma lógica de dimensionamento de posição.
  6. As ordens de proteção são gerenciadas por StartProtection:
    • StopLossPoints é convertido em etapas de preço e funciona como um amplo stop fixo, assim como a entrada StopLoss em MetaTrader;
    • se EnableTakeProfit for verdadeiro, TakeProfitPoints replica o pequeno alvo de escalpelamento que o EA aplicou por meio de OrderModify.
  7. Os contadores de posição (_longTrades/_shortTrades) são redefinidos sempre que a posição líquida retorna a zero ou muda para o lado oposto, garantindo que o limite de redução seja aplicado em interrupções e reversões.

Gestão e dimensionamento de riscos

  • BaseVolume espelha a entrada MiniLots de MetaTrader. Ele atua como o tamanho do lote inicial para cada negociação.
  • Quando EnableRiskControl é verdadeiro, a estratégia reproduz a fórmula PowerRisk original: volume = BaseVolume * max(1, PortfolioValue / RiskDivider). O divisor padrão (10000) corresponde à implementação de MQL.
  • Depois que a fórmula é aplicada, o resultado é limitado por MinVolume, MaxVolume, os limites de troca (Security.MinVolume, Security.MaxVolume) e a etapa de volume (Security.VolumeStep). Isso evita que o EA solicite um tamanho que o local rejeitaria.
  • O tamanho calculado é usado para cada nova ordem de redução, enquanto a direção correspondente permanece dentro do limite MaxTrades.

Parâmetros

Nome Tipo Padrão MetaTrader contraparte Descrição
AdxPeriod int 14 iADX(..., 14, ...) Período de suavização do Índice Direcional Médio.
CandleType DataType Período de 15 minutos prazo do gráfico Série de velas usada para todos os cálculos.
MaxTrades int 9 MaxTrades Número máximo de pedidos de redução por direção.
EnableRiskControl bool true RiskControl Permite o cálculo dinâmico do lote com base no valor do portfólio.
BaseVolume decimal 0.15 MiniLots Tamanho base do lote antes de aplicar o multiplicador de risco.
RiskDivider decimal 10000 implícito (divisor em PowerRisk) Divisor aplicado ao valor da carteira quando o controle de risco está ativo.
MaxVolume decimal 3 MaxLot Limite para o volume calculado automaticamente (antes do arredondamento cambial).
MinVolume decimal 0.01 MarketInfo(..., MODE_MINLOT) Volume mínimo permitido no pedido final.
StopLossPoints int 1000 StopLoss Distância de stop-loss em etapas de preço. Defina como 0 para desativar a parada.
EnableTakeProfit bool true ScalpeControl Permite o lucro de scalping apertado.
TakeProfitPoints int 10 ScalpeProfit Distância de lucro em etapas de preço.

Diferenças do consultor especialista original

  • A versão MetaTrader continha um bug que substituiu o valor +DI pelo valor -DI. A porta StockSharp compara os componentes corretos, refletindo o comportamento pretendido da estratégia.
  • MetaTrader permite cobertura. StockSharp opera em um ambiente de compensação, então a porta fecha a exposição oposta antes de adicionar novas negociações na direção do sinal.
  • A detecção de derrapagem (GetSlippage) e a saída de comentários foram removidas porque StockSharp lida com a derrapagem de pedidos internamente e as strings de risco eram puramente cosméticas.
  • As modificações de pedido (OrderModify) são substituídas por uma única chamada StartProtection, que cobre distâncias de stop-loss e take-profit com arredondamento com reconhecimento de exchange.

Dicas de uso

  • Certifique-se de que a segurança tenha metadados PriceStep, VolumeStep, MinVolume e MaxVolume adequados para que o ajuste de volume integrado possa funcionar corretamente.
  • Alinhe BaseVolume, MinVolume e MaxVolume com o instrumento que você negocia. O construtor também atribui o volume base ajustado a Strategy.Volume, o que torna as ações manuais na IU consistentes com pedidos automatizados.
  • Aumente o intervalo de tempo ou ADX período quando os sinais +DI/-DI se tornam muito barulhentos; a lógica de redução tem melhor desempenho durante tendências constantes.
  • Desative EnableTakeProfit se preferir deixar o stop loss sair da posição em vez de escalar pequenos lucros.
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 StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the "errorEA" MetaTrader strategy that compares +DI and -DI lines of ADX.
/// </summary>
public class ErrorEaStrategy : Strategy
{
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<bool> _enableRiskControl;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _minVolume;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _riskDivider;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<bool> _enableTakeProfit;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private AverageDirectionalIndex _adx;
	private int _longTrades;
	private int _shortTrades;

	/// <summary>
	/// ADX averaging period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// Maximum number of scale-in entries per direction.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Enables dynamic position sizing based on the portfolio value.
	/// </summary>
	public bool EnableRiskControl
	{
		get => _enableRiskControl.Value;
		set => _enableRiskControl.Value = value;
	}

	/// <summary>
	/// Maximum order volume allowed by the strategy.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Minimum order volume that should be used.
	/// </summary>
	public decimal MinVolume
	{
		get => _minVolume.Value;
		set => _minVolume.Value = value;
	}

	/// <summary>
	/// Base volume multiplier that matches the MiniLots parameter from MQL.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Divider applied to the portfolio value when risk control is enabled.
	/// </summary>
	public decimal RiskDivider
	{
		get => _riskDivider.Value;
		set => _riskDivider.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Enables the scalping take-profit mode from the original EA.
	/// </summary>
	public bool EnableTakeProfit
	{
		get => _enableTakeProfit.Value;
		set => _enableTakeProfit.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Candle type used for data subscription.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ErrorEaStrategy"/> class.
	/// </summary>
	public ErrorEaStrategy()
	{
		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetRange(5, 50)
			.SetDisplay("ADX Period", "Smoothing period for the Average Directional Index", "Indicators")
			;

		_maxTrades = Param(nameof(MaxTrades), 9)
			.SetRange(1, 15)
			.SetDisplay("Max Trades", "Maximum number of simultaneous entries per direction", "Risk")
			;

		_enableRiskControl = Param(nameof(EnableRiskControl), true)
			.SetDisplay("Enable Risk Control", "Adjust volume by portfolio value similar to the MQL version", "Risk");

		_maxVolume = Param(nameof(MaxVolume), 3m)
			.SetNotNegative()
			.SetDisplay("Max Volume", "Upper limit for market orders", "Risk");

		_minVolume = Param(nameof(MinVolume), 0.01m)
			.SetNotNegative()
			.SetDisplay("Min Volume", "Lower limit for market orders", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.15m)
			.SetNotNegative()
			.SetDisplay("Base Volume", "Base lot used before applying risk control", "Risk")
			;

		_riskDivider = Param(nameof(RiskDivider), 10000m)
			.SetNotNegative()
			.SetDisplay("Risk Divider", "Portfolio divider used to scale volume when risk control is enabled", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetNotNegative()
			.SetDisplay("Stop Loss Points", "Stop distance converted to price steps", "Protection")
			;

		_enableTakeProfit = Param(nameof(EnableTakeProfit), true)
			.SetDisplay("Enable Take Profit", "Activate the small scalping take profit from the EA", "Protection");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 10)
			.SetNotNegative()
			.SetDisplay("Take Profit Points", "Take-profit distance converted to price steps", "Protection")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for the strategy", "General");
	}

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

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

		_adx = null;
		_longTrades = 0;
		_shortTrades = 0;

		Volume = BaseVolume;
	}

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

		_adx = new AverageDirectionalIndex { Length = AdxPeriod };

		// Subscribe to the configured candle series and calculate ADX on the fly.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_adx, ProcessCandle)
			.Start();

		var takeProfitUnit = EnableTakeProfit && TakeProfitPoints > 0
			? new Unit(TakeProfitPoints, UnitTypes.Absolute)
			: null;
		var stopLossUnit = StopLossPoints > 0
			? new Unit(StopLossPoints, UnitTypes.Absolute)
			: null;

		// Mirror the original stop-loss and scalping take-profit distances.
		StartProtection(
			takeProfit: takeProfitUnit,
			stopLoss: stopLossUnit,
			useMarketOrders: true);

		// Preload the base volume so manual actions in the UI use the same size.
		var adjustedVolume = AdjustVolume(BaseVolume);
		Volume = adjustedVolume > 0m ? adjustedVolume : BaseVolume;

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

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
	{
		// Only evaluate completed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait until ADX indicator is formed.
		if (!_adx.IsFormed)
			return;

		// Ensure ADX produced a final value for this bar.
		if (adxValue is not AverageDirectionalIndexValue adx || !adxValue.IsFinal)
			return;

		var plusDi = adx.Dx.Plus ?? 0m;
		var minusDi = adx.Dx.Minus ?? 0m;

		// Compare +DI and -DI components to determine the signal.
		var direction = CalculateDirection(plusDi, minusDi);

		switch (direction)
		{
			case > 0:
				HandleLongSignal();
				break;
			case < 0:
				HandleShortSignal();
				break;
			default:
				break;
		}
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		// Reset scaling counters once the net position flips or becomes flat.
		if (Position == 0)
		{
			_longTrades = 0;
			_shortTrades = 0;
		}
		else if (Position > 0)
		{
			_shortTrades = 0;
		}
		else
		{
			_longTrades = 0;
		}
	}

	private int CalculateDirection(decimal plusDi, decimal minusDi)
	{
		if (plusDi > minusDi)
			return 1;

		if (minusDi > plusDi)
			return -1;

		return 0;
	}

	private void HandleLongSignal()
	{
		if (Security is null)
			return;

		// Netting accounts cannot keep opposite positions, so close shorts first.
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			_shortTrades = 0;
		}

		// Respect the scaling cap inherited from the original EA.
		if (_longTrades >= MaxTrades)
			return;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return;

		// Add one more market order using the calculated lot size.
		BuyMarket(volume);
		_longTrades++;
	}

	private void HandleShortSignal()
	{
		if (Security is null)
			return;

		// Flat the long exposure before opening new short trades.
		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
			_longTrades = 0;
		}

		if (_shortTrades >= MaxTrades)
			return;

		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_shortTrades++;
	}

	private decimal CalculateOrderVolume()
	{
		// Start from the base lot size defined by BaseVolume.
		var volume = BaseVolume;

		if (EnableRiskControl)
		{
			// Reproduce the PowerRisk logic: balance / divider with a floor of 1.
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (portfolioValue <= 0m)
				portfolioValue = 0m;

			var riskFactor = RiskDivider > 0m ? portfolioValue / RiskDivider : 0m;

			if (riskFactor < 1m)
				riskFactor = 1m;

			volume *= riskFactor;
		}

		// Apply user-defined caps before exchange-specific adjustments.
		if (MaxVolume > 0m && volume > MaxVolume)
			volume = MaxVolume;

		if (MinVolume > 0m && volume < MinVolume)
			volume = MinVolume;

		// Align with exchange volume constraints.
		var adjusted = AdjustVolume(volume);
		if (MaxVolume > 0m && adjusted > MaxVolume)
			adjusted = MaxVolume;

		if (adjusted <= 0m && MinVolume > 0m)
			adjusted = MinVolume;

		return adjusted;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (Security is null)
			return volume;

		var step = Security.VolumeStep ?? 0m;
		if (step > 0m)
		{
			// Round the value to the nearest allowed volume step.
			var rounded = step * Math.Floor(volume / step);
			volume = rounded > 0m ? rounded : step;
		}

		var minVolume = Security.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var maxVolume = Security.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
			volume = maxVolume.Value;

		return volume;
	}
}