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:
- 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.
- 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; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class hans_indicator_cloud_system_strategy(Strategy):
_PERIOD1_START = 4
_PERIOD1_END = 8
_PERIOD2_END = 12
def __init__(self):
super(hans_indicator_cloud_system_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle type", "Primary timeframe analysed by the strategy", "General")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal bar", "Historical bar index inspected for colour changes", "Signals")
self._local_time_zone = self.Param("LocalTimeZone", 0) \
.SetDisplay("Local timezone", "Broker/server timezone (hours)", "Time zones")
self._destination_time_zone = self.Param("DestinationTimeZone", 4) \
.SetDisplay("Destination timezone", "Target timezone for Hans ranges (hours)", "Time zones")
self._pips_for_entry = self.Param("PipsForEntry", 300.0) \
.SetDisplay("Breakout buffer", "Extra price steps added above/below the session ranges", "Indicator")
self._cooldown_bars = self.Param("CooldownBars", 48) \
.SetDisplay("Cooldown bars", "Bars to wait after a close or entry", "Trading")
self._buy_pos_open = self.Param("BuyPosOpen", True) \
.SetDisplay("Enable long entries", "Allow opening new long positions", "Trading")
self._sell_pos_open = self.Param("SellPosOpen", True) \
.SetDisplay("Enable short entries", "Allow opening new short positions", "Trading")
self._buy_pos_close = self.Param("BuyPosClose", True) \
.SetDisplay("Enable long exits", "Allow closing existing longs", "Trading")
self._sell_pos_close = self.Param("SellPosClose", True) \
.SetDisplay("Enable short exits", "Allow closing existing shorts", "Trading")
self._color_history = []
self._current_day_date = None
self._p1_high = None
self._p1_low = None
self._p1_seen = False
self._p1_closed = False
self._p2_high = None
self._p2_low = None
self._p2_seen = False
self._p2_closed = False
self._time_shift_hours = 0
self._cooldown_left = 0
@property
def CandleType(self):
return self._candle_type.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def LocalTimeZone(self):
return self._local_time_zone.Value
@property
def DestinationTimeZone(self):
return self._destination_time_zone.Value
@property
def PipsForEntry(self):
return self._pips_for_entry.Value
@property
def CooldownBars(self):
return self._cooldown_bars.Value
@property
def BuyPosOpen(self):
return self._buy_pos_open.Value
@property
def SellPosOpen(self):
return self._sell_pos_open.Value
@property
def BuyPosClose(self):
return self._buy_pos_close.Value
@property
def SellPosClose(self):
return self._sell_pos_close.Value
def OnReseted(self):
super(hans_indicator_cloud_system_strategy, self).OnReseted()
self._color_history = []
self._current_day_date = None
self._p1_high = None
self._p1_low = None
self._p1_seen = False
self._p1_closed = False
self._p2_high = None
self._p2_low = None
self._p2_seen = False
self._p2_closed = False
self._cooldown_left = 0
def OnStarted2(self, time):
super(hans_indicator_cloud_system_strategy, self).OnStarted2(time)
self._time_shift_hours = self.DestinationTimeZone - self.LocalTimeZone
self._current_day_date = None
self._color_history = []
self._cooldown_left = 0
self._p1_high = None
self._p1_low = None
self._p1_seen = False
self._p1_closed = False
self._p2_high = None
self._p2_low = None
self._p2_seen = False
self._p2_closed = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
color = self._calculate_color(candle)
self._color_history.append(color)
if self._cooldown_left > 0:
self._cooldown_left -= 1
max_history = max(5, self.SignalBar + 3)
if len(self._color_history) > max_history:
self._color_history.pop(0)
target_index = len(self._color_history) - 1 - self.SignalBar
if target_index <= 0:
return
col0 = self._color_history[target_index]
col1 = self._color_history[target_index - 1]
bullish_breakout = col1 == 0 or col1 == 1
bearish_breakout = col1 == 3 or col1 == 4
should_close_short = self.SellPosClose and bullish_breakout
should_open_long = self.BuyPosOpen and bullish_breakout and col0 != 0 and col0 != 1
should_close_long = self.BuyPosClose and bearish_breakout
should_open_short = self.SellPosOpen and bearish_breakout and col0 != 3 and col0 != 4
if should_close_long and self.Position > 0:
self.SellMarket()
self._cooldown_left = self.CooldownBars
return
if should_close_short and self.Position < 0:
self.BuyMarket()
self._cooldown_left = self.CooldownBars
return
if self._cooldown_left == 0 and should_open_long and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_left = self.CooldownBars
elif self._cooldown_left == 0 and should_open_short and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_left = self.CooldownBars
def _calculate_color(self, candle):
shifted_hour = candle.OpenTime.Hour + self._time_shift_hours
shifted_date = candle.OpenTime.Date
if shifted_hour >= 24:
shifted_hour -= 24
shifted_date = shifted_date.AddDays(1)
elif shifted_hour < 0:
shifted_hour += 24
shifted_date = shifted_date.AddDays(-1)
if self._current_day_date is None or self._current_day_date != shifted_date:
self._current_day_date = shifted_date
self._p1_high = None
self._p1_low = None
self._p1_seen = False
self._p1_closed = False
self._p2_high = None
self._p2_low = None
self._p2_seen = False
self._p2_closed = False
self._update_session_extremes(candle, shifted_hour)
zone = self._get_active_zone()
if zone is None:
return 2
upper, lower = zone
close = float(candle.ClosePrice)
open_p = float(candle.OpenPrice)
if close > upper:
return 0 if close >= open_p else 1
if close < lower:
return 4 if close <= open_p else 3
return 2
def _update_session_extremes(self, candle, local_hour):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if local_hour >= self._PERIOD1_START and local_hour < self._PERIOD1_END:
self._p1_seen = True
self._p1_high = max(self._p1_high, high) if self._p1_high is not None else high
self._p1_low = min(self._p1_low, low) if self._p1_low is not None else low
elif local_hour >= self._PERIOD1_END and local_hour < self._PERIOD2_END:
if not self._p1_closed and self._p1_seen:
self._p1_closed = True
self._p2_seen = True
self._p2_high = max(self._p2_high, high) if self._p2_high is not None else high
self._p2_low = min(self._p2_low, low) if self._p2_low is not None else low
else:
if not self._p1_closed and self._p1_seen and local_hour >= self._PERIOD1_END:
self._p1_closed = True
if not self._p2_closed and self._p2_seen and local_hour >= self._PERIOD2_END:
self._p2_closed = True
if local_hour >= self._PERIOD2_END and self._p2_seen:
self._p2_closed = True
def _get_active_zone(self):
entry_offset = self._get_entry_offset()
if self._p2_closed and self._p2_high is not None and self._p2_low is not None:
return (self._p2_high + entry_offset, self._p2_low - entry_offset)
if self._p1_closed and self._p1_high is not None and self._p1_low is not None:
return (self._p1_high + entry_offset, self._p1_low - entry_offset)
return None
def _get_entry_offset(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if step <= 0.0:
step = 1.0
return float(self.PipsForEntry) * step
def CreateClone(self):
return hans_indicator_cloud_system_strategy()