View on GitHub

Executor AO Strategy

Overview

Executor AO is a saucer-style Awesome Oscillator strategy originally distributed as the "Executor AO" MetaTrader expert advisor. The StockSharp port keeps the indicator-based reversal detection while simplifying money management to a fixed order size. The strategy watches completed candles from the configured timeframe, evaluates the Awesome Oscillator slope change, and opens a single net position whenever bullish or bearish conditions occur below or above the zero line. Optional protective stop, take- profit, and trailing logic reproduce the original EA's risk management behaviour.

Trading logic

  1. Subscribe to the candle series defined by CandleType and feed every finished candle into the Awesome Oscillator with the configured AoShortPeriod and AoLongPeriod parameters.
  2. Store the last three completed Awesome Oscillator values to reproduce the MetaTrader buffer access pattern used by the original expert.
  3. When no position is open:
    • Bullish setup: the latest AO value is greater than the previous one, the previous value is lower than the value two bars ago (a trough), and the latest value remains below -MinimumAoIndent. In that case send a market buy order with TradeVolume lots.
    • Bearish setup: the latest AO value is smaller than the previous one, the previous value is higher than the value two bars ago (a peak), and the latest value stays above MinimumAoIndent. In that case submit a market sell order with the fixed volume.
  4. When a position exists, the strategy emulates the EA's exits:
    • Calculate stop-loss and take-profit prices from the entry using StopLossPips and TakeProfitPips multiplied by the adjusted pip size (MetaTrader's 3/5-digit handling is replicated).
    • Apply the trailing-stop rule whenever price moves in favour of the position by more than TrailingStopPips + TrailingStepPips pips. The stop is only advanced if the new level is beyond the previous one, matching the EA's trailing step requirement.
    • Close long positions when price touches the take-profit or stop-loss or when the Awesome Oscillator value from the previous bar turns positive. Close short positions when price hits their targets or the previous AO value falls below zero.
  5. All orders are market orders; StockSharp's net position model ensures only one direction is active at a time.

Parameters

Name Type Default Description
CandleType DataType 5-minute candles Primary timeframe used to compute and trade the strategy.
TradeVolume decimal 1 Fixed order size used for every entry.
AoShortPeriod int 5 Fast period for the Awesome Oscillator's short SMA.
AoLongPeriod int 34 Slow period for the Awesome Oscillator's long SMA.
MinimumAoIndent decimal 0.001 Minimum absolute distance from zero required for new signals. Prevents trades when AO hovers around zero.
StopLossPips decimal 50 Protective stop-loss distance expressed in MetaTrader-style pips. Set to 0 to disable the stop.
TakeProfitPips decimal 50 Take-profit distance expressed in pips. Set to 0 to disable the target.
TrailingStopPips decimal 5 Trailing-stop activation distance. Only used when greater than zero.
TrailingStepPips decimal 5 Minimum price improvement required before the trailing stop is updated. Must stay positive when trailing is enabled.

Differences versus the MetaTrader EA

  • The MetaTrader version allowed risk-based position sizing. The StockSharp port implements the fixed-lot option (TradeVolume) and leaves percent-risk management out for clarity.
  • Order management is simulated inside the strategy: when stop-loss or take-profit thresholds are reached on completed candles, the strategy sends market orders to flatten the position. This mirrors the EA's behaviour without creating separate child orders.
  • Trailing adjustments occur on candle close events rather than on every tick. This keeps the implementation consistent with the high-level API while following the same threshold logic.
  • All code paths rely on StockSharp's high-level SubscribeCandles + Bind pattern instead of manually copying indicator buffers.

Usage tips

  • Align TradeVolume with the instrument's lot step before starting the strategy. The constructor also assigns the same value to Strategy.Volume, so helper methods automatically use the chosen size.
  • MinimumAoIndent can be increased on noisy markets to avoid frequent flips near zero. Setting it to 0 reproduces the most aggressive behaviour of the EA.
  • When enabling the trailing stop, keep TrailingStepPips above zero; otherwise the constructor throws an exception, reproducing the original EA's parameter validation.
  • Attach the strategy to a chart to visualise both candles and the Awesome Oscillator overlay. This helps validate trough/peak detection after conversion.

Indicator

  • Awesome Oscillator: difference between a fast and a slow simple moving average of the median price. The default 5/34 configuration matches the MetaTrader indicator.
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;
	}
}