Ver en GitHub

ErrorEstrategia EA

Descripción general

La Estrategia ErrorEA es un puerto StockSharp del asesor MetaTrader errorEA.mq4. El experto original comparó los componentes +DI y -DI del índice direccional promedio y siguió acumulando órdenes de mercado en la dirección de la tendencia detectada mientras aplicaba un stop-loss de seguridad muy grande y una toma de ganancias ajustada. Esta versión de C# recrea la misma idea con el nivel alto API de StockSharp, agrega controles de parámetros claros y documenta el modelo de riesgo explícitamente.

Lógica comercial

  1. Suscríbase al período de tiempo configurado (CandleType) y alimente un indicador AverageDirectionalIndex con las velas entrantes.
  2. Espere hasta que la vela esté completamente cerrada y ADX produzca un valor final para esa barra.
  3. Compare las líneas +DI y -DI:
    • si +DI > -DI, la estrategia trata el mercado como alcista;
    • si -DI > +DI, el mercado se considera bajista;
    • valores iguales no generan nuevas señales.
  4. Ante una señal alcista:
    • aplanar una posición neta corta existente (StockSharp utiliza cuentas de compensación, por lo que se cierran las coberturas opuestas);
    • Si el número de operaciones escaladas largas aún es inferior a MaxTrades, envíe una orden de compra de mercado más con el volumen devuelto por el bloque de control de riesgo.
  5. Ante una señal bajista:
    • cerrar una posición larga existente;
    • si el número de tramos cortos es inferior a MaxTrades, envíe una orden de venta de mercado con la misma lógica de tamaño de posición.
  6. Las órdenes de protección son gestionadas por StartProtection:
    • StopLossPoints se convierte en pasos de precio y funciona como un stop fijo amplio, al igual que la entrada StopLoss en MetaTrader;
    • si EnableTakeProfit es verdadero, TakeProfitPoints replica el pequeño objetivo de especulación que EA aplicó a través de OrderModify.
  7. Los contadores de posición (_longTrades/_shortTrades) se reinician cada vez que la posición neta vuelve a cero o gira hacia el lado opuesto, lo que garantiza que el límite de ampliación se aplique en los stop-outs y reversiones.

Gestión de riesgos y dimensionamiento.

  • BaseVolume refleja la entrada MiniLots de MetaTrader. Actúa como el tamaño del lote inicial para cada operación.
  • Cuando EnableRiskControl es verdadero, la estrategia reproduce la fórmula PowerRisk original: volume = BaseVolume * max(1, PortfolioValue / RiskDivider). El divisor predeterminado (10000) coincide con la implementación de MQL.
  • Después de aplicar la fórmula, el resultado está limitado por MinVolume, MaxVolume, los límites de intercambio (Security.MinVolume, Security.MaxVolume) y el paso de volumen (Security.VolumeStep). Esto evita que el EA solicite un tamaño que el lugar rechazaría.
  • El tamaño calculado se utiliza para cada nuevo orden de escala mientras la dirección correspondiente permanece dentro del límite MaxTrades.

Parámetros

Nombre Tipo Predeterminado MetaTrader contraparte Descripción
AdxPeriod int 14 iADX(..., 14, ...) Período de suavizado del Índice Direccional Promedio.
CandleType DataType plazo de 15 minutos cronograma del gráfico Serie de velas utilizada para todos los cálculos.
MaxTrades int 9 MaxTrades Número máximo de pedidos escalables por dirección.
EnableRiskControl bool true RiskControl Habilita el cálculo dinámico de lotes en función del valor de la cartera.
BaseVolume decimal 0.15 MiniLots Tamaño del lote base antes de aplicar el multiplicador de riesgo.
RiskDivider decimal 10000 implícito (divisor en PowerRisk) Divisor que se aplica al valor de la cartera cuando el control de riesgos está activo.
MaxVolume decimal 3 MaxLot Límite para el volumen calculado automáticamente (antes del redondeo cambiario).
MinVolume decimal 0.01 MarketInfo(..., MODE_MINLOT) Volumen mínimo permitido en el pedido final.
StopLossPoints int 1000 StopLoss Distancia de stop-loss en pasos de precio. Establezca en 0 para desactivar la parada.
EnableTakeProfit bool true ScalpeControl Permite la obtención de beneficios mediante especulación estricta.
TakeProfitPoints int 10 ScalpeProfit Distancia de obtención de beneficios en pasos de precio.

Diferencias con el asesor experto original

  • La versión MetaTrader contenía un error que sobrescribía el valor +DI con el valor -DI. El puerto StockSharp compara los componentes correctos, reflejando el comportamiento previsto de la estrategia.
  • MetaTrader permite cobertura. StockSharp opera en un entorno de compensación, por lo que el puerto cierra la exposición opuesta antes de agregar nuevas operaciones en la dirección de la señal.
  • La detección de deslizamiento (GetSlippage) y la salida de comentarios se eliminaron porque StockSharp maneja internamente el deslizamiento de pedidos y las cadenas de riesgo eran puramente cosméticas.
  • Las modificaciones de órdenes (OrderModify) se reemplazan con una única llamada StartProtection, que cubre distancias de stop-loss y take-profit con redondeo según el tipo de cambio.

Consejos de uso

  • Asegúrese de que la seguridad tenga metadatos PriceStep, VolumeStep, MinVolume y MaxVolume adecuados para que el ajuste de volumen integrado pueda funcionar correctamente.
  • Alinee BaseVolume, MinVolume y MaxVolume con el instrumento con el que opera. El constructor también asigna el volumen base ajustado a Strategy.Volume, lo que hace que las acciones manuales en la interfaz de usuario sean consistentes con los pedidos automatizados.
  • Aumente el período de tiempo o ADX período en el que las señales +DI/-DI se vuelven demasiado ruidosas; La lógica de escalado funciona mejor durante tendencias constantes.
  • Desactive EnableTakeProfit si prefiere dejar que el stop-loss salga de la posición en lugar de arrancar pequeñas ganancias.
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;
	}
}