Ver en GitHub

Estrategia Executor AO

Descripción general

Executor AO es una estrategia de saucer (platillo) basada en el Awesome Oscillator, originalmente distribuida como el experto de MetaTrader "Executor AO". El puerto a StockSharp conserva la detección de reversión basada en indicadores mientras simplifica la gestión del dinero a un tamaño de orden fijo. La estrategia observa las velas completadas del marco temporal configurado, evalúa el cambio de pendiente del Awesome Oscillator, y abre una sola posición neta cuando ocurren condiciones alcistas o bajistas por debajo o por encima de la línea cero. El stop de protección opcional, el take-profit y la lógica de trailing reproducen el comportamiento de gestión de riesgo del EA original.

Lógica de trading

  1. Suscribirse a la serie de velas definida por CandleType y alimentar cada vela cerrada al Awesome Oscillator con los parámetros AoShortPeriod y AoLongPeriod configurados.
  2. Almacenar los tres últimos valores completados del Awesome Oscillator para reproducir el patrón de acceso al búfer de MetaTrader utilizado por el experto original.
  3. Cuando no hay posición abierta:
    • Configuración alcista: el último valor de AO es mayor que el anterior, el valor anterior es menor que el valor hace dos barras (un mínimo), y el último valor permanece por debajo de -MinimumAoIndent. En ese caso, enviar una orden de compra a mercado con TradeVolume lotes.
    • Configuración bajista: el último valor de AO es menor que el anterior, el valor anterior es mayor que el valor hace dos barras (un máximo), y el último valor permanece por encima de MinimumAoIndent. En ese caso, enviar una orden de venta a mercado con el volumen fijo.
  4. Cuando existe una posición, la estrategia emula las salidas del EA:
    • Calcular los precios de stop-loss y take-profit desde la entrada usando StopLossPips y TakeProfitPips multiplicados por el tamaño de pip ajustado (se replica el manejo de 3/5 dígitos de MetaTrader).
    • Aplicar la regla de trailing stop cuando el precio se mueve a favor de la posición más de TrailingStopPips + TrailingStepPips pips. El stop solo se avanza si el nuevo nivel está más allá del anterior, coincidiendo con el requisito de paso de trailing del EA.
    • Cerrar posiciones largas cuando el precio toca el take-profit o stop-loss, o cuando el valor del Awesome Oscillator de la barra anterior se vuelve positivo. Cerrar posiciones cortas cuando el precio alcanza sus objetivos o el valor anterior de AO cae por debajo de cero.
  5. Todas las órdenes son a mercado; el modelo de posición neta de StockSharp asegura que solo una dirección esté activa a la vez.

Parámetros

Nombre Tipo Predeterminado Descripción
CandleType DataType Velas de 5 minutos Marco temporal principal utilizado para calcular y operar la estrategia.
TradeVolume decimal 1 Tamaño de orden fijo utilizado para cada entrada.
AoShortPeriod int 5 Período rápido para la SMA corta del Awesome Oscillator.
AoLongPeriod int 34 Período lento para la SMA larga del Awesome Oscillator.
MinimumAoIndent decimal 0.001 Distancia absoluta mínima desde cero requerida para nuevas señales. Previene operaciones cuando AO ronda el cero.
StopLossPips decimal 50 Distancia de stop-loss protector expresada en pips estilo MetaTrader. Poner en 0 para deshabilitar el stop.
TakeProfitPips decimal 50 Distancia de take-profit expresada en pips. Poner en 0 para deshabilitar el objetivo.
TrailingStopPips decimal 5 Distancia de activación del trailing stop. Solo se usa cuando es mayor que cero.
TrailingStepPips decimal 5 Mejora mínima de precio requerida antes de actualizar el trailing stop. Debe permanecer positivo cuando el trailing está habilitado.

Diferencias respecto al EA de MetaTrader

  • La versión de MetaTrader permitía el dimensionamiento de posiciones basado en riesgo. El puerto a StockSharp implementa la opción de lote fijo (TradeVolume) y deja fuera la gestión por porcentaje de riesgo por claridad.
  • La gestión de órdenes se simula dentro de la estrategia: cuando los umbrales de stop-loss o take-profit se alcanzan en velas completadas, la estrategia envía órdenes a mercado para aplanar la posición. Esto refleja el comportamiento del EA sin crear órdenes hijo separadas.
  • Los ajustes de trailing ocurren en eventos de cierre de vela en lugar de en cada tick. Esto mantiene la implementación coherente con la API de alto nivel mientras sigue la misma lógica de umbral.
  • Todas las rutas de código se basan en el patrón SubscribeCandles + Bind de alto nivel de StockSharp en lugar de copiar manualmente los búferes del indicador.

Consejos de uso

  • Alinear TradeVolume con el paso de lote del instrumento antes de iniciar la estrategia. El constructor también asigna el mismo valor a Strategy.Volume, por lo que los métodos auxiliares usan automáticamente el tamaño elegido.
  • MinimumAoIndent puede aumentarse en mercados ruidosos para evitar cambios frecuentes cerca de cero. Establecerlo en 0 reproduce el comportamiento más agresivo del EA.
  • Al habilitar el trailing stop, mantener TrailingStepPips por encima de cero; de lo contrario el constructor lanza una excepción, reproduciendo la validación de parámetros del EA original.
  • Adjuntar la estrategia a un gráfico para visualizar tanto las velas como el Awesome Oscillator superpuesto. Esto ayuda a validar la detección de mínimos/máximos después de la conversión.

Indicador

  • Awesome Oscillator: diferencia entre una media móvil simple rápida y una lenta del precio mediano. La configuración predeterminada 5/34 coincide con el indicador de MetaTrader.
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;
	}
}