Ver en GitHub

Estrategia Volatility HFT EA

Esta estrategia porta el asesor experto Volatility HFT EA de MetaTrader 5 a la API de alto nivel de StockSharp. Reproduce la lógica original que compra cuando el precio de cierre salta muy por encima de una media móvil simple rápida y espera un retroceso hacia esa media. La generación de órdenes, la gestión de indicadores y las salidas protectoras siguen las directrices de AGENTS.md mientras mantienen el comportamiento del script MQL.

Cómo funciona

  1. Configuración del indicador – se calcula una única media móvil simple (longitud por defecto: 5) en el marco temporal de trabajo especificado por CandleType.
  2. Detección de nueva barra – el procesamiento ocurre solo una vez que una vela está terminada (CandleStates.Finished), imitando las verificaciones IsNewBar en el EA.
  3. Requisito de calentamiento – la estrategia espera 60 velas completadas antes de evaluar operaciones, coincidiendo con la guardia Bars < 60 usada en MQL.
  4. Filtro de entrada – aparece una configuración larga cuando el último cierre está al menos MaDifferencePips por encima de la SMA (diferencia convertida a precio usando el tamaño del pip del instrumento) y el valor de la SMA es más alto que hace dos barras. El EA original usaba val[0] < -0.0015 y MA_Val1[0] > MA_Val1[2]; este port verifica las mismas condiciones sin almacenar manualmente los arrays.
  5. Una posición a la vez – solo se soportan operaciones largas porque la rama de venta fue comentada en el archivo fuente. Una nueva señal se ignora mientras hay una posición abierta.

Gestión de riesgo

  • Stop-loss – stop de protección opcional expresado en pips. El código deriva el tamaño del pip de Security.PriceStep, multiplicando por 10 cuando el instrumento tiene 3 o 5 decimales, reproduciendo el escalado _Digits de MetaTrader.
  • Take-profit – el objetivo de salida está anclado al valor de la SMA capturado en la entrada, imitando la llamada mrequest.tp = MA_Val1[0];. La estrategia cierra la posición cuando el mínimo de la vela toca el nivel SMA almacenado, emulando una orden limitada en la media.

Parámetros

Parámetro Descripción
OrderVolume Volumen usado para cada orden de mercado.
FastMaLength Período de la media móvil simple rápida (por defecto 5).
StopLossPips Distancia del stop-loss en pips; establecer en 0 para deshabilitar.
MaDifferencePips Distancia mínima (en pips) entre el cierre y la SMA requerida para una entrada larga.
CandleType Marco temporal usado para la suscripción de velas y cálculos del indicador.

MinimumBars es una constante interna fija igual a 60, reflejando el requisito del EA para suficiente historial.

Uso

  1. Adjunte la estrategia a un instrumento y seleccione el CandleType deseado (por ejemplo, barras de 1 minuto para comportamiento de alta frecuencia).
  2. Ajuste FastMaLength, MaDifferencePips y StopLossPips para adaptarse a la volatilidad del instrumento. Las entradas basadas en pips se convierten automáticamente usando el tamaño de pip detectado, por lo que los mismos valores por defecto funcionan en símbolos de FX de 4 y 5 dígitos.
  3. Configure OrderVolume para que coincida con sus reglas de dimensionamiento del portafolio. La estrategia solo envía órdenes de mercado y no piramidará posiciones.
  4. Inicie la estrategia. Se suscribirá a las velas elegidas, construirá la SMA, esperará 60 barras de calentamiento y luego evaluará entradas en cada vela completada.
  5. Monitoree la gestión de operaciones: las salidas se desencadenan por el toque de la SMA o por el precio del stop-loss derivado en la entrada.

Notas y diferencias con el EA original

  • La versión MQL solicitaba el tamaño mínimo de lote a través de SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); aquí el volumen se expone como parámetro para mantener la estrategia flexible entre brokers y clases de activos.
  • Las condiciones de venta se omiten porque estaban comentadas en Volatility_HFT_EA.mq5. El comportamiento por tanto coincide con el script publicado, que solo ejecutaba la rama larga.
  • El manejo del take-profit usa mínimos de velas para detectar un toque de la media móvil en lugar de registrar una orden limitada, asegurando que la misma intención funcione de manera confiable dentro del flujo de trabajo de StockSharp.
  • La gestión manual de arrays (CopyRates, CopyBuffer, ArraySetAsSeries) se reemplaza por enlaces de indicadores de StockSharp. Esto reduce el código repetitivo mientras preserva los umbrales originales y las comparaciones de pendiente.
  • Todos los cálculos siguen siendo basados en velas; la estrategia no consulta hacia atrás los buffers de indicadores con GetValue, manteniéndose en conformidad con las directrices del repositorio.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Mean-reversion expert advisor that buys volatility spikes when price stretches far above a fast moving average.
/// Converted from the MetaTrader 5 "Volatility HFT EA" script.
/// </summary>
public class VolatilityHftEaStrategy : Strategy
{
	private readonly StrategyParam<int> _minimumBars;

	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _fastMaLength;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _maDifferencePips;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _fastMa = null!;

	private decimal _pipSize = 1m;
	private decimal? _previousSma;
	private decimal? _smaTwoBarsAgo;
	private int _processedCandles;
	private int _cooldownLeft;

	private decimal _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	public VolatilityHftEaStrategy()
	{
		_minimumBars = Param(nameof(MinimumBars), 60)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Bars", "Minimum completed candles before signal evaluation", "Signal");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume applied to market orders", "Trading");

		_fastMaLength = Param(nameof(FastMaLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Length", "Period of the fast simple moving average", "Signal");

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

		_maDifferencePips = Param(nameof(MaDifferencePips), 15m)
			.SetGreaterThanZero()
			.SetDisplay("MA Difference (pips)", "Minimum distance between price and the moving average", "Signal");

		_cooldownBars = Param(nameof(CooldownBars), 24)
			.SetNotNegative()
			.SetDisplay("Cooldown Bars", "Bars to wait after entry or exit", "Signal");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for signal detection", "General");
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int FastMaLength
	{
		get => _fastMaLength.Value;
		set => _fastMaLength.Value = value;
	}

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal MaDifferencePips
	{
		get => _maDifferencePips.Value;
		set => _maDifferencePips.Value = value;
	}

	public int MinimumBars
	{
		get => _minimumBars.Value;
		set => _minimumBars.Value = value;
	}

	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_fastMa = null!;
		_previousSma = null;
		_smaTwoBarsAgo = null;
		_processedCandles = 0;
		_cooldownLeft = 0;
		ResetPositionState();
	}

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

		_pipSize = CalculatePipSize();
		Volume = OrderVolume;

		_fastMa = new SMA
		{
			Length = FastMaLength
		};

		_previousSma = null;
		_smaTwoBarsAgo = null;
		_processedCandles = 0;
		_cooldownLeft = 0;
		ResetPositionState();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastMa, ProcessCandle)
			.Start();
	}

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

		ManageActivePosition(candle);

		if (_cooldownLeft > 0)
			_cooldownLeft--;

		if (!_fastMa.IsFormed)
		{
			UpdateSmaHistory(smaValue);
			_processedCandles++;
			return;
		}

		if (_processedCandles < MinimumBars)
		{
			UpdateSmaHistory(smaValue);
			_processedCandles++;
			return;
		}

		var threshold = Math.Max(MaDifferencePips, 10m) * _pipSize;

		if (_smaTwoBarsAgo.HasValue && _cooldownLeft == 0)
		{
			var distance = candle.ClosePrice - smaValue;
			var isBreakout = distance >= threshold;
			var isSlopePositive = _previousSma.HasValue && _previousSma.Value > _smaTwoBarsAgo.Value && smaValue > _previousSma.Value;
			var isBullishBar = candle.ClosePrice > candle.OpenPrice;

			if (isBreakout && isSlopePositive && isBullishBar && Position == 0)
			{
				EnterLong(candle, smaValue);
			}
		}

		UpdateSmaHistory(smaValue);
		_processedCandles++;
	}

	private void EnterLong(ICandleMessage candle, decimal smaValue)
	{
		// Strategy holds only one long position at a time.
		if (Position != 0)
			return;

		Volume = OrderVolume;
		BuyMarket();
		_cooldownLeft = CooldownBars;

		_entryPrice = candle.ClosePrice;

		var stopDistance = StopLossPips * _pipSize;
		_stopLossPrice = stopDistance > 0m ? _entryPrice - stopDistance : null;
		_takeProfitPrice = smaValue;
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetPositionState();
			return;
		}

		var exitVolume = Math.Abs(Position);

		if (Position > 0)
		{
			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				SellMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
				return;
			}

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
			}
		}
		else if (Position < 0)
		{
			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				BuyMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
				return;
			}

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(exitVolume);
				_cooldownLeft = CooldownBars;
				ResetPositionState();
			}
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	private void UpdateSmaHistory(decimal smaValue)
	{
		_smaTwoBarsAgo = _previousSma;
		_previousSma = smaValue;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
			return 1m;

		var decimals = GetDecimalPlaces(step);

		return decimals is 3 or 5
			? step * 10m
			: step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var text = Math.Abs(value).ToString(CultureInfo.InvariantCulture);
		var separatorIndex = text.IndexOf('.');

		return separatorIndex < 0 ? 0 : text.Length - separatorIndex - 1;
	}
}