Стратегия Hans Indicator Cloud System
Обзор
Стратегия является портом советника MQL5 Exp_Hans_Indicator_Cloud_System на высокоуровневый API StockSharp. Она восстанавливает
индикаторные «облака» Hans, которые делят торговый день на две сессии и отслеживают пробои выше или ниже сформированных диапазонов.
Обработка ведётся по настраиваемым свечам (по умолчанию M30), анализируются только закрытые свечи, а сигналы исполняются на
следующем баре после смены цвета, полностью повторяя задержку оригинального кода.
Воссоздание индикатора Hans
Индикатор сдвигает временные метки из часового пояса брокера (LocalTimeZone) в целевой часовой пояс (DestinationTimeZone).
Порт StockSharp применяет тот же сдвиг, а затем разбивает сутки на две сессии:
- Сессия 1 (04:00–08:00 целевого времени) – фиксируются максимумы и минимумы всех свечей внутри интервала. После завершения окна диапазон считается сформированным.
- Сессия 2 (08:00–12:00 целевого времени) – аналогичный процесс для второй сессии. После её окончания диапазон второй сессии заменяет первый до конца дня.
К верхней и нижней границе активного диапазона добавляется буфер PipsForEntry, измеряемый в шагах цены. Цвета индикатора
воспроизводятся следующим образом:
0– закрытие выше верхней границы, свеча бычья.1– закрытие выше верхней границы, свеча медвежья.3– закрытие ниже нижней границы, свеча бычья.4– закрытие ниже нижней границы, свеча медвежья.2– пробоя нет (нейтральное состояние).
Полученные значения сохраняются, что имитирует обращения CopyBuffer из оригинального советника.
Торговая логика
- Ведётся скользящая история цветовых кодов. Анализируются
SignalBarбаров назад (по умолчанию 1) плюс ещё один бар, что соответствует вызовуCopyBuffer(..., SignalBar, 2, ...)в MQL5. - Открыть long: более старый бар (
SignalBar + 1) имеет цвет0или1, а ближайший бар (SignalBar) уже не имеет этого цвета. Перед покупкой закрываются все короткие позиции. - Открыть short: более старый бар имеет цвет
3или4, а ближайший бар уже не равен3/4. Перед продажей закрываются все длинные позиции. - Закрыть long: если более старый бар окрашен в
3или4, а закрытие длинных включено. - Закрыть short: если более старый бар окрашен в
0или1, а закрытие коротких включено.
Выходы обрабатываются раньше входов, как и в функциях TradeAlgorithms.mqh, поэтому противоположные позиции закрываются до подачи
новых заявок.
Параметры
- Тип свечей (
CandleType) – таймфрейм обрабатываемых свечей. - Сигнальный бар (
SignalBar) – количество закрытых свечей назад для оценки смены цвета. - Часовой пояс брокера (
LocalTimeZone) – смещение сервера в часах. - Целевой часовой пояс (
DestinationTimeZone) – смещение, задающее границы сессий. - Буфер пробоя (
PipsForEntry) – число шагов цены, добавляемых к верхней/нижней границе диапазона. - Включить открытия/закрытия long (
BuyPosOpen,BuyPosClose) – переключатели управления длинными позициями. - Включить открытия/закрытия short (
SellPosOpen,SellPosClose) – переключатели управления короткими позициями. - Объём сделки (
TradeVolume) – размер ордера для каждой новой позиции, дополнительно синхронизируется соStrategy.Volumeпри запуске.
Примечания
- Реализации доступны на C# и Python.
- Функции манименеджмента из
TradeAlgorithms.mqh(режимы маржи, динамический объём, стоп-лоссы/тейк-профиты) заменены на фиксированный объём и явные правила выхода. - Если инструмент не предоставляет
PriceStep, буфер пробоя трактуется как абсолютное изменение цены – это лучшая возможная аппроксимация без информации о минимальном шаге.
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()