在 GitHub 上查看

Hans Indicator Cloud System 策略

概述

该策略将 MQL5 智能交易系统 Exp_Hans_Indicator_Cloud_System 迁移到 StockSharp 的高级 API。它重新构建 Hans 指标的 “云区”范围,把交易日划分为两个参考时段,并在价格向上或向下突破时段区间时执行交易。策略订阅可配置的 K 线 (默认 M30),只处理收盘 K 线,并在颜色发生变化后的下一根 K 线上下单,以保持与原始脚本相同的延迟逻辑。

Hans 指标的复现方式

原指标会把时间从经纪商时区 (LocalTimeZone) 转换到目标时区 (DestinationTimeZone)。移植版本使用相同的偏移, 然后将每天拆分为两个时段:

  1. 时段 1(目标时间 04:00–08:00) – 统计该区间内所有 K 线的最高价和最低价,区间结束后得到第一组范围。
  2. 时段 2(目标时间 08:00–12:00) – 对第二个区间重复上述过程。该时段完成后,其高低点将在当日余下时间内 取代第一时段的范围。

在活动范围的高点和低点上分别加上以价格步长计量的缓冲 PipsForEntry。指标颜色映射如下:

  • 0 – 收盘价突破上沿且 K 线实体为阳线。
  • 1 – 收盘价突破上沿且 K 线实体为阴线。
  • 3 – 收盘价跌破下沿且 K 线实体为阳线。
  • 4 – 收盘价跌破下沿且 K 线实体为阴线。
  • 2 – 未发生突破(中性状态)。

这些颜色值被保存下来,以模拟原策略中对 CopyBuffer 的访问。

交易逻辑

  • 策略维护一个颜色历史序列,回看 SignalBar 根(默认 1)以及再往前的一根 K 线,对应 MQL5 中的 CopyBuffer(..., SignalBar, 2, ...) 调用。
  • 开多头:更早的那根 K 线(SignalBar + 1)颜色为 01,而最近的 K 线(SignalBar)已经不是 0/1。 在建仓前会平掉所有空头仓位,并以 TradeVolume 的固定数量买入。
  • 开空头:更早的 K 线颜色为 34,最近的 K 线不再是 3/4。在做空之前会平掉所有多头仓位。
  • 平多头:当更早的 K 线颜色为 34 且允许平多时触发。
  • 平空头:当更早的 K 线颜色为 01 且允许平空时触发。

平仓逻辑优先于开仓逻辑,与 TradeAlgorithms.mqh 中的辅助函数一致,确保在发送新委托前关闭对向仓位。

参数

  • K 线类型 (CandleType):用于计算的时间框架。
  • 信号 K 线 (SignalBar):回看多少根已收盘 K 线来判断颜色变化。
  • 本地时区 (LocalTimeZone):经纪商/服务器的小时偏移。
  • 目标时区 (DestinationTimeZone):定义两个交易时段的小时偏移。
  • 突破缓冲 (PipsForEntry):在区间上下沿额外添加的价格步数。
  • 允许多头开/平仓 (BuyPosOpen, BuyPosClose):管理多头仓位的开仓与离场开关。
  • 允许空头开/平仓 (SellPosOpen, SellPosClose):管理空头仓位的开仓与离场开关。
  • 交易量 (TradeVolume):每次新仓位使用的下单数量,并在启动时同步到 Strategy.Volume

说明

  • 根据要求未提供 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; }
	}
}