Auf GitHub ansehen

ErrorEA-Strategie

Überblick

Die ErrorEA-Strategie ist ein StockSharp-Port des MetaTrader-Advisors errorEA.mq4. Der ursprüngliche Experte verglich die +DI- und -DI-Komponenten des Average Directional Index und stapelte weiterhin Marktaufträge in der erkannten Trendrichtung, während er einen sehr hohen Sicherheits-Stop-Loss und einen engen Scalping-Take-Profit anwendete. Diese C#-Version stellt die gleiche Idee mit dem übergeordneten API von StockSharp wieder her, fügt klare Parameterkontrollen hinzu und dokumentiert das Risikomodell explizit.

Handelslogik

  1. Abonnieren Sie den konfigurierten Zeitrahmen (CandleType) und füttern Sie einen AverageDirectionalIndex-Indikator mit den eingehenden Kerzen.
  2. Warten Sie, bis die Kerze vollständig geschlossen ist und ADX einen endgültigen Wert für diesen Balken erzeugt.
  3. Vergleichen Sie die Zeilen +DI und -DI:
    • wenn +DI > -DI, behandelt die Strategie den Markt als bullisch;
    • wenn -DI > +DI, gilt der Markt als bärisch;
    • Gleiche Werte erzeugen keine neuen Signale.
  4. Bei einem bullischen Signal:
    • eine bestehende Short-Nettoposition glätten (StockSharp verwendet Netting-Konten, sodass gegenteilige Absicherungen geschlossen werden);
    • Wenn die Anzahl der Long-Scale-in-Trades immer noch unter MaxTrades liegt, senden Sie eine weitere Marktkauforder mit dem vom Risikokontrollblock zurückgegebenen Volumen.
  5. Bei einem bärischen Signal:
    • eine bestehende Long-Position schließen;
    • Wenn die Anzahl der Short-Tranchen unter MaxTrades liegt, senden Sie einen Marktverkaufsauftrag mit derselben Positionsgrößenlogik.
  6. Schutzanordnungen werden verwaltet von StartProtection:
    • StopLossPoints wird in Preisschritte umgewandelt und fungiert als breiter fester Stop, genau wie die Eingabe StopLoss in MetaTrader;
    • Wenn EnableTakeProfit wahr ist, repliziert TakeProfitPoints das kleine Scalping-Ziel, das EA über OrderModify angewendet hat.
  7. Positionszähler (_longTrades/_shortTrades) werden immer dann zurückgesetzt, wenn die Nettoposition auf Null zurückkehrt oder auf die entgegengesetzte Seite wechselt, um sicherzustellen, dass die Scale-In-Obergrenze bei Stop-Outs und Umkehrungen durchgesetzt wird.

Risikomanagement und Dimensionierung

  • BaseVolume spiegelt die MiniLots-Eingabe von MetaTrader wider. Sie dient als Startlosgröße für jeden Trade.
  • Wenn EnableRiskControl wahr ist, reproduziert die Strategie die ursprüngliche PowerRisk-Formel: volume = BaseVolume * max(1, PortfolioValue / RiskDivider). Der Standardteiler (10000) entspricht der MQL-Implementierung.
  • Nachdem die Formel angewendet wurde, wird das Ergebnis durch MinVolume, MaxVolume, die Austauschlimits (Security.MinVolume, Security.MaxVolume) und den Volumenschritt (Security.VolumeStep) begrenzt. Dadurch wird verhindert, dass EA eine Größe anfordert, die der Veranstaltungsort ablehnen würde.
  • Die berechnete Größe wird für jede neue Skalierungsreihenfolge verwendet, während die entsprechende Richtung innerhalb der Obergrenze MaxTrades bleibt.

Parameter

Name Typ Standard MetaTrader Gegenstück Beschreibung
AdxPeriod int 14 iADX(..., 14, ...) Glättungszeitraum des durchschnittlichen Richtungsindex.
CandleType DataType 15-minütiger Zeitrahmen Zeitrahmen des Diagramms Für alle Berechnungen verwendete Kerzenreihe.
MaxTrades int 9 MaxTrades Maximale Anzahl von Scale-in-Aufträgen pro Richtung.
EnableRiskControl bool true RiskControl Ermöglicht die dynamische Losberechnung basierend auf dem Portfoliowert.
BaseVolume decimal 0.15 MiniLots Basislosgröße vor Anwendung des Risikomultiplikators.
RiskDivider decimal 10000 implizit (Teiler in PowerRisk) Dividendenwert, der auf den Portfoliowert angewendet wird, wenn die Risikokontrolle aktiv ist.
MaxVolume decimal 3 MaxLot Obergrenze für das automatisch berechnete Volumen (vor Börsenrundung).
MinVolume decimal 0.01 MarketInfo(..., MODE_MINLOT) In der endgültigen Bestellung zulässiges Mindestvolumen.
StopLossPoints int 1000 StopLoss Stop-Loss-Distanz in Preisschritten. Auf 0 setzen, um den Stopp zu deaktivieren.
EnableTakeProfit bool true ScalpeControl Ermöglicht das Tight Scalping Take-Profit.
TakeProfitPoints int 10 ScalpeProfit Take-Profit-Distanz in Preisschritten.

Unterschiede zum ursprünglichen Fachberater

  • Die MetaTrader-Version enthielt einen Fehler, der den +DI-Wert mit dem -DI-Wert überschrieb. Der StockSharp-Port vergleicht die richtigen Komponenten und spiegelt das beabsichtigte Verhalten der Strategie wider.
  • MetaTrader ermöglicht eine Absicherung. StockSharp arbeitet in einer Netting-Umgebung, sodass der Port das entgegengesetzte Risiko schließt, bevor er neue Geschäfte in Signalrichtung hinzufügt.
  • Slippage-Erkennung (GetSlippage) und Kommentarausgabe wurden entfernt, da StockSharp Order-Slippage intern verarbeitet und die Risikozeichenfolgen rein kosmetischer Natur waren.
  • Auftragsänderungen (OrderModify) werden durch einen einzelnen StartProtection-Aufruf ersetzt, der sowohl Stop-Loss- als auch Take-Profit-Abstände mit börsenbezogener Rundung abdeckt.

Anwendungstipps

  • Stellen Sie sicher, dass die Sicherheit über die richtigen PriceStep-, VolumeStep-, MinVolume- und MaxVolume-Metadaten verfügt, damit die integrierte Lautstärkeanpassung ordnungsgemäß funktionieren kann.
  • Richten Sie BaseVolume, MinVolume und MaxVolume auf das Instrument aus, mit dem Sie handeln. Der Konstruktor weist außerdem Strategy.Volume das angepasste Basisvolumen zu, wodurch manuelle Aktionen in der Benutzeroberfläche mit automatisierten Bestellungen konsistent werden.
  • Erhöhen Sie den Zeitrahmen oder die ADX-Periode, wenn die +DI/-DI-Signale zu verrauscht werden. Die Scale-In-Logik funktioniert bei stetigen Trends am besten.
  • Deaktivieren Sie EnableTakeProfit, wenn Sie lieber zulassen möchten, dass der Stop-Loss die Position verlässt, anstatt kleine Gewinne zu erzielen.
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;
	}
}