Ver en GitHub

Estrategia UmnickTrader

Sistema adaptativo de reversión a la media convertido del asesor experto MQL5 original UmnickTrader. La estrategia trabaja con una única posición a la vez, alternando entre sesgo largo y corto dependiendo del resultado de la operación anterior. Evalúa el movimiento de precio usando el promedio de los precios de apertura, máximo, mínimo y cierre, y solo toma acción una vez que ese promedio se ha desplazado al menos la distancia StopBase configurada.

Lógica principal

  • Para cada vela terminada se calcula el precio promedio (O + H + L + C) / 4.
  • Las señales se procesan solo cuando la diferencia absoluta entre el promedio actual y el promedio procesado anteriormente es mayor o igual a StopBase. Esto imita el comportamiento del EA original de esperar un movimiento suficientemente grande.
  • Cuando no hay posición abierta la estrategia calcula distancias adaptativas de take-profit y stop-loss usando dos buffers circulares que almacenan las ocho excursiones de ganancia y pérdida más recientes.
  • Después de una operación rentable, la excursión favorable máxima observada mientras la posición estaba abierta se guarda en el buffer de ganancia (menos un relleno de spread), mientras que el buffer de pérdida recibe StopBase + 7 * Spread.
  • Después de una operación perdedora, el buffer de ganancia se restablece a StopBase - 3 * Spread, el buffer de pérdida se actualiza con el drawdown registrado más un relleno de spread, y la dirección de trading se invierte para que la próxima configuración opere el lado opuesto.

Gestión de operaciones

  • La distancia predeterminada tanto para el take-profit como para el stop-loss es StopBase. Si el buffer de ganancia o pérdida acumulado supera StopBase / 2, sus respectivos promedios reemplazan la distancia predeterminada para ampliar o ajustar adaptativamente los niveles de salida.
  • Se usan órdenes de mercado para las entradas. Los precios esperados de take-profit y stop-loss se almacenan y gestionan por la propia estrategia, por lo que las posiciones se cierran cuando los máximos o mínimos de la vela tocan los niveles correspondientes.
  • Mientras una posición está activa, el movimiento favorable más alto y el drawdown más profundo se rastrean usando extremos intrabar. Estas estadísticas alimentan los buffers cuando la operación se cierra.
  • Solo puede existir una posición en cualquier momento. Una nueva señal se ignora si la operación anterior no se ha completado.

Parámetros

  • StopBase – distancia base (en unidades de precio) requerida para tratar un movimiento como significativo y la distancia TP/SL predeterminada. Predeterminado: 0.017.
  • TradeVolume – volumen para órdenes de mercado. Predeterminado: 0.1.
  • Spread – compensación de spread aplicada al actualizar los buffers adaptativos. Predeterminado: 0.0005.
  • CandleType – suscripción de velas utilizada para evaluar promedios. Predeterminado: TimeSpan.FromMinutes(5).TimeFrame().

Clasificación y filtros

  • Dirección: Ambos (pero nunca simultáneamente).
  • Estilo: Swing adaptativo / contratendencia.
  • Indicadores: Promedio de precio, buffers de excursión personalizados.
  • Stops: Stop-loss y take-profit dinámico gestionado por la estrategia.
  • Complejidad: Intermedio – combina buffers con estado con dimensionamiento adaptativo de salida.
  • Marco temporal: Configurable mediante CandleType.
  • Estacionalidad / Filtros de noticias: No se usan.
  • Gestión de riesgos: El tamaño de la posición está fijado por TradeVolume; las distancias de salida se adaptan según el rendimiento reciente.
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;
	}
}