Ver en GitHub

ErrorEA Strategy

Overview

The ErrorEA Strategy is a StockSharp port of the MetaTrader advisor errorEA.mq4. The original expert compared the +DI and -DI components of the Average Directional Index and kept stacking market orders in the detected trend direction while applying a very large safety stop-loss and a tight scalping take-profit. This C# version recreates the same idea with StockSharp's high-level API, adds clear parameter controls, and documents the risk model explicitly.

Trading logic

  1. Subscribe to the configured timeframe (CandleType) and feed an AverageDirectionalIndex indicator with the incoming candles.
  2. Wait until the candle is fully closed and the ADX produces a final value for that bar.
  3. Compare the +DI and -DI lines:
    • if +DI > -DI, the strategy treats the market as bullish;
    • if -DI > +DI, the market is considered bearish;
    • equal values generate no new signals.
  4. On a bullish signal:
    • flatten an existing short net position (StockSharp uses netting accounts, so opposite hedges are closed);
    • if the number of long scale-in trades is still below MaxTrades, send one more market buy order with the volume returned by the risk-control block.
  5. On a bearish signal:
    • close an existing long position;
    • if the number of short tranches is below MaxTrades, send one market sell order with the same position-sizing logic.
  6. Protective orders are managed by StartProtection:
    • StopLossPoints is converted to price steps and works as a wide fixed stop, just like the StopLoss input in MetaTrader;
    • if EnableTakeProfit is true, TakeProfitPoints replicates the small scalping target that the EA applied through OrderModify.
  7. Position counters (_longTrades/_shortTrades) are reset whenever the net position returns to zero or flips to the opposite side, ensuring the scale-in cap is enforced across stop-outs and reversals.

Risk management and sizing

  • BaseVolume mirrors the MiniLots input from MetaTrader. It acts as the starting lot size for every trade.
  • When EnableRiskControl is true, the strategy reproduces the original PowerRisk formula: volume = BaseVolume * max(1, PortfolioValue / RiskDivider). The default divider (10000) matches the MQL implementation.
  • After the formula is applied, the result is clamped by MinVolume, MaxVolume, the exchange limits (Security.MinVolume, Security.MaxVolume) and the volume step (Security.VolumeStep). This prevents the EA from requesting a size that the venue would reject.
  • The calculated size is used for every new scale-in order while the corresponding direction stays within the MaxTrades cap.

Parameters

Name Type Default MetaTrader counterpart Description
AdxPeriod int 14 iADX(..., 14, ...) Smoothing period of the Average Directional Index.
CandleType DataType 15-minute time frame chart timeframe Candle series used for all calculations.
MaxTrades int 9 MaxTrades Maximum number of scale-in orders per direction.
EnableRiskControl bool true RiskControl Enables the dynamic lot calculation based on the portfolio value.
BaseVolume decimal 0.15 MiniLots Base lot size before applying the risk multiplier.
RiskDivider decimal 10000 implicit (divisor in PowerRisk) Divider applied to the portfolio value when risk control is active.
MaxVolume decimal 3 MaxLot Cap for the auto-calculated volume (before exchange rounding).
MinVolume decimal 0.01 MarketInfo(..., MODE_MINLOT) Minimum volume allowed in the final order.
StopLossPoints int 1000 StopLoss Stop-loss distance in price steps. Set to 0 to disable the stop.
EnableTakeProfit bool true ScalpeControl Enables the tight scalping take-profit.
TakeProfitPoints int 10 ScalpeProfit Take-profit distance in price steps.

Differences from the original expert advisor

  • The MetaTrader version contained a bug that overwrote the +DI value with the -DI value. The StockSharp port compares the correct components, reflecting the intended behaviour of the strategy.
  • MetaTrader allows hedging. StockSharp operates in a netting environment, so the port closes the opposite exposure before adding new trades in the signal direction.
  • Slippage detection (GetSlippage) and comment output were removed because StockSharp handles order slippage internally and the risk strings were purely cosmetic.
  • Order modifications (OrderModify) are replaced with a single StartProtection call, which covers both stop-loss and take-profit distances with exchange-aware rounding.

Usage tips

  • Ensure the security has proper PriceStep, VolumeStep, MinVolume, and MaxVolume metadata so the built-in volume adjustment can work correctly.
  • Align BaseVolume, MinVolume, and MaxVolume with the instrument you trade. The constructor also assigns the adjusted base volume to Strategy.Volume, which makes manual actions in the UI consistent with automated orders.
  • Increase the timeframe or ADX period when the +DI/-DI signals become too noisy; the scale-in logic performs best during steady trends.
  • Disable EnableTakeProfit if you prefer to let the stop-loss exit the position instead of scalping small profits.
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;
	}
}