Auf GitHub ansehen

Executor AO Strategie

Überblick

Executor AO ist eine Saucer-Strategie für den Awesome Oscillator, die ursprünglich als "Executor AO" MetaTrader-Experte verteilt wurde. Der StockSharp-Port behält die indikatorbasierte Umkehrerkennung bei und vereinfacht das Geldmanagement auf eine feste Ordergröße. Die Strategie beobachtet abgeschlossene Kerzen des konfigurierten Zeitrahmens, wertet die Steigungsänderung des Awesome Oscillators aus und öffnet eine einzelne Nettoposition, wenn bullische oder bärische Bedingungen unter oder über der Nulllinie auftreten. Der optionale Schutz-Stop, Take-Profit und die Trailing-Logik reproduzieren das Risikomanagementverhalten des Original-EA.

Handelslogik

  1. Die Kerzenserie, die durch CandleType definiert ist, abonnieren und jede fertige Kerze mit den konfigurierten Parametern AoShortPeriod und AoLongPeriod in den Awesome Oscillator einspeisen.
  2. Die letzten drei abgeschlossenen Awesome-Oscillator-Werte speichern, um das MetaTrader-Pufferzugriffsmuster des Original-Experten zu reproduzieren.
  3. Wenn keine Position offen ist:
    • Bullische Aufstellung: Der neueste AO-Wert ist größer als der vorherige, der vorherige Wert ist kleiner als der Wert vor zwei Balken (ein Tal), und der neueste Wert bleibt unter -MinimumAoIndent. In diesem Fall eine Markt-Kauforder mit TradeVolume Lots senden.
    • Bärische Aufstellung: Der neueste AO-Wert ist kleiner als der vorherige, der vorherige Wert ist größer als der Wert vor zwei Balken (ein Gipfel), und der neueste Wert bleibt über MinimumAoIndent. In diesem Fall eine Markt-Verkaufsorder mit dem festen Volumen einreichen.
  4. Wenn eine Position besteht, emuliert die Strategie die Ausstiege des EA:
    • Stop-Loss- und Take-Profit-Preise vom Einstieg aus berechnen, indem StopLossPips und TakeProfitPips mit der angepassten Pip-Größe multipliziert werden (MetaTraders 3/5-Stellenbehandlung wird repliziert).
    • Die Trailing-Stop-Regel anwenden, wenn sich der Preis zugunsten der Position um mehr als TrailingStopPips + TrailingStepPips Pips bewegt. Der Stop wird nur vorgerückt, wenn das neue Niveau über dem vorherigen liegt, entsprechend der Trailing-Step-Anforderung des EA.
    • Long-Positionen schließen, wenn der Preis den Take-Profit oder Stop-Loss berührt oder wenn der Awesome-Oscillator-Wert des vorherigen Balkens positiv wird. Short-Positionen schließen, wenn der Preis ihre Ziele trifft oder der vorherige AO-Wert unter null fällt.
  5. Alle Orders sind Marktorders; das Nettoppositionsmodell von StockSharp stellt sicher, dass nur eine Richtung gleichzeitig aktiv ist.

Parameter

Name Typ Standard Beschreibung
CandleType DataType 5-Minuten-Kerzen Primärer Zeitrahmen zur Berechnung und zum Handel der Strategie.
TradeVolume decimal 1 Feste Ordergröße für jeden Einstieg.
AoShortPeriod int 5 Schnelle Periode für den kurzen SMA des Awesome Oscillators.
AoLongPeriod int 34 Langsame Periode für den langen SMA des Awesome Oscillators.
MinimumAoIndent decimal 0.001 Mindestabstand von null für neue Signale. Verhindert Trades, wenn AO nahe null schwebt.
StopLossPips decimal 50 Schutz-Stop-Loss-Abstand in MetaTrader-Pips. Auf 0 setzen, um den Stop zu deaktivieren.
TakeProfitPips decimal 50 Take-Profit-Abstand in Pips. Auf 0 setzen, um das Ziel zu deaktivieren.
TrailingStopPips decimal 5 Trailing-Stop-Aktivierungsabstand. Wird nur verwendet, wenn größer als null.
TrailingStepPips decimal 5 Mindest-Preisverbesserung vor Aktualisierung des Trailing Stops. Muss positiv bleiben, wenn Trailing aktiviert ist.

Unterschiede zum MetaTrader-EA

  • Die MetaTrader-Version erlaubte risikobasierte Positionsgrößenbestimmung. Der StockSharp-Port implementiert die Festlot-Option (TradeVolume) und lässt das Prozentrisikomanagement aus Gründen der Klarheit weg.
  • Das Ordermanagement wird innerhalb der Strategie simuliert: Wenn Stop-Loss- oder Take-Profit-Schwellen bei abgeschlossenen Kerzen erreicht werden, sendet die Strategie Marktorders, um die Position zu schließen. Dies spiegelt das EA-Verhalten ohne separate Kind-Orders wider.
  • Trailing-Anpassungen erfolgen bei Kerzenschluss-Ereignissen statt bei jedem Tick. Dies hält die Implementierung konsistent mit der High-Level-API bei gleicher Schwellenlogik.
  • Alle Code-Pfade verwenden das SubscribeCandles + Bind-Muster von StockSharp statt manuell Indikatorpuffer zu kopieren.

Verwendungstipps

  • TradeVolume vor dem Start der Strategie am Lot-Schritt des Instruments ausrichten. Der Konstruktor weist denselben Wert auch Strategy.Volume zu, sodass Hilfsmethoden automatisch die gewählte Größe verwenden.
  • MinimumAoIndent kann auf rauschenden Märkten erhöht werden, um häufige Wechsel nahe null zu vermeiden. Es auf 0 zu setzen reproduziert das aggressivste Verhalten des EA.
  • Beim Aktivieren des Trailing Stops TrailingStepPips über null halten; andernfalls wirft der Konstruktor eine Exception und reproduziert damit die Parametervalidierung des Original-EA.
  • Die Strategie an ein Diagramm anhängen, um sowohl Kerzen als auch den Awesome-Oscillator-Overlay zu visualisieren. Dies hilft bei der Validierung der Tal-/Gipfelerkennung nach der Konvertierung.

Indikator

  • Awesome Oscillator: Differenz zwischen einem schnellen und einem langsamen einfachen gleitenden Durchschnitt des Medianpreises. Die Standard-5/34-Konfiguration entspricht dem MetaTrader-Indikator.
using System;

using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Awesome Oscillator swing strategy converted from the "Executor AO" MetaTrader expert.
/// Implements the saucer-based entry logic with optional stop, take-profit, and trailing exit management.
/// </summary>
public class ExecutorAoStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _aoShortPeriod;
	private readonly StrategyParam<int> _aoLongPeriod;
	private readonly StrategyParam<decimal> _minimumAoIndent;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;
	private static readonly object _sync = new();

	private AwesomeOscillator _ao = null!;

	private decimal? _currentAo;
	private decimal? _previousAo;
	private decimal? _previousAo2;

	private decimal _pipSize;

	private decimal? _longEntryPrice;
	private decimal? _longStop;
	private decimal? _longTake;

	private decimal? _shortEntryPrice;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExecutorAoStrategy"/> class.
	/// </summary>
	public ExecutorAoStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Fixed order size", "Risk")
			;

		_aoShortPeriod = Param(nameof(AoShortPeriod), 5)
			.SetDisplay("AO Short Period", "Fast period for Awesome Oscillator", "Indicators")
			;

		_aoLongPeriod = Param(nameof(AoLongPeriod), 34)
			.SetDisplay("AO Long Period", "Slow period for Awesome Oscillator", "Indicators")
			;

		_minimumAoIndent = Param(nameof(MinimumAoIndent), 0.001m)
			.SetNotNegative()
			.SetDisplay("Minimum AO Indent", "Minimum distance from zero before signals are valid", "Logic")
			;

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk")
			;

		_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing distance in pips", "Risk")
			;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Minimum move before trailing adjusts", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for analysis", "General");
	}

	/// <summary>
	/// Fixed order volume used for market entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Fast period for the Awesome Oscillator calculation.
	/// </summary>
	public int AoShortPeriod
	{
		get => _aoShortPeriod.Value;
		set => _aoShortPeriod.Value = value;
	}

	/// <summary>
	/// Slow period for the Awesome Oscillator calculation.
	/// </summary>
	public int AoLongPeriod
	{
		get => _aoLongPeriod.Value;
		set => _aoLongPeriod.Value = value;
	}

	/// <summary>
	/// Minimum absolute AO value required before trades are allowed.
	/// </summary>
	public decimal MinimumAoIndent
	{
		get => _minimumAoIndent.Value;
		set => _minimumAoIndent.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum step required before the trailing stop moves.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Candle series used to generate signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_ao = null!;
		_currentAo = null;
		_previousAo = null;
		_previousAo2 = null;
		_pipSize = 0m;
		ResetLongState();
		ResetShortState();
	}

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

		Volume = TradeVolume;
		_pipSize = CalculatePipSize();

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
			throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");

		_ao = new AwesomeOscillator
		{
			ShortMa = { Length = AoShortPeriod },
			LongMa = { Length = AoLongPeriod }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(candle => ProcessCandle(candle))
			.Start();

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		lock (_sync)
		{
			var aoValue = _ao.Process(new CandleIndicatorValue(_ao, candle) { IsFinal = true });
			if (!aoValue.IsFinal || _ao == null || !_ao.IsFormed)
				return;

			var previousAo = _currentAo;
			var previousAo2 = _previousAo;

			var positionClosed = HandleActivePositions(candle, previousAo);

			StoreAoValue(aoValue.ToDecimal());

			if (positionClosed || !previousAo.HasValue || !previousAo2.HasValue || !_currentAo.HasValue)
				return;

			if (Position != 0m)
				return;

			var current = _currentAo.Value;
			var prev = previousAo.Value;
			var prev2 = previousAo2.Value;
			var indent = MinimumAoIndent;

			if (current > prev && prev < prev2 && current <= -indent)
			{
				OpenLong(candle.ClosePrice);
				return;
			}

			if (current < prev && prev > prev2 && current >= indent)
				OpenShort(candle.ClosePrice);
		}
	}

	private bool HandleActivePositions(ICandleMessage candle, decimal? previousAo)
	{
		if (Position > 0m)
		{
			_longEntryPrice ??= candle.ClosePrice;
			UpdateTrailingForLong(candle);

			if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetLongState();
				return true;
			}

			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetLongState();
				return true;
			}

			if (previousAo.HasValue && previousAo.Value > 0m)
			{
				SellMarket(Math.Abs(Position));
				ResetLongState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			_shortEntryPrice ??= candle.ClosePrice;
			UpdateTrailingForShort(candle);

			if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetShortState();
				return true;
			}

			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetShortState();
				return true;
			}

			if (previousAo.HasValue && previousAo.Value < 0m)
			{
				BuyMarket(Math.Abs(Position));
				ResetShortState();
				return true;
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}

		return false;
	}

	private void OpenLong(decimal price)
	{
		var volume = GetTradeVolume();
		if (volume <= 0m)
			return;

		BuyMarket(volume);

		_longEntryPrice = price;
		_longStop = StopLossPips > 0m ? price - StopLossPips * _pipSize : null;
		_longTake = TakeProfitPips > 0m ? price + TakeProfitPips * _pipSize : null;
		ResetShortState();
	}

	private void OpenShort(decimal price)
	{
		var volume = GetTradeVolume();
		if (volume <= 0m)
			return;

		SellMarket(volume);

		_shortEntryPrice = price;
		_shortStop = StopLossPips > 0m ? price + StopLossPips * _pipSize : null;
		_shortTake = TakeProfitPips > 0m ? price - TakeProfitPips * _pipSize : null;
		ResetLongState();
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || !_longEntryPrice.HasValue)
			return;

		var trailingDistance = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var price = candle.ClosePrice;
		var entry = _longEntryPrice.Value;

		if (price - entry > trailingDistance + trailingStep)
		{
			var minimalAllowed = price - (trailingDistance + trailingStep);
			if (!_longStop.HasValue || _longStop.Value < minimalAllowed)
				_longStop = price - trailingDistance;
		}
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || !_shortEntryPrice.HasValue)
			return;

		var trailingDistance = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var price = candle.ClosePrice;
		var entry = _shortEntryPrice.Value;

		if (entry - price > trailingDistance + trailingStep)
		{
			var maximalAllowed = price + (trailingDistance + trailingStep);
			if (!_shortStop.HasValue || _shortStop.Value > maximalAllowed)
				_shortStop = price + trailingDistance;
		}
	}

	private decimal GetTradeVolume()
	{
		var volume = Volume;
		if (volume <= 0m)
			volume = TradeVolume;
		return volume;
	}

	private void StoreAoValue(decimal value)
	{
		_previousAo2 = _previousAo;
		_previousAo = _currentAo;
		_currentAo = value;
	}

	private decimal CalculatePipSize()
	{
		var priceStep = Security?.PriceStep ?? 1m;
		if (priceStep <= 0m)
			priceStep = 1m;

		var decimals = GetDecimalPlaces(priceStep);
		var factor = decimals == 3 || decimals == 5 ? 10m : 1m;
		return priceStep * factor;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		value = Math.Abs(value);
		if (value == 0m)
			return 0;

		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStop = null;
		_shortTake = null;
	}
}