Ver en GitHub

Estrategia de Trading Nocturno en Rango Plano

La Estrategia de Trading Nocturno en Rango Plano reproduce el asesor experto MQL5 clásico que busca rangos nocturnos ajustados en velas H1 de EURUSD. Se enfoca en la hora que rodea el cambio del día de trading, esperando que el precio regrese hacia los bordes de un canal de consolidación estrecho y apostando a la continuación de un rompimiento. La versión StockSharp mantiene las ideas originales intactas mientras depende de suscripciones de velas de alto nivel, vínculos de indicadores y objetos de parámetros para mejor configurabilidad.

Descripción general

  • Mercado y marco temporal: Diseñado para EURUSD en el marco temporal H1, pero puede usarse cualquier instrumento con un paso de precio claramente definido.
  • Ventana de sesión: Las entradas están permitidas solo durante una ventana de dos horas que comienza en el OpenHour configurado y termina en OpenHour + 1 (hora de la bolsa).
  • Filtro de rango: El tramo alto-bajo de las últimas tres velas completadas debe permanecer entre DiffMinPips y DiffMaxPips (convertidos a unidades de precio).
  • Sesgo: Solo largo o solo corto dependiendo de dónde se sitúa el último cierre dentro del rango que califica.

Lógica de trading

  1. Calcular los límites del rango

    • La estrategia vincula a los indicadores incorporados Highest y Lowest (longitud = 3) para obtener el máximo más alto y el mínimo más bajo a lo largo de las últimas tres velas.
    • La distancia entre esas fronteras es el rango de trabajo usado para todas las comprobaciones posteriores.
  2. Condiciones de entrada

    • Configuración larga: Durante la sesión activa, si el precio de cierre está por encima del mínimo del rango pero aún dentro del cuarto inferior (lowest + range/4), la estrategia abre una posición larga con un stop protector inicial en lowest - range/3.
    • Configuración corta: Simétricamente, si el cierre está por debajo del máximo del rango pero aún dentro del cuarto superior (highest - range/4), se abre una posición corta con un stop en highest + range/3.
  3. Gestión de salidas

    • Stop-Loss: Los stops se simulan internamente y activan una salida de mercado cuando la siguiente vela viola el umbral almacenado.
    • Take-Profit: Cuando TakeProfitPips > 0, se crea un nivel adicional fijo de take-profit (en pips) relativo al precio de entrada.
    • Trailing Stop: Cuando tanto TrailingStopPips como TrailingStepPips son positivos, el stop se ajusta solo después de que el precio avanza TrailingStop + TrailingStep pips a favor de la operación. Los ajustes posteriores requieren un progreso adicional de TrailingStepPips para reflejar el comportamiento de trailing escalonado original.
  4. Control de re-entrada

    • El algoritmo siempre espera a que la posición actual esté completamente cerrada antes de buscar una nueva señal, manteniendo el sistema plano entre operaciones como en el asesor experto de referencia.

Parámetros

Parámetro Descripción Predeterminado
CandleType Serie de velas a suscribir (por defecto H1). Velas de 1 hora
TakeProfitPips Distancia de take-profit opcional en pips. 50
TrailingStopPips Distancia entre precio y trailing stop en pips (0 desactiva trailing). 15
TrailingStepPips Pips adicionales requeridos antes de cada actualización del trailing stop. 5
DiffMinPips Rango mínimo permitido de tres velas (pips). 18
DiffMaxPips Rango máximo permitido de tres velas (pips). 28
OpenHour Hora de inicio de la sesión en hora de la bolsa (entradas permitidas hasta OpenHour + 1). 0

Indicadores

  • Highest(Length = 3) para monitorear la parte superior del rango reciente.
  • Lowest(Length = 3) para monitorear la parte inferior del rango reciente.

Notas de implementación

  • La conversión de pips se adapta automáticamente a instrumentos con 3 o 5 decimales multiplicando el paso de precio reportado por 10, exactamente como la implementación original de MQ5.
  • Dado que StockSharp opera en velas completadas en este ejemplo, las condiciones de entrada intra-vela se aproximan usando el precio de cierre. Esto mantiene la lógica determinista mientras permanece fiel al intento del código fuente.
  • Todos los parámetros de riesgo están expuestos mediante objetos StrategyParam<T>, haciéndolos visibles en la UI y listos para optimización o experimentos por lotes.
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>
/// Night session flat trading strategy that enters near range extremes.
/// </summary>
public class NightFlatTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _rangeLength;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _diffMinPips;
	private readonly StrategyParam<decimal> _diffMaxPips;
	private readonly StrategyParam<int> _openHour;

	private Highest _highest = null!;
	private Lowest _lowest = null!;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

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

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal DiffMinPips
	{
		get => _diffMinPips.Value;
		set => _diffMinPips.Value = value;
	}

	public decimal DiffMaxPips
	{
		get => _diffMaxPips.Value;
		set => _diffMaxPips.Value = value;
	}

	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Number of candles used to form the overnight range.
	/// </summary>
	public int RangeLength
	{
		get => _rangeLength.Value;
		set => _rangeLength.Value = value;
	}

	public NightFlatTradeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for the setup", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Step (pips)", "Extra advance required to shift the trailing stop", "Risk");

		_diffMinPips = Param(nameof(DiffMinPips), 18m)
			.SetGreaterThanZero()
			.SetDisplay("Min Range (pips)", "Minimum three-candle range in pips", "Setup");

		_diffMaxPips = Param(nameof(DiffMaxPips), 28m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range (pips)", "Maximum three-candle range in pips", "Setup");

		_openHour = Param(nameof(OpenHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Open Hour", "Hour (exchange time) when entries become active", "Schedule");

		_rangeLength = Param(nameof(RangeLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Range Length", "Number of candles composing the range", "Setup");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_highest = null!;
		_lowest = null!;
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_highest = new Highest { Length = RangeLength };
		_lowest = new Lowest { Length = RangeLength };

		var priceStep = Security?.PriceStep ?? 0m;
		var decimals = Security?.Decimals;

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

		_pipSize = priceStep;

		if (decimals.HasValue && (decimals.Value == 3 || decimals.Value == 5))
			_pipSize *= 10m;

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

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

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

		// Manage active trades before scanning for new setups.
		HandleExistingPosition(candle);

		if (Position != 0m)
			return;

		if (_highest == null || _lowest == null)
			return;

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var diff = highestValue - lowestValue;
		if (diff <= 0m)
			return;

		var quarter = diff / 4m;
		var closePrice = candle.ClosePrice;

		if (closePrice > lowestValue && closePrice <= lowestValue + quarter)
		{
			BuyMarket();
			_entryPrice = closePrice;
			_stopPrice = lowestValue - diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice + ToPrice(TakeProfitPips) : null;
			return;
		}

		if (closePrice < highestValue && closePrice >= highestValue - quarter)
		{
			SellMarket();
			_entryPrice = closePrice;
			_stopPrice = highestValue + diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice - ToPrice(TakeProfitPips) : null;
		}
	}

	private void HandleExistingPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			UpdateTrailingForLong(candle);

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		else if (Position < 0m)
		{
			UpdateTrailingForShort(candle);

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = candle.HighPrice - _entryPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.HighPrice - trailingDistance;

		if (newStop <= _stopPrice.Value || newStop - _stopPrice.Value < stepDistance)
			return;

		// Raise the stop only after price travels an additional step distance.
		_stopPrice = newStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = _entryPrice - candle.LowPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.LowPrice + trailingDistance;

		if (newStop >= _stopPrice.Value || _stopPrice.Value - newStop < stepDistance)
			return;

		// Lower the stop only after price moves the additional step distance in favor of the trade.
		_stopPrice = newStop;
	}

	private decimal ToPrice(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var pip = _pipSize > 0m ? _pipSize : 0.0001m;
		return pips * pip;
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}
}