Ver en GitHub

Estrategia Hans Indicator Sistema de Nube

Descripción general

Esta estrategia porta el asesor experto MQL5 Exp_Hans_Indicator_Cloud_System a la API de alto nivel de StockSharp. Reproduce los rangos de "nube" del indicador Hans que dividen cada día de trading en dos sesiones de referencia y opera cuando el indicador reporta un breakout por encima o por debajo de esos rangos dinámicos. La implementación consume una serie de velas configurable (predeterminado: M30), procesa solo velas finalizadas, y refleja la lógica de ejecución retardada del script original actuando en la siguiente barra después de un cambio de color.

Recreación del indicador Hans

El indicador original desplaza todas las marcas de tiempo desde la zona horaria del broker (LocalTimeZone) a una zona horaria objetivo (DestinationTimeZone). El port de StockSharp aplica el mismo desplazamiento antes de dividir cada día en dos sesiones:

  1. Sesión 1 (04:00–08:00 hora objetivo) – la estrategia registra el máximo más alto y el mínimo más bajo de todas las velas que caen dentro de esta ventana. Una vez que la ventana termina, la zona se considera completa.
  2. Sesión 2 (08:00–12:00 hora objetivo) – el proceso se repite para la segunda ventana. Cuando esta sesión termina, sus valores alto/bajo supersedan la primera zona por el resto del día.

Un buffer configurable (PipsForEntry) expresado en pasos de precio se agrega por encima del máximo y por debajo del mínimo de la zona activa. El mapa de colores del indicador se reproduce de la siguiente manera:

  • 0 – el cierre está por encima de la zona superior y el cuerpo de la vela es alcista.
  • 1 – el cierre está por encima de la zona superior y el cuerpo de la vela es bajista.
  • 3 – el cierre está por debajo de la zona inferior y el cuerpo de la vela es alcista.
  • 4 – el cierre está por debajo de la zona inferior y el cuerpo de la vela es bajista.
  • 2 – sin breakout (estado neutral).

Estos valores se almacenan para emular las búsquedas de CopyBuffer realizadas por el experto MQL5.

Lógica de trading

  • La estrategia mantiene un historial rodante de códigos de color y mira atrás SignalBar barras (predeterminado 1) más una barra extra, coincidiendo con la llamada CopyBuffer(..., SignalBar, 2, ...) del fuente.
  • Abrir largo: la barra más antigua (SignalBar + 1) reporta color 0 o 1 y la barra más reciente (SignalBar) no está coloreada 0/1. Cualquier exposición corta existente se cierra antes de abrir un nuevo largo de TradeVolume unidades.
  • Abrir corto: la barra más antigua reporta color 3 o 4 y la barra más reciente no está coloreada 3/4. Cualquier exposición larga existente se aplana primero y luego se abre un nuevo corto.
  • Cerrar largo: cuando la barra más antigua está coloreada 3 o 4 y los cierres largos están habilitados.
  • Cerrar corto: cuando la barra más antigua está coloreada 0 o 1 y los cierres cortos están habilitados.

Las salidas se procesan antes que las entradas exactamente como las funciones auxiliares dentro de TradeAlgorithms.mqh, asegurando que las posiciones opuestas se cierren antes de emitir nuevas órdenes.

Parámetros

  • Tipo de vela (CandleType): marco temporal de las velas procesadas.
  • Barra de señal (SignalBar): cuántas velas finalizadas atrás inspeccionar para un cambio de color.
  • Zona horaria local (LocalTimeZone): zona horaria del broker/servidor en horas.
  • Zona horaria de destino (DestinationTimeZone): zona horaria objetivo que define las ventanas de sesión.
  • Buffer de breakout (PipsForEntry): número de pasos de precio agregados por encima/por debajo del rango de sesión detectado.
  • Habilitar entradas/salidas largas (BuyPosOpen, BuyPosClose): interruptores para gestionar posiciones largas.
  • Habilitar entradas/salidas cortas (SellPosOpen, SellPosClose): interruptores para gestionar posiciones cortas.
  • Volumen de trading (TradeVolume): tamaño de orden usado para cada nueva posición; también sincronizado con Strategy.Volume al inicio.

Notas

  • La traducción de Python se omite intencionalmente según lo solicitado.
  • Los auxiliares de gestión de dinero de TradeAlgorithms.mqh (modos de margen, dimensionamiento de posición dinámico, colocación de stop-loss/take-profit) se simplifican a un volumen de trading fijo y reglas de salida explícitas.
  • Cuando el valor no expone PriceStep, el buffer de breakout se interpreta como unidades de precio absolutas, coincidiendo con la mejor aproximación disponible sin información sobre el tamaño del tick.
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using StockSharp.Algo.Strategies;
using StockSharp.Messages;

public class HansIndicatorCloudSystemStrategy : Strategy
{
	private static readonly TimeSpan Period1Start = TimeSpan.FromHours(4);
	private static readonly TimeSpan Period1End = TimeSpan.FromHours(8);
	private static readonly TimeSpan Period2End = TimeSpan.FromHours(12);

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<int> _localTimeZone;
	private readonly StrategyParam<int> _destinationTimeZone;
	private readonly StrategyParam<decimal> _pipsForEntry;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<bool> _buyPosOpen;
	private readonly StrategyParam<bool> _sellPosOpen;
	private readonly StrategyParam<bool> _buyPosClose;
	private readonly StrategyParam<bool> _sellPosClose;
	private readonly StrategyParam<decimal> _tradeVolume;

	private readonly List<int> _colorHistory = new();
	private DayState _currentDay;
	private TimeSpan _timeShift;
	private int _cooldownLeft;

	public HansIndicatorCloudSystemStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe analysed by the strategy.", "General");

		_signalBar = Param(nameof(SignalBar), 1)
			.SetNotNegative()
			.SetDisplay("Signal bar", "Historical bar index inspected for colour changes.", "Signals");

		_localTimeZone = Param(nameof(LocalTimeZone), 0)
			.SetDisplay("Local timezone", "Broker/server timezone used by the raw candles (hours).", "Time zones");

		_destinationTimeZone = Param(nameof(DestinationTimeZone), 4)
			.SetDisplay("Destination timezone", "Target timezone for Hans ranges (hours).", "Time zones");

		_pipsForEntry = Param(nameof(PipsForEntry), 300m)
			.SetNotNegative()
			.SetDisplay("Breakout buffer", "Extra price steps added above/below the session ranges.", "Indicator");

		_cooldownBars = Param(nameof(CooldownBars), 48)
			.SetNotNegative()
			.SetDisplay("Cooldown bars", "Bars to wait after a close or entry before another entry.", "Trading");

		_buyPosOpen = Param(nameof(BuyPosOpen), true)
			.SetDisplay("Enable long entries", "Allow opening new long positions when an upper breakout appears.", "Trading");

		_sellPosOpen = Param(nameof(SellPosOpen), true)
			.SetDisplay("Enable short entries", "Allow opening new short positions when a lower breakout appears.", "Trading");

		_buyPosClose = Param(nameof(BuyPosClose), true)
			.SetDisplay("Enable long exits", "Allow closing existing longs on a bearish breakout.", "Trading");

		_sellPosClose = Param(nameof(SellPosClose), true)
			.SetDisplay("Enable short exits", "Allow closing existing shorts on a bullish breakout.", "Trading");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade volume", "Order size used for every new position.", "Trading");
	}

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

	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	public int LocalTimeZone
	{
		get => _localTimeZone.Value;
		set => _localTimeZone.Value = value;
	}

	public int DestinationTimeZone
	{
		get => _destinationTimeZone.Value;
		set => _destinationTimeZone.Value = value;
	}

	public decimal PipsForEntry
	{
		get => _pipsForEntry.Value;
		set => _pipsForEntry.Value = value;
	}

	public bool BuyPosOpen
	{
		get => _buyPosOpen.Value;
		set => _buyPosOpen.Value = value;
	}

	public bool SellPosOpen
	{
		get => _sellPosOpen.Value;
		set => _sellPosOpen.Value = value;
	}

	public bool BuyPosClose
	{
		get => _buyPosClose.Value;
		set => _buyPosClose.Value = value;
	}

	public bool SellPosClose
	{
		get => _sellPosClose.Value;
		set => _sellPosClose.Value = value;
	}

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_timeShift = default;
		_currentDay = null;
		_colorHistory.Clear();
		_cooldownLeft = 0;
	}

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

		Volume = TradeVolume; // Keep the default Strategy volume aligned with the configured trade size.

		_timeShift = TimeSpan.FromHours(DestinationTimeZone - LocalTimeZone);
		_currentDay = null;
		_colorHistory.Clear();
		_cooldownLeft = 0;

		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;

		var color = CalculateColor(candle);
		_colorHistory.Add(color); // Store Hans indicator colour codes for historical lookups.
		if (_cooldownLeft > 0)
			_cooldownLeft--;

		var maxHistory = Math.Max(5, SignalBar + 3);
		if (_colorHistory.Count > maxHistory)
			_colorHistory.RemoveAt(0); // Keep just enough history for signal evaluation.

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		// Align the history pointer with the requested SignalBar offset.
		var targetIndex = _colorHistory.Count - 1 - SignalBar;
		if (targetIndex <= 0)
			return;

		// Evaluate the Hans indicator codes for breakout conditions.
		var col0 = _colorHistory[targetIndex];
		var col1 = _colorHistory[targetIndex - 1];

		var bullishBreakout = col1 == 0 || col1 == 1;
		var bearishBreakout = col1 == 3 || col1 == 4;

		// Prepare trading decisions that mimic TradeAlgorithms.mqh helper flags.
		var shouldCloseShort = SellPosClose && bullishBreakout;
		var shouldOpenLong = BuyPosOpen && bullishBreakout && col0 != 0 && col0 != 1;
		var shouldCloseLong = BuyPosClose && bearishBreakout;
		var shouldOpenShort = SellPosOpen && bearishBreakout && col0 != 3 && col0 != 4;

		// Close existing long positions before handling new entries.
		if (shouldCloseLong && Position > 0)
		{
			var volume = Position;
			if (volume > 0)
				SellMarket(volume);

			_cooldownLeft = CooldownBars;
			return;
		}

		// Close existing short positions before handling new entries.
		if (shouldCloseShort && Position < 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0)
				BuyMarket(volume);

			_cooldownLeft = CooldownBars;
			return;
		}

		// Flatten any opposite exposure before opening a fresh long trade.
		if (_cooldownLeft == 0 && shouldOpenLong && Position <= 0 && TradeVolume > 0)
		{
			if (Position < 0)
			{
				var covering = Math.Abs(Position);
				if (covering > 0)
					BuyMarket(covering);
			}

			BuyMarket(TradeVolume);
			_cooldownLeft = CooldownBars;
		}

		// Flatten any opposite exposure before opening a fresh short trade.
		else if (_cooldownLeft == 0 && shouldOpenShort && Position >= 0 && TradeVolume > 0)
		{
			if (Position > 0)
			{
				var covering = Position;
				if (covering > 0)
					SellMarket(covering);
			}

			SellMarket(TradeVolume);
			_cooldownLeft = CooldownBars;
		}
	}

	private int CalculateColor(ICandleMessage candle)
	{
		var shiftedTime = candle.OpenTime + _timeShift;
		var day = shiftedTime.Date;

		// Build or reset the daily session state after applying the timezone shift.
		if (_currentDay == null || _currentDay.Date != day)
			_currentDay = new DayState(day);

		UpdateSessionExtremes(_currentDay, candle, shiftedTime.TimeOfDay);

		var zone = GetActiveZone(_currentDay);
		if (zone == null)
			return 2;

		var (upper, lower) = zone.Value;
		var close = candle.ClosePrice;
		var open = candle.OpenPrice;

		// The Hans indicator paints breakout candles with colour codes 0/1 (bullish) and 3/4 (bearish).
		if (close > upper)
			return close >= open ? 0 : 1;

		if (close < lower)
			return close <= open ? 4 : 3;

		return 2;
	}

	// Track the two Hans sessions (04:00-08:00 and 08:00-12:00 target time) and their high/low ranges.
	private void UpdateSessionExtremes(DayState dayState, ICandleMessage candle, TimeSpan localTime)
	{
		if (localTime >= Period1Start && localTime < Period1End)
		{
			// First session: update running high/low.
			dayState.Period1Seen = true;
			dayState.Period1High = dayState.Period1High.HasValue
				? Math.Max(dayState.Period1High.Value, candle.HighPrice)
				: candle.HighPrice;
			dayState.Period1Low = dayState.Period1Low.HasValue
				? Math.Min(dayState.Period1Low.Value, candle.LowPrice)
				: candle.LowPrice;
		}
		else if (localTime >= Period1End && localTime < Period2End)
		{
			// Second session: finalise the first zone and accumulate the second zone.
			if (!dayState.Period1Closed && dayState.Period1Seen)
				dayState.Period1Closed = true;

			dayState.Period2Seen = true;
			dayState.Period2High = dayState.Period2High.HasValue
				? Math.Max(dayState.Period2High.Value, candle.HighPrice)
				: candle.HighPrice;
			dayState.Period2Low = dayState.Period2Low.HasValue
				? Math.Min(dayState.Period2Low.Value, candle.LowPrice)
				: candle.LowPrice;
		}
		else
		{
			// After the monitored windows we just lock the zones if they received data.
			if (!dayState.Period1Closed && dayState.Period1Seen && localTime >= Period1End)
				dayState.Period1Closed = true;

			if (!dayState.Period2Closed && dayState.Period2Seen && localTime >= Period2End)
				dayState.Period2Closed = true;
		}

		if (localTime >= Period2End && dayState.Period2Seen)
			dayState.Period2Closed = true;
	}

	// Prefer the second session range when available, otherwise fall back to the first session.
	private (decimal upper, decimal lower)? GetActiveZone(DayState dayState)
	{
		var entryOffset = GetEntryOffset();
		if (dayState.Period2Closed && dayState.Period2High.HasValue && dayState.Period2Low.HasValue)
		{
			return (
				dayState.Period2High.Value + entryOffset,
				dayState.Period2Low.Value - entryOffset);
		}

		if (dayState.Period1Closed && dayState.Period1High.HasValue && dayState.Period1Low.HasValue)
		{
			return (
				dayState.Period1High.Value + entryOffset,
				dayState.Period1Low.Value - entryOffset);
		}

		return null;
	}

	// Convert the buffer measured in points into absolute price units.
	private decimal GetEntryOffset()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0)
			step = 1m;

		return PipsForEntry * step;
	}

	// Container for daily session statistics.
	private sealed class DayState
	{
		public DayState(DateTime date)
		{
			Date = date;
		}

		public DateTime Date { get; }

		public decimal? Period1High { get; set; }
		public decimal? Period1Low { get; set; }
		public bool Period1Seen { get; set; }
		public bool Period1Closed { get; set; }

		public decimal? Period2High { get; set; }
		public decimal? Period2Low { get; set; }
		public bool Period2Seen { get; set; }
		public bool Period2Closed { get; set; }
	}
}