Ver no GitHub

Estratégia Volatility HFT EA

Esta estratégia porta o consultor especialista Volatility HFT EA do MetaTrader 5 para a API de alto nível do StockSharp. Reproduz a lógica original que compra quando o preço de fechamento salta bem acima de uma média móvil simples rápida e aguarda um pullback para essa média. A geração de ordens, o gerenciamento de indicadores e as saídas protetoras seguem as diretrizes de AGENTS.md enquanto mantêm o comportamento do script MQL.

Como funciona

  1. Configuração do indicador – uma única média móvil simples (comprimento padrão: 5) é calculada no período de trabalho especificado por CandleType.
  2. Detecção de nova barra – o processamento ocorre apenas uma vez que uma vela está concluída (CandleStates.Finished), espelhando as verificações IsNewBar no EA.
  3. Requisito de aquecimento – a estratégia aguarda 60 velas concluídas antes de avaliar trades, correspondendo ao guarda Bars < 60 usado no MQL.
  4. Filtro de entrada – uma configuração longa aparece quando o último fechamento está pelo menos MaDifferencePips acima da SMA (diferença convertida em preço usando o tamanho do pip do instrumento) e o valor da SMA é mais alto do que era duas barras atrás. O EA original usava val[0] < -0.0015 e MA_Val1[0] > MA_Val1[2]; este port verifica as mesmas condições sem armazenar manualmente os arrays.
  5. Uma posição por vez – apenas trades longos são suportados porque a ramificação de venda foi comentada no arquivo fonte. Um novo sinal é ignorado enquanto há uma posição aberta.

Gestão de risco

  • Stop-loss – stop de proteção opcional expresso em pips. O código deriva o tamanho do pip de Security.PriceStep, multiplicando por 10 quando o instrumento tem 3 ou 5 casas decimais, reproduzindo o escalonamento _Digits do MetaTrader.
  • Take-profit – o alvo de saída está ancorado ao valor da SMA capturado na entrada, espelhando a chamada mrequest.tp = MA_Val1[0];. A estratégia fecha a posição quando a mínima da vela toca o nível SMA armazenado, emulando uma ordem limitada na média.

Parâmetros

Parâmetro Descrição
OrderVolume Volume usado para cada ordem de mercado.
FastMaLength Período da média móvil simples rápida (padrão 5).
StopLossPips Distância do stop-loss em pips; definir como 0 para desabilitar.
MaDifferencePips Distância mínima (em pips) entre o fechamento e a SMA necessária para uma entrada longa.
CandleType Período usado para subscrição de velas e cálculos do indicador.

MinimumBars é uma constante interna fixa igual a 60, refletindo o requisito do EA para histórico suficiente.

Uso

  1. Vincule a estratégia a um ativo e selecione o CandleType desejado (por exemplo, barras de 1 minuto para comportamento de alta frequência).
  2. Ajuste FastMaLength, MaDifferencePips e StopLossPips para se adequar à volatilidade do instrumento. As entradas baseadas em pip são automaticamente convertidas usando o tamanho de pip detectado, então os mesmos padrões funcionam em símbolos FX de 4 e 5 dígitos.
  3. Configure OrderVolume para corresponder às suas regras de dimensionamento do portfólio. A estratégia envia apenas ordens de mercado e não avolumará posições.
  4. Inicie a estratégia. Subscreverá as velas escolhidas, construirá a SMA, aguardará 60 barras de aquecimento e então avaliará entradas em cada vela concluída.
  5. Monitore o gerenciamento de trades: as saídas são acionadas pelo toque da SMA ou pelo preço do stop-loss derivado na entrada.

Notas e diferenças com o EA original

  • A versão MQL solicitava o tamanho mínimo de lote via SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); aqui o volume é exposto como parâmetro para manter a estratégia flexível entre corretoras e classes de ativos.
  • As condições de venda são omitidas porque estavam comentadas em Volatility_HFT_EA.mq5. O comportamento portanto corresponde ao script publicado, que apenas executava a ramificação longa.
  • O gerenciamento de take-profit usa mínimas de velas para detectar um toque da média móvil em vez de registrar uma ordem limitada, garantindo que a mesma intenção funcione de forma confiável dentro do fluxo de trabalho do StockSharp.
  • O gerenciamento manual de arrays (CopyRates, CopyBuffer, ArraySetAsSeries) é substituído por vínculos de indicadores do StockSharp. Isso reduz o código repetitivo enquanto preserva os limiares originais e as comparações de inclinação.
  • Todos os cálculos permanecem baseados em velas; a estratégia não consulta buffers de indicadores com GetValue, mantendo-se em conformidade com as diretrizes do repositório.
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;
	}
}