GitHub で見る

Hans Indicator クラウドシステム戦略

概要

この戦略は MQL5 エキスパートアドバイザー Exp_Hans_Indicator_Cloud_System を StockSharp 高レベル API に移植します。各取引日を2つの参照セッションに分割する Hans インジケーターの「クラウド」レンジを再現し、インジケーターがそれらの動的レンジを上回るまたは下回るブレイクアウトを報告したときに取引します。実装は設定可能なローソク足シリーズ (デフォルト: M30) を消費し、完了したローソク足のみを処理し、色の変化後の次のバーで行動することにより元のスクリプトの遅延実行ロジックを反映します。

Hans インジケーターの再現

元のインジケーターはすべてのタイムスタンプをブローカーのタイムゾーン (LocalTimeZone) からターゲットのタイムゾーン (DestinationTimeZone) にシフトします。StockSharp ポートは各日を2つのセッションに分割する前に同じオフセットを適用します:

  1. セッション 1 (ターゲット時間 04:00–08:00) – 戦略はこのウィンドウ内に入るすべてのローソク足の最高値の高値と最低値の安値を記録します。ウィンドウが終了すると、ゾーンは完成したとみなされます。
  2. セッション 2 (ターゲット時間 08:00–12:00) – 2番目のウィンドウでプロセスが繰り返されます。このセッションが終了すると、その高値/安値がその日の残りの時間で最初のゾーンに取って代わります。

価格ステップで表現された設定可能なバッファ (PipsForEntry) がアクティブゾーンの高値の上と安値の下に追加されます。インジケーターのカラーマップは次のように再現されます:

  • 0 – クローズが上部ゾーンより上にあり、ローソク足のボディが強気。
  • 1 – クローズが上部ゾーンより上にあり、ローソク足のボディが弱気。
  • 3 – クローズが下部ゾーンより下にあり、ローソク足のボディが強気。
  • 4 – クローズが下部ゾーンより下にあり、ローソク足のボディが弱気。
  • 2 – ブレイクアウトなし (ニュートラル状態)。

これらの値は MQL5 エキスパートが実行する CopyBuffer の参照をエミュレートするために保存されます。

取引ロジック

  • 戦略はカラーコードのローリング履歴を維持し、ソースの CopyBuffer(..., SignalBar, 2, ...) 呼び出しと一致して SignalBar バー (デフォルト 1) プラス1つの追加バーを遡ります。
  • ロングを開く: 古いバー (SignalBar + 1) が色 0 または 1 を報告し、より新しいバー (SignalBar) が 0/1 で色付けされていない。TradeVolume 単位の新しいロングを開く前に、既存のショートエクスポージャーがクローズされます。
  • ショートを開く: 古いバーが色 3 または 4 を報告し、より新しいバーが 3/4 で色付けされていない。既存のロングエクスポージャーが最初にフラット化され、その後新しいショートが開かれます。
  • ロングをクローズ: 古いバーが 3 または 4 で色付けされており、ロング決済が有効な場合はいつでも。
  • ショートをクローズ: 古いバーが 0 または 1 で色付けされており、ショート決済が有効な場合はいつでも。

決済は TradeAlgorithms.mqh 内のヘルパー関数と全く同様にエントリーの前に処理され、新しい注文を出す前に反対のポジションがクローズされることを確認します。

パラメーター

  • ローソク足タイプ (CandleType): 処理されるローソク足の時間軸。
  • シグナルバー (SignalBar): 色の変化を検査するために遡る完了したローソク足の数。
  • ローカルタイムゾーン (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; }
	}
}