Ver no GitHub

Estratégia UmnickTrader

Sistema adaptativo de reversão à média convertido do consultor especialista MQL5 original UmnickTrader. A estratégia trabalha com uma única posição por vez, alternando entre viés comprado e vendido dependendo do resultado da operação anterior. Ela avalia o movimento de preço usando a média dos preços de abertura, máxima, mínima e fechamento, e só toma ação quando essa média se deslocou pelo menos a distância StopBase configurada.

Lógica principal

  • Para cada vela terminada, o preço médio (O + H + L + C) / 4 é calculado.
  • Os sinais são processados apenas quando a diferença absoluta entre a média atual e a média processada anteriormente é maior ou igual a StopBase. Isso imita o comportamento original do EA de esperar por um movimento suficientemente grande.
  • Quando não há posição aberta, a estratégia calcula distâncias adaptativas de take-profit e stop-loss usando dois buffers circulares que armazenam as oito excursões de ganho e perda mais recentes.
  • Após uma operação lucrativa, a excursão favorável máxima observada enquanto a posição estava aberta é salva no buffer de lucro (menos um preenchimento de spread), enquanto o buffer de perda recebe StopBase + 7 * Spread.
  • Após uma operação perdedora, o buffer de lucro é redefinido para StopBase - 3 * Spread, o buffer de perda é atualizado com o drawdown registrado mais um preenchimento de spread, e a direção de trading é invertida para que a próxima configuração opere o lado oposto.

Gestão de operações

  • A distância padrão tanto para o take-profit quanto para o stop-loss é StopBase. Se o buffer acumulado de lucro ou perda exceder StopBase / 2, suas respectivas médias substituem a distância padrão para ampliar ou ajustar adaptativamente os níveis de saída.
  • Ordens de mercado são usadas para entradas. Os preços esperados de take-profit e stop-loss são armazenados e gerenciados pela própria estratégia, portanto as posições são fechadas quando as máximas ou mínimas das velas tocam os níveis correspondentes.
  • Enquanto uma posição está ativa, o movimento favorável mais alto e o drawdown mais profundo são rastreados usando extremos intrabar. Essas estatísticas alimentam os buffers quando a operação fecha.
  • Apenas uma posição pode existir a qualquer momento. Um novo sinal é ignorado se a operação anterior não foi concluída.

Parâmetros

  • StopBase – distância base (em unidades de preço) necessária para tratar um movimento como significativo e a distância TP/SL padrão. Padrão: 0.017.
  • TradeVolume – volume para ordens de mercado. Padrão: 0.1.
  • Spread – compensação de spread aplicada ao atualizar os buffers adaptativos. Padrão: 0.0005.
  • CandleType – assinatura de velas usada para avaliar médias. Padrão: TimeSpan.FromMinutes(5).TimeFrame().

Classificação e filtros

  • Direção: Ambos (mas nunca simultaneamente).
  • Estilo: Swing adaptativo / contratendência.
  • Indicadores: Média de preço, buffers de excursão personalizados.
  • Stops: Stop-loss e take-profit dinâmico gerenciado pela estratégia.
  • Complexidade: Intermediário – combina buffers com estado com dimensionamento adaptativo de saída.
  • Período: Configurável via CandleType.
  • Sazonalidade / Filtros de notícias: Não utilizados.
  • Gestão de risco: O tamanho da posição é fixo pelo TradeVolume; as distâncias de saída se adaptam com base no desempenho recente.
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;

namespace StockSharp.Samples.Strategies;



/// <summary>
/// Adaptive mean-reversion strategy converted from the UmnickTrader MQL5 expert advisor.
/// </summary>
public class UmnickTraderStrategy : Strategy
{
	// Number of trade results stored for adaptive calculations.
	private readonly StrategyParam<int> _bufferLength;

	private readonly StrategyParam<decimal> _stopBase;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _spread;
	private readonly StrategyParam<int> _entryCooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	// Adaptive buffers storing profit and loss distances observed recently.
	private decimal[] _profitBuffer = Array.Empty<decimal>();
	private decimal[] _lossBuffer = Array.Empty<decimal>();

	// Rolling state for signal detection and risk metrics.
	private decimal _lastAveragePrice;
	private decimal _entryPrice;
	private decimal _takeProfitPrice;
	private decimal _stopLossPrice;
	private decimal _maxProfit;
	private decimal _drawdown;
	private decimal _lastTradeProfit;

	private int _currentIndex;
	private int _currentDirection = 1;
	private int _cooldownRemaining;

	private bool _positionActive;
	private bool _isLongPosition;
	private bool _positionJustClosed;

	/// <summary>
	/// Number of trade results stored for adaptive calculations.
	/// </summary>
	public int BufferLength
	{
		get => _bufferLength.Value;
		set
		{
			if (_bufferLength.Value == value)
				return;

			_bufferLength.Value = value;
			ResizeBuffers();
		}
	}

	public decimal StopBase
	{
		get => _stopBase.Value;
		set => _stopBase.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	public decimal Spread
	{
		get => _spread.Value;
		set => _spread.Value = value;
	}

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

	public int EntryCooldownBars
	{
		get => _entryCooldownBars.Value;
		set => _entryCooldownBars.Value = value;
	}

	public UmnickTraderStrategy()
	{
		_bufferLength = Param(nameof(BufferLength), 8)
		.SetGreaterThanZero()
		.SetDisplay("Buffer Length", "Number of trade results stored for adaptive calculations.", "Parameters")
		
		.SetOptimize(4, 32, 1);

		ResizeBuffers();

		_stopBase = Param(nameof(StopBase), 0.017m)
			.SetDisplay("Base Stop Distance", "Minimum average price move required to trigger evaluation.", "Parameters")
			
			.SetOptimize(0.005m, 0.05m, 0.005m);

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Trade Volume", "Order volume for each position.", "Parameters")
			
			.SetOptimize(0.05m, 1m, 0.05m);

		_spread = Param(nameof(Spread), 0.0005m)
			.SetDisplay("Spread Padding", "Spread compensation used when updating adaptive buffers.", "Parameters")
			
			.SetOptimize(0.0001m, 0.002m, 0.0001m);

		_entryCooldownBars = Param(nameof(EntryCooldownBars), 6)
			.SetGreaterThanZero()
			.SetDisplay("Entry Cooldown", "Bars to wait after a position is closed.", "Parameters");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Source candle series.", "General");
	}

	private void ResizeBuffers()
	{
		var length = BufferLength;

		_profitBuffer = new decimal[length];
		_lossBuffer = new decimal[length];
		_currentIndex = 0;
	}

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

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

		_lastAveragePrice = 0m;
		_entryPrice = 0m;
		_takeProfitPrice = 0m;
		_stopLossPrice = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
		_lastTradeProfit = 0m;
		_currentIndex = 0;
		_currentDirection = 1;
		_cooldownRemaining = 0;
		_positionActive = false;
		_isLongPosition = false;
		_positionJustClosed = false;

		Array.Clear(_profitBuffer, 0, _profitBuffer.Length);
		Array.Clear(_lossBuffer, 0, _lossBuffer.Length);
	}

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

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

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

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

		// Update metrics for an active position before generating new signals.
		UpdateOpenPosition(candle);

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		// Average of OHLC replicates the MQL5 price smoothing logic.
		var averagePrice = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
		if (!ShouldProcessAverage(averagePrice))
			return;

		if (Position != 0)
			return;

		var limitDistance = StopBase;
		var stopDistance = StopBase;

		decimal sumProfit = 0m;
		decimal sumLoss = 0m;
		var bufferLength = _profitBuffer.Length;

		if (bufferLength == 0)
			return;

		for (var i = 0; i < bufferLength; i++)
		{
			sumProfit += _profitBuffer[i];
			sumLoss += _lossBuffer[i];
		}

		// Recalculate adaptive take-profit and stop-loss distances.
		if (sumProfit > StopBase / 2m)
			limitDistance = sumProfit / bufferLength;

		if (sumLoss > StopBase / 2m)
			stopDistance = sumLoss / bufferLength;

		if (_positionJustClosed)
		{
			_positionJustClosed = false;

			// Store the most recent excursion metrics.
			if (_lastTradeProfit > 0m)
			{
				_profitBuffer[_currentIndex] = _maxProfit - Spread * 3m;
				_lossBuffer[_currentIndex] = StopBase + Spread * 7m;
			}
			else
			{
				_profitBuffer[_currentIndex] = StopBase - Spread * 3m;
				_lossBuffer[_currentIndex] = _drawdown + Spread * 7m;
				_currentDirection = -_currentDirection;
			}

			_currentIndex++;
			if (_currentIndex >= bufferLength)
				_currentIndex = 0;

			_cooldownRemaining = EntryCooldownBars;
			return;
		}

		if (limitDistance <= 0m || stopDistance <= 0m)
			return;

		if (_cooldownRemaining > 0)
			return;

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

		// Enter in the current direction using market orders.
		if (_currentDirection > 0)
			OpenLong(candle.ClosePrice, limitDistance, stopDistance, volume);
		else
			OpenShort(candle.ClosePrice, limitDistance, stopDistance, volume);
	}

	private bool ShouldProcessAverage(decimal averagePrice)
	{
		if (_lastAveragePrice == 0m)
		{
			_lastAveragePrice = averagePrice;
			return true;
		}

		var difference = Math.Abs(averagePrice - _lastAveragePrice);
		if (difference >= StopBase)
		{
			_lastAveragePrice = averagePrice;
			return true;
		}

		return false;
	}

	private void UpdateOpenPosition(ICandleMessage candle)
	{
		if (!_positionActive)
			return;

		// Track intrabar extremes to measure maximum favorable and adverse excursions.
		if (_isLongPosition)
		{
			var profitMove = candle.HighPrice - _entryPrice;
			if (profitMove > _maxProfit)
				_maxProfit = profitMove;

			var lossMove = _entryPrice - candle.LowPrice;
			if (lossMove > _drawdown)
				_drawdown = lossMove;

			if (candle.LowPrice <= _stopLossPrice)
			{
				CloseCurrentPosition(_stopLossPrice);
				return;
			}

			if (candle.HighPrice >= _takeProfitPrice)
			{
				CloseCurrentPosition(_takeProfitPrice);
				return;
			}
		}
		else
		{
			var profitMove = _entryPrice - candle.LowPrice;
			if (profitMove > _maxProfit)
				_maxProfit = profitMove;

			var lossMove = candle.HighPrice - _entryPrice;
			if (lossMove > _drawdown)
				_drawdown = lossMove;

			if (candle.HighPrice >= _stopLossPrice)
			{
				CloseCurrentPosition(_stopLossPrice);
				return;
			}

			if (candle.LowPrice <= _takeProfitPrice)
			{
				CloseCurrentPosition(_takeProfitPrice);
				return;
			}
		}
	}

	private void CloseCurrentPosition(decimal exitPrice)
	{
		// Close the position and record realized profit for buffer updates.
		var profit = _isLongPosition ? exitPrice - _entryPrice : _entryPrice - exitPrice;
		_positionActive = false;
		_isLongPosition = false;
		_entryPrice = 0m;
		_takeProfitPrice = 0m;
		_stopLossPrice = 0m;

		_lastTradeProfit = profit;
		_positionJustClosed = true;

		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();
	}

	private void OpenLong(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
	{
		BuyMarket(volume);

		// Store trade parameters for managing exits on subsequent candles.
		_entryPrice = price;
		_takeProfitPrice = price + limitDistance;
		_stopLossPrice = price - stopDistance;
		_positionActive = true;
		_isLongPosition = true;
		_lastTradeProfit = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
	}

	private void OpenShort(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
	{
		SellMarket(volume);

		// Store trade parameters for managing exits on subsequent candles.
		_entryPrice = price;
		_takeProfitPrice = price - limitDistance;
		_stopLossPrice = price + stopDistance;
		_positionActive = true;
		_isLongPosition = false;
		_lastTradeProfit = 0m;
		_maxProfit = 0m;
		_drawdown = 0m;
	}
}