Ver en GitHub

Estrategia OzFx Accelerator Stochastic

Descripción general

  • Conversión del asesor experto MetaTrader OzFx (edición de barabashkakvn) a la API de estrategia de alto nivel de StockSharp.
  • Combina el oscilador Acceleration/Deceleration (AC) con un umbral estocástico para entrar en capas en tendencias.
  • Diseñado para trading forex de estilo discrecional donde las órdenes se dimensionan en lotes y la protección se expresa en pips.

Lógica de trading

  1. Calcular el oscilador Acceleration/Deceleration como la diferencia entre el Awesome Oscillator y su SMA de 5 períodos.
  2. Suscribirse a un oscilador estocástico con períodos %K, %D y de suavizado configurables.
  3. Cuando se cierra una nueva vela, evaluar los dos valores de AC más recientes junto con el nivel estocástico:
    • Configuración larga: %K cruza por encima del nivel configurado, el AC actual es positivo y sube mientras el valor anterior era negativo.
    • Configuración corta: %K cruza por debajo del nivel, el AC actual es negativo y cae mientras el valor anterior era positivo.
  4. Con una señal válida se abren hasta cinco órdenes de mercado de igual tamaño. La primera capa refleja el EA original al lanzarse sin stop/objetivo, mientras que las capas restantes heredan el stop loss configurado y take profits escalonados.
  5. La gestión de salidas emula el comportamiento original del flag modok:
    • Cuando los trailing stops están deshabilitados, la estrategia solo ajusta stops a punto de equilibrio después de una salida rentable, y cerrará todas las capas si la combinación estocástico/AC se voltea contra la posición.
    • Con trailing stops habilitados, el stop sigue al precio una vez que el movimiento supera TrailingStop + TrailingStep, y la misma reversión de momentum cierra la pila.

Escalado de posición y objetivos

  • Las posiciones largas colocan cuatro capas adicionales con take profits en entry + TakeProfit * i para i = 1..4. Los cortos lo reflejan por debajo del precio.
  • Los stop losses (cuando están configurados) se adjuntan a cada capa excepto la primera, exactamente como el script MT5.
  • Los take profits parciales actualizan el flag interno para que la siguiente campaña comience inmediatamente en estado "modok = true", desbloqueando la protección de punto de equilibrio para la capa inicial.

Gestión de riesgos

  • StopLossPips y TakeProfitPips se definen en pips. La estrategia los convierte usando el tamaño de tick del instrumento y la precisión de dígitos (5 o 3 pares decimales cuentan como pips fraccionales).
  • TrailingStopPips = 0 desactiva la lógica de trailing y habilita solo el ajuste de punto de equilibrio después de un take profit. Cualquier valor positivo activa el bloque de trailing descrito anteriormente.
  • Todas las salidas se ejecutan con órdenes de mercado cuando el rango de la vela cruza los niveles de stop o objetivo almacenados, coincidiendo con el comportamiento del experto original que dependía de órdenes protectoras del lado del broker.

Parámetros

Nombre Descripción Valor predeterminado
OrderVolume Tamaño de lote por capa. 0.1
StopLossPips Distancia para órdenes stop protectoras (pips). 100
TakeProfitPips Distancia base entre take profits en capas (pips). 50
TrailingStopPips Distancia de trailing stop en pips (0 deshabilita trailing). 50
TrailingStepPips Distancia adicional antes de avanzar el trailing stop. 5
KPeriod Lookback del %K estocástico. 5
DPeriod Suavizado del %D estocástico. 3
SmoothingPeriod Suavizado final aplicado al %K. 3
StochasticLevel Umbral que separa regímenes alcistas/bajistas. 50
CandleType Serie de velas fuente para cálculos. Marco temporal 4h

Notas de implementación

  • Las señales, actualizaciones de trailing y salidas protectoras se procesan en velas completadas para mantenerse consistente con el EA que se activa en nuevas barras.
  • El indicador AC se reproduce vinculando el Awesome Oscillator y restando su SMA de 5 períodos; no se accede a buffers de indicadores de bajo nivel.
  • La conversión de pips se adapta automáticamente a símbolos forex de 4/5 dígitos y recurre a un valor predeterminado razonable cuando faltan metadatos del tamaño de tick.
  • La estrategia mantiene un registro interno de entradas en capas para que los take profits parciales y ajustes de stop coincidan con la lógica por posición de la versión MetaTrader.
  • Dado que StockSharp ejecuta salidas mediante órdenes de mercado, los trades se aplanan cuando el máximo/mínimo de la vela perfora los niveles almacenados de stop o objetivo en lugar de esperar activadores del lado del broker.
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>
/// OzFx strategy converted from MetaTrader 5 to the StockSharp high-level API.
/// Stacks multiple entries when the Acceleration/Deceleration oscillator and stochastic agree.
/// Implements layered targets and dynamic protection to mimic the expert advisor behaviour.
/// </summary>
public class OzFxAcceleratorStochasticStrategy : Strategy
{
	private readonly StrategyParam<int> _maxLayers;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<int> _smoothingPeriod;
	private readonly StrategyParam<decimal> _stochasticLevel;
	private readonly StrategyParam<DataType> _candleType;

	private AwesomeOscillator _ao = null!;
	private SimpleMovingAverage _aoSma = null!;
	private StochasticOscillator _stochastic = null!;

	private decimal? _lastAc;
	private bool _lastExitWasTakeProfit;
	private decimal _pipSize;
	private bool _pipInitialized;

	private readonly List<EntryInfo> _longEntries = new();
	private readonly List<EntryInfo> _shortEntries = new();

	/// <summary>
	/// Defines exit origin to replicate modok flag logic.
	/// </summary>
	private enum ExitReasons
	{
		Manual,
		TakeProfit,
		StopLoss,
	}

	/// <summary>
	/// Stores layered entry metadata (volume, price, protective levels).
	/// </summary>
	private sealed class EntryInfo
	{
		public decimal Volume;
		public decimal EntryPrice;
		public decimal? StopPrice;
		public decimal? TakeProfitPrice;
		public int Layer;
	}

	/// <summary>
	/// Maximum number of layered positions.
	/// </summary>
	public int MaxLayers
	{
		get => _maxLayers.Value;
		set => _maxLayers.Value = value;
	}

	/// <summary>
	/// Order volume for each layer.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

	/// <summary>
	/// Base take profit distance per layer measured in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

	/// <summary>
	/// Additional distance in pips before the trailing stop is advanced.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Main stochastic lookback period.
	/// </summary>
	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	/// <summary>
	/// %D smoothing length.
	/// </summary>
	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	/// <summary>
	/// Final smoothing applied to %K.
	/// </summary>
	public int SmoothingPeriod
	{
		get => _smoothingPeriod.Value;
		set => _smoothingPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic threshold separating bullish and bearish regimes.
	/// </summary>
	public decimal StochasticLevel
	{
		get => _stochasticLevel.Value;
		set => _stochasticLevel.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="OzFxAcceleratorStochasticStrategy"/>.
	/// </summary>
	public OzFxAcceleratorStochasticStrategy()
	{
		_maxLayers = Param(nameof(MaxLayers), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Layers", "Maximum number of layered positions", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume for each layer", "Trading");

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

		_takeProfitPips = Param(nameof(TakeProfitPips), 5m)
			.SetDisplay("Take Profit (pips)", "Base take profit increment in pips", "Risk");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetDisplay("Trailing Step (pips)", "Extra move required before advancing the trailing stop", "Risk");

		_kPeriod = Param(nameof(KPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("%K Period", "Stochastic lookback window", "Stochastic");

		_dPeriod = Param(nameof(DPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("%D Period", "Smoothing length for %D", "Stochastic");

		_smoothingPeriod = Param(nameof(SmoothingPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Slowing", "Final smoothing for %K", "Stochastic");

		_stochasticLevel = Param(nameof(StochasticLevel), 50m)
			.SetDisplay("Stochastic Level", "Threshold used to trigger signals", "Stochastic");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

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

		_longEntries.Clear();
		_shortEntries.Clear();
		_lastAc = null;
		_lastExitWasTakeProfit = false;
		_pipInitialized = false;
		_pipSize = 0m;
	}

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

		_ao = new AwesomeOscillator
		{
			ShortMa = { Length = 5 },
			LongMa = { Length = 34 },
		};

		_aoSma = new SMA
		{
			Length = 5,
		};

		_stochastic = new StochasticOscillator
		{
			K = { Length = KPeriod },
			D = { Length = DPeriod },
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_ao, _stochastic, ProcessCandle)
			.Start();

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

	/// <summary>
	/// Processes finished candles, updates indicators, and manages entries/exits.
	/// </summary>
	private void ProcessCandle(ICandleMessage candle, IIndicatorValue aoValue, IIndicatorValue stochValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!aoValue.IsFinal || !stochValue.IsFinal)
			return;

		var stoch = (StochasticOscillatorValue)stochValue;
		if (stoch.K is not decimal stochK)
			return;

		var ao = aoValue.GetValue<decimal>();
		var aoSmaValue = _aoSma.Process(new DecimalIndicatorValue(_aoSma, ao, candle.ServerTime) { IsFinal = true });
		if (!_aoSma.IsFormed)
			return;

		var ac = ao - aoSmaValue.GetValue<decimal>();
		var prevAcNullable = _lastAc;
		if (prevAcNullable is not decimal prevAc)
		{
			_lastAc = ac;
			return;
		}

		// indicators checked via BindEx
		if (!_ao.IsFormed || !_stochastic.IsFormed)
		{
			_lastAc = ac;
			return;
		}

		var pip = GetPipSize();
		var stopDistance = StopLossPips > 0m ? StopLossPips * pip : 0m;
		var takeDistance = TakeProfitPips > 0m ? TakeProfitPips * pip : 0m;
		var trailingStopDistance = TrailingStopPips > 0m ? TrailingStopPips * pip : 0m;
		var trailingStepDistance = TrailingStepPips > 0m ? TrailingStepPips * pip : 0m;
		var useTrailing = TrailingStopPips > 0m;

		TryEnterLong(candle, stochK, ac, prevAc, stopDistance, takeDistance);
		TryEnterShort(candle, stochK, ac, prevAc, stopDistance, takeDistance);

		ManageLongPositions(candle, stochK, ac, prevAc, trailingStopDistance, trailingStepDistance, useTrailing);
		ManageShortPositions(candle, stochK, ac, prevAc, trailingStopDistance, trailingStepDistance, useTrailing);

		_lastAc = ac;
	}

	/// <summary>
	/// Opens up to five long layers when momentum turns bullish.
	/// </summary>
	private void TryEnterLong(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal stopDistance, decimal takeDistance)
	{
		if (_longEntries.Count != 0 || _shortEntries.Count != 0)
			return;

		if (!(stochK > StochasticLevel && currentAc > previousAc))
			return;

		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		var entryPrice = candle.ClosePrice;

		// First layer mirrors the expert advisor: no stop or target until trailing engages.
		BuyMarket();
		_longEntries.Add(new EntryInfo
		{
			Volume = volume,
			EntryPrice = entryPrice,
			StopPrice = null,
			TakeProfitPrice = null,
			Layer = 0,
		});

		for (var i = 1; i < MaxLayers; i++)
		{
			BuyMarket();

			var stopPrice = stopDistance > 0m ? entryPrice - stopDistance : (decimal?)null;
			var takePrice = takeDistance > 0m ? entryPrice + takeDistance * i : (decimal?)null;

			_longEntries.Add(new EntryInfo
			{
				Volume = volume,
				EntryPrice = entryPrice,
				StopPrice = stopPrice,
				TakeProfitPrice = takePrice,
				Layer = i,
			});
		}
	}

	/// <summary>
	/// Opens up to five short layers when momentum turns bearish.
	/// </summary>
	private void TryEnterShort(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal stopDistance, decimal takeDistance)
	{
		if (_shortEntries.Count != 0 || _longEntries.Count != 0)
			return;

		if (!(stochK < StochasticLevel && currentAc < previousAc))
			return;

		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		var entryPrice = candle.ClosePrice;

		SellMarket();
		_shortEntries.Add(new EntryInfo
		{
			Volume = volume,
			EntryPrice = entryPrice,
			StopPrice = null,
			TakeProfitPrice = null,
			Layer = 0,
		});

		for (var i = 1; i < MaxLayers; i++)
		{
			SellMarket();

			var stopPrice = stopDistance > 0m ? entryPrice + stopDistance : (decimal?)null;
			var takePrice = takeDistance > 0m ? entryPrice - takeDistance * i : (decimal?)null;

			_shortEntries.Add(new EntryInfo
			{
				Volume = volume,
				EntryPrice = entryPrice,
				StopPrice = stopPrice,
				TakeProfitPrice = takePrice,
				Layer = i,
			});
		}
	}

	/// <summary>
	/// Manages open long layers including trailing logic and staged targets.
	/// </summary>
	private void ManageLongPositions(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal trailingStopDistance, decimal trailingStepDistance, bool useTrailing)
	{
		if (_longEntries.Count == 0)
			return;

		if (Position <= 0m)
		{
			_longEntries.Clear();
			return;
		}

		var closePrice = candle.ClosePrice;
		var highPrice = candle.HighPrice;
		var lowPrice = candle.LowPrice;

		var exitSignal = stochK < 50m && currentAc < previousAc;

		if (useTrailing)
		{
			if (exitSignal)
			{
				CloseAllLong(ExitReasons.Manual);
				return;
			}

			if (trailingStopDistance > 0m)
			{
				for (var i = 0; i < _longEntries.Count; i++)
				{
					var entry = _longEntries[i];
					var profit = closePrice - entry.EntryPrice;
					if (profit > trailingStopDistance + trailingStepDistance)
					{
						var newStop = closePrice - trailingStopDistance;
						if (entry.StopPrice is not decimal existing || newStop > existing)
							entry.StopPrice = newStop;
					}
				}
			}
		}
		else if (_lastExitWasTakeProfit)
		{
			if (exitSignal)
			{
				CloseAllLong(ExitReasons.Manual);
				return;
			}

			for (var i = 0; i < _longEntries.Count; i++)
			{
				var entry = _longEntries[i];
				if (entry.StopPrice is null && closePrice > entry.EntryPrice)
					entry.StopPrice = entry.EntryPrice;
			}
		}

		for (var i = 0; i < _longEntries.Count; i++)
		{
			var entry = _longEntries[i];
			if (entry.StopPrice is decimal stopPrice && lowPrice <= stopPrice)
			{
				CloseAllLong(ExitReasons.StopLoss);
				return;
			}
		}

		var anyTakeProfit = false;
		for (var i = _longEntries.Count - 1; i >= 0; i--)
		{
			var entry = _longEntries[i];
			if (entry.TakeProfitPrice is decimal takePrice && highPrice >= takePrice)
			{
				SellMarket();
				_longEntries.RemoveAt(i);
				anyTakeProfit = true;
			}
		}

		if (anyTakeProfit)
			_lastExitWasTakeProfit = true;
	}

	/// <summary>
	/// Manages open short layers including trailing logic and staged targets.
	/// </summary>
	private void ManageShortPositions(ICandleMessage candle, decimal stochK, decimal currentAc, decimal previousAc, decimal trailingStopDistance, decimal trailingStepDistance, bool useTrailing)
	{
		if (_shortEntries.Count == 0)
			return;

		if (Position >= 0m)
		{
			_shortEntries.Clear();
			return;
		}

		var closePrice = candle.ClosePrice;
		var highPrice = candle.HighPrice;
		var lowPrice = candle.LowPrice;

		var exitSignal = stochK > 50m && currentAc > previousAc;

		if (useTrailing)
		{
			if (exitSignal)
			{
				CloseAllShort(ExitReasons.Manual);
				return;
			}

			if (trailingStopDistance > 0m)
			{
				for (var i = 0; i < _shortEntries.Count; i++)
				{
					var entry = _shortEntries[i];
					var profit = entry.EntryPrice - closePrice;
					if (profit > trailingStopDistance + trailingStepDistance)
					{
						var newStop = closePrice + trailingStopDistance;
						if (entry.StopPrice is not decimal existing || newStop < existing)
							entry.StopPrice = newStop;
					}
				}
			}
		}
		else if (_lastExitWasTakeProfit)
		{
			if (exitSignal)
			{
				CloseAllShort(ExitReasons.Manual);
				return;
			}

			for (var i = 0; i < _shortEntries.Count; i++)
			{
				var entry = _shortEntries[i];
				if (entry.StopPrice is null && closePrice < entry.EntryPrice)
					entry.StopPrice = entry.EntryPrice;
			}
		}

		for (var i = 0; i < _shortEntries.Count; i++)
		{
			var entry = _shortEntries[i];
			if (entry.StopPrice is decimal stopPrice && highPrice >= stopPrice)
			{
				CloseAllShort(ExitReasons.StopLoss);
				return;
			}
		}

		var anyTakeProfit = false;
		for (var i = _shortEntries.Count - 1; i >= 0; i--)
		{
			var entry = _shortEntries[i];
			if (entry.TakeProfitPrice is decimal takePrice && lowPrice <= takePrice)
			{
				BuyMarket();
				_shortEntries.RemoveAt(i);
				anyTakeProfit = true;
			}
		}

		if (anyTakeProfit)
			_lastExitWasTakeProfit = true;
	}

	/// <summary>
	/// Closes all long layers and updates the modok-like flag.
	/// </summary>
	private void CloseAllLong(ExitReasons reason)
	{
		var volume = 0m;
		for (var i = 0; i < _longEntries.Count; i++)
			volume += _longEntries[i].Volume;

		if (volume > 0m && Position > 0m)
			SellMarket();

		_longEntries.Clear();

		if (reason == ExitReasons.TakeProfit)
			_lastExitWasTakeProfit = true;
		else if (reason == ExitReasons.StopLoss)
			_lastExitWasTakeProfit = false;
	}

	/// <summary>
	/// Closes all short layers and updates the modok-like flag.
	/// </summary>
	private void CloseAllShort(ExitReasons reason)
	{
		var volume = 0m;
		for (var i = 0; i < _shortEntries.Count; i++)
			volume += _shortEntries[i].Volume;

		if (volume > 0m && Position < 0m)
			BuyMarket();

		_shortEntries.Clear();

		if (reason == ExitReasons.TakeProfit)
			_lastExitWasTakeProfit = true;
		else if (reason == ExitReasons.StopLoss)
			_lastExitWasTakeProfit = false;
	}

	/// <summary>
	/// Calculates pip value based on the security tick size and decimal digits.
	/// </summary>
	private decimal GetPipSize()
	{
		if (_pipInitialized)
			return _pipSize;

		var security = Security;
		var step = security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 0.0001m;

		var decimals = security?.Decimals ?? 0;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;

		_pipSize = step * adjust;
		if (_pipSize <= 0m)
			_pipSize = step;

		if (_pipSize <= 0m)
			_pipSize = 0.0001m;

		_pipInitialized = true;
		return _pipSize;
	}
}