Auf GitHub ansehen

Hans Indicator Cloud System Strategy

Overview

This strategy ports the MQL5 expert advisor Exp_Hans_Indicator_Cloud_System to the StockSharp high-level API. It reproduces the Hans indicator "cloud" ranges that divide each trading day into two reference sessions and trades when the indicator reports a breakout above or below those dynamic ranges. The implementation consumes a configurable candle series (default: M30), processes only finished candles, and mirrors the delayed execution logic from the original script by acting on the next bar after a colour change.

Hans indicator recreation

The original indicator shifts all timestamps from the broker timezone (LocalTimeZone) to a target timezone (DestinationTimeZone). The StockSharp port applies the same offset before splitting every day into two sessions:

  1. Session 1 (04:00–08:00 target time) – the strategy records the highest high and lowest low of all candles that fall inside this window. Once the window ends the zone is considered complete.
  2. Session 2 (08:00–12:00 target time) – the process repeats for the second window. When this session finishes its high/low values supersede the first zone for the rest of the day.

A configurable buffer (PipsForEntry) expressed in price steps is added above the high and below the low of the active zone. The indicator colour map is reproduced as follows:

  • 0 – close is above the upper zone and the candle body is bullish.
  • 1 – close is above the upper zone and the candle body is bearish.
  • 3 – close is below the lower zone and the candle body is bullish.
  • 4 – close is below the lower zone and the candle body is bearish.
  • 2 – no breakout (neutral state).

These values are stored to emulate the CopyBuffer look-ups performed by the MQL5 expert.

Trading logic

  • The strategy keeps a rolling history of colour codes and looks back SignalBar bars (default 1) plus one extra bar, matching the CopyBuffer(..., SignalBar, 2, ...) call from the source.
  • Open long: the older bar (SignalBar + 1) reports colour 0 or 1 and the more recent bar (SignalBar) is not coloured 0/1. Any existing short exposure is closed before opening a new long of TradeVolume units.
  • Open short: the older bar reports colour 3 or 4 and the more recent bar is not coloured 3/4. Any existing long exposure is flattened first and then a new short is opened.
  • Close long: whenever the older bar is coloured 3 or 4 and long exits are enabled.
  • Close short: whenever the older bar is coloured 0 or 1 and short exits are enabled.

Exits are processed before entries exactly like the helper functions inside TradeAlgorithms.mqh, ensuring that opposite positions are closed prior to issuing fresh orders.

Parameters

  • Candle type (CandleType): timeframe of the processed candles.
  • Signal bar (SignalBar): how many finished candles back to inspect for a colour change.
  • Local timezone (LocalTimeZone): broker/server timezone in hours.
  • Destination timezone (DestinationTimeZone): target timezone that defines the session windows.
  • Breakout buffer (PipsForEntry): number of price steps added above/below the detected session range.
  • Enable long entries/exits (BuyPosOpen, BuyPosClose): toggles for managing long positions.
  • Enable short entries/exits (SellPosOpen, SellPosClose): toggles for managing short positions.
  • Trade volume (TradeVolume): order size used for every new position; also synced with Strategy.Volume on start.

Notes

  • Python translation is intentionally omitted as requested.
  • The money-management helpers from TradeAlgorithms.mqh (margin modes, dynamic position sizing, stop-loss/ take-profit placement) are simplified to a fixed trade volume and explicit exit rules.
  • When the security does not expose PriceStep the breakout buffer is interpreted as absolute price units, matching the best approximation available without tick-size information.
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; }
	}
}