Estratégia Hans Indicator Sistema de Nuvem
Visão geral
Esta estratégia porta o expert advisor MQL5 Exp_Hans_Indicator_Cloud_System para a API de alto nível do StockSharp. Reproduz os
intervalos de "nuvem" do indicador Hans que dividem cada dia de trading em duas sessões de referência e opera quando o indicador reporta um
rompimento acima ou abaixo desses intervalos dinâmicos. A implementação consome uma série de velas configurável (padrão: M30), processa
apenas velas finalizadas, e espelha a lógica de execução retardada do script original atuando na próxima barra após uma mudança
de cor.
Recriação do indicador Hans
O indicador original desloca todos os timestamps do fuso horário do broker (LocalTimeZone) para um fuso horário alvo (DestinationTimeZone).
O port do StockSharp aplica o mesmo offset antes de dividir cada dia em duas sessões:
- Sessão 1 (04:00–08:00 hora alvo) – a estratégia registra a máxima mais alta e a mínima mais baixa de todas as velas que caem dentro
desta janela. Uma vez que a janela termina, a zona é considerada completa.
- Sessão 2 (08:00–12:00 hora alvo) – o processo se repete para a segunda janela. Quando esta sessão termina, seus valores alto/baixo
substituem a primeira zona pelo resto do dia.
Um buffer configurável (PipsForEntry) expresso em passos de preço é adicionado acima da máxima e abaixo da mínima da zona ativa. O
mapa de cores do indicador é reproduzido da seguinte forma:
0 – o fechamento está acima da zona superior e o corpo da vela é de alta.
1 – o fechamento está acima da zona superior e o corpo da vela é de baixa.
3 – o fechamento está abaixo da zona inferior e o corpo da vela é de alta.
4 – o fechamento está abaixo da zona inferior e o corpo da vela é de baixa.
2 – sem rompimento (estado neutro).
Esses valores são armazenados para emular as pesquisas de CopyBuffer realizadas pelo expert MQL5.
Lógica de trading
- A estratégia mantém um histórico deslizante de códigos de cores e olha
SignalBar barras (padrão 1) mais uma barra extra, correspondendo à
chamada CopyBuffer(..., SignalBar, 2, ...) da fonte.
- Abrir comprado: a barra mais antiga (
SignalBar + 1) reporta cor 0 ou 1 e a barra mais recente (SignalBar) não está colorida
0/1. Qualquer exposição vendida existente é fechada antes de abrir um novo comprado de TradeVolume unidades.
- Abrir vendido: a barra mais antiga reporta cor
3 ou 4 e a barra mais recente não está colorida 3/4. Qualquer exposição comprada
existente é nivelada primeiro e então um novo vendido é aberto.
- Fechar comprado: sempre que a barra mais antiga está colorida
3 ou 4 e as saídas compradas estão habilitadas.
- Fechar vendido: sempre que a barra mais antiga está colorida
0 ou 1 e as saídas vendidas estão habilitadas.
As saídas são processadas antes das entradas exatamente como as funções auxiliares dentro de TradeAlgorithms.mqh, garantindo que posições opostas
sejam fechadas antes de emitir novas ordens.
Parâmetros
- Tipo de vela (
CandleType): período das velas processadas.
- Barra de sinal (
SignalBar): quantas velas finalizadas atrás inspecionar para uma mudança de cor.
- Fuso horário local (
LocalTimeZone): fuso horário do broker/servidor em horas.
- Fuso horário de destino (
DestinationTimeZone): fuso horário alvo que define as janelas de sessão.
- Buffer de rompimento (
PipsForEntry): número de passos de preço adicionados acima/abaixo do intervalo de sessão detectado.
- Habilitar entradas/saídas compradas (
BuyPosOpen, BuyPosClose): alternadores para gerenciar posições compradas.
- Habilitar entradas/saídas vendidas (
SellPosOpen, SellPosClose): alternadores para gerenciar posições vendidas.
- Volume de trading (
TradeVolume): tamanho da ordem usada para cada nova posição; também sincronizado com Strategy.Volume no início.
Notas
- A tradução Python é intencionalmente omitida conforme solicitado.
- Os auxiliares de gestão de dinheiro de
TradeAlgorithms.mqh (modos de margem, dimensionamento dinâmico de posição, colocação de stop-loss/take-profit)
são simplificados para um volume de trading fixo e regras de saída explícitas.
- Quando o ativo não expõe
PriceStep, o buffer de rompimento é interpretado como unidades de preço absolutas, correspondendo à melhor
aproximação disponível sem informações sobre o tamanho do 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; }
}
}
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()