GitHub で見る

Heiken Ashi Idea 戦略

概要

この戦略はStockSharpの高レベルAPIを使用して、オリジナルのHeikenAshiIdea.mq4エキスパートアドバイザーの動作を再現します。2つのタイムフレームのHeikin Ashiローソク足で一致した強気または弱気のシグナルを待ち、市場から設定可能な距離に指値の指値注文を置きます。目標は、最新のHeikin Ashiローソク足がトレンドの方向と反対のヒゲを持たない場合に強いトレンド継続の動きを捉えることです。

トレーディングロジック

  1. Heikin Ashi再構築 – 戦略は内部的にプライマリ取引タイムフレームとより高い確認タイムフレームのHeikin Ashiローソク足を再構築します。各タイムフレームで最後の2本のHeikin Ashiローソク足が保存され、実体の方向とヒゲの存在を分析できます。
  2. ブレイクアウト条件 – 両方のタイムフレームが以下を示す場合にロングセットアップが現れます:
    • 最新のHeikin Ashiローソク足は強気で始値が安値と等しい(下ヒゲなし)、かつ
    • 前のHeikin Ashiローソク足も強気だが下ヒゲがある。 ショートセットアップは対称的な弱気条件を必要とします(最新のローソク足に上ヒゲなし、前のローソク足に上ヒゲあり)。
  3. ATRボラティリティフィルター – フィルターが有効な場合、設定可能な長さのAverage True Rangeが上昇している必要があります(ATR[t] > ATR[t-1])。これはオリジナルのActiveMarketボラティリティチェックを再現します。
  4. 取引ウィンドウ – ユーザー定義の取引セッション外のシグナルは無視されます(デフォルト: 09:00–19:00)。
  5. 注文配置 – シグナルが有効な場合、戦略は単一の指値注文を置きます:
    • ロングシグナル → ClosePrice - DistancePoints * PriceStepでの買い指値注文。
    • ショートシグナル → ClosePrice + DistancePoints * PriceStepでの売り指値注文。 新しい注文をキューに入れる前に、既存の反対方向の指値注文はキャンセルされます。戦略は方向ごとに1つの指値注文のみを追跡し、注文が非アクティブになると自動的に参照をクリアします。
  6. ポジション管理 – オプションのテイクプロフィットとストップロスの距離はStartProtectionを通じてStockSharpの保護メカニズムに変換されます。「全クローズ」タイムフレームの新しいローソク足が開いたとき、フラグが有効な場合、戦略はすべての指値注文をキャンセルし、オープンポジションを閉じます。これはオリジナルEAのUseCloseAll動作を模倣します。

リスク管理

  • 保護レベルはMetaTrader実装に近づけるため価格ステップ(ポイント)で表されます。オプションです。0を使用すると対応する保護が無効になります。
  • 指値注文は計算された距離が正で取引ボリュームがゼロより大きい場合にのみ置かれます。
  • 戦略はポジションを自動的に平均化しません。新しいものをスケジュールする前にまず反対方向の指値注文を解消します。
  • Heikin Ashiローソク足にヒゲがあるかどうかを確認する際に、インストゥルメントの価格ステップの半分に等しい許容誤差が使用されます。これにより浮動小数点丸め問題を防ぎながら、オリジナルの厳密な比較に忠実であり続けます。

パラメーター

名前 説明 デフォルト値
DistancePoints 指値注文のための価格ステップ単位の距離。 8
StopLossPoints 価格ステップ単位のストップロス距離(0でストップ無効)。 0
TakeProfitPoints 価格ステップ単位のテイクプロフィット距離(0で目標無効)。 20
UseCloseAllOnNewBar 全クローズタイムフレームの新しいローソク足が開いたときにポジションを閉じ注文をキャンセルするか。 true
CandleType 取引シグナルに使用するプライマリローソク足タイプ。 30mタイムフレーム
HigherCandleType マルチタイムフレームフィルターの確認ローソク足タイプ。 1dタイムフレーム
CloseAllCandleType 全クローズルーティンをトリガーするローソク足タイプ。 7dタイムフレーム
StartHour 取引セッションの最初の時間(含む)。 9
EndHour 取引セッションの最後の時間(含む)。 19
UseAtrFilter ATR上昇ボラティリティフィルターを有効にするか。 true
AtrPeriod ボラティリティフィルターで使用するATR期間。 14

追加メモ

  • 戦略はベース注文サイズとしてStrategyの組み込みVolumeプロパティを使用します。戦略を開始する前に調整してください。
  • StockSharp実装が指値注文配置にローソク足の終値を使用するため、ライブ実行はbid/askクォートを使用したオリジナルのMT4コードと若干異なる場合がありますが、コアアイデアはそのままです。
  • 異なる市場に対してロジックを拡張するには、マルチタイムフレーム確認を維持しながらローソク足タイプ、取引ウィンドウ、距離パラメーターを調整するだけです。
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Multi-timeframe Heikin Ashi strategy that uses pending limit orders and an ATR filter.
/// </summary>
public class HeikenAshiIdeaStrategy : Strategy
{
	private readonly StrategyParam<decimal> _distancePoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<bool> _useCloseAll;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _higherCandleType;
	private readonly StrategyParam<DataType> _closeAllCandleType;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<bool> _useAtrFilter;
	private readonly StrategyParam<int> _atrPeriod;

	private AverageTrueRange _atr;

	private bool _hasAtrValue;
	private bool _hasPrevAtrValue;
	private decimal _lastAtrValue;
	private decimal _prevAtrValue;

	private bool _baseHasCurrent;
	private bool _baseHasPrevious;
	private HeikinAshiCandle _baseCurrentHa;
	private HeikinAshiCandle _basePreviousHa;

	private bool _higherHasCurrent;
	private bool _higherHasPrevious;
	private HeikinAshiCandle _higherCurrentHa;
	private HeikinAshiCandle _higherPreviousHa;

	private Order _buyOrder;
	private Order _sellOrder;

	private DateTimeOffset? _lastCloseAllTime;

	private decimal _priceStep;
	private decimal _comparisonTolerance;

	/// <summary>
	/// Distance in price steps used to offset pending orders from the market price.
	/// </summary>
	public decimal DistancePoints
	{
		get => _distancePoints.Value;
		set => _distancePoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in price steps (0 disables the protective stop).
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price steps (0 disables the target).
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Whether to flatten positions when a new candle of the close-all timeframe opens.
	/// </summary>
	public bool UseCloseAllOnNewBar
	{
		get => _useCloseAll.Value;
		set => _useCloseAll.Value = value;
	}

	/// <summary>
	/// Primary candle type used for trade signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe candle type that confirms the trend.
	/// </summary>
	public DataType HigherCandleType
	{
		get => _higherCandleType.Value;
		set => _higherCandleType.Value = value;
	}

	/// <summary>
	/// Candle type used to trigger the close-all routine.
	/// </summary>
	public DataType CloseAllCandleType
	{
		get => _closeAllCandleType.Value;
		set => _closeAllCandleType.Value = value;
	}

	/// <summary>
	/// First hour of the trading window (inclusive).
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Last hour of the trading window (inclusive).
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Whether the strategy requires rising ATR values before placing new orders.
	/// </summary>
	public bool UseAtrFilter
	{
		get => _useAtrFilter.Value;
		set => _useAtrFilter.Value = value;
	}

	/// <summary>
	/// ATR period used in the volatility filter.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="HeikenAshiIdeaStrategy"/> class.
	/// </summary>
	public HeikenAshiIdeaStrategy()
	{
		_distancePoints = Param(nameof(DistancePoints), 8m)
				.SetGreaterThanZero()
				.SetDisplay("Pending Distance (pts)", "Distance for pending limit orders measured in price steps.", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0m)
				.SetDisplay("Stop Loss (pts)", "Stop-loss distance in price steps. Set to 0 to disable the protective stop.", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20m)
				.SetDisplay("Take Profit (pts)", "Take-profit distance in price steps. Set to 0 to disable the target.", "Risk");

		_useCloseAll = Param(nameof(UseCloseAllOnNewBar), true)
				.SetDisplay("Close On Higher Bar", "Flatten positions when a new candle of the close-all timeframe opens.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
				.SetDisplay("Primary Candle Type", "Primary timeframe used for trading signals.", "Data");

		_higherCandleType = Param(nameof(HigherCandleType), TimeSpan.FromHours(1).TimeFrame())
				.SetDisplay("Higher Candle Type", "Confirmation timeframe used for Heikin Ashi trend filter.", "Data");

		_closeAllCandleType = Param(nameof(CloseAllCandleType), TimeSpan.FromHours(4).TimeFrame())
				.SetDisplay("Close-All Candle Type", "Timeframe that triggers a complete exit on a new bar.", "Data");

		_startHour = Param(nameof(StartHour), 0)
				.SetRange(0, 23)
				.SetDisplay("Start Hour", "First hour of the trading window (inclusive).", "Session");

		_endHour = Param(nameof(EndHour), 23)
				.SetRange(0, 23)
				.SetDisplay("End Hour", "Last hour of the trading window (inclusive).", "Session");

		_useAtrFilter = Param(nameof(UseAtrFilter), false)
				.SetDisplay("Use ATR Filter", "Require rising ATR to allow new orders.", "Filters");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
				.SetGreaterThanZero()
				.SetDisplay("ATR Period", "Period used for the ATR volatility filter.", "Filters");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
		yield return (Security, HigherCandleType);

		if (UseCloseAllOnNewBar)
				yield return (Security, CloseAllCandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_atr?.Reset();
		_hasAtrValue = false;
		_hasPrevAtrValue = false;
		_lastAtrValue = 0m;
		_prevAtrValue = 0m;

		_baseHasCurrent = false;
		_baseHasPrevious = false;
		_baseCurrentHa = default;
		_basePreviousHa = default;

		_higherHasCurrent = false;
		_higherHasPrevious = false;
		_higherCurrentHa = default;
		_higherPreviousHa = default;

		_buyOrder = null;
		_sellOrder = null;
		_lastCloseAllTime = null;
		_priceStep = 0m;
		_comparisonTolerance = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_priceStep = Security?.PriceStep ?? 1m;

		if (_priceStep <= 0m)
				_priceStep = 1m;

		_comparisonTolerance = _priceStep / 2m;

		_atr = new AverageTrueRange { Length = AtrPeriod };

		var primarySubscription = SubscribeCandles(CandleType);
		primarySubscription
				.Bind(_atr, ProcessPrimaryCandle)
				.Start();

		var higherSubscription = SubscribeCandles(HigherCandleType);
		higherSubscription
				.Bind(ProcessHigherCandle)
				.Start();

		if (UseCloseAllOnNewBar)
		{
				var closeAllSubscription = SubscribeCandles(CloseAllCandleType);
				closeAllSubscription
					.Bind(ProcessCloseAllCandle)
					.Start();
		}

		var area = CreateChartArea();
		if (area != null)
		{
				DrawCandles(area, primarySubscription);
				DrawOwnTrades(area);
		}

		var takeProfitUnit = TakeProfitPoints > 0m ? new Unit(TakeProfitPoints * _priceStep, UnitTypes.Absolute) : null;
		var stopLossUnit = StopLossPoints > 0m ? new Unit(StopLossPoints * _priceStep, UnitTypes.Absolute) : null;

		if (takeProfitUnit != null || stopLossUnit != null)
		{
				StartProtection(takeProfitUnit, stopLossUnit);
		}
	}

	private void ProcessPrimaryCandle(ICandleMessage candle, decimal atrValue)
	{
		if (candle.State != CandleStates.Finished)
				return;

		UpdateAtrState(atrValue);
		UpdateHeikinAshiState(candle, ref _baseHasCurrent, ref _baseHasPrevious, ref _baseCurrentHa, ref _basePreviousHa);
		TryPlaceOrders(candle);
	}

	private void ProcessHigherCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
				return;

		UpdateHeikinAshiState(candle, ref _higherHasCurrent, ref _higherHasPrevious, ref _higherCurrentHa, ref _higherPreviousHa);
	}

	private void ProcessCloseAllCandle(ICandleMessage candle)
	{
		if (!UseCloseAllOnNewBar || candle.State != CandleStates.Finished)
				return;

		if (_lastCloseAllTime == candle.OpenTime)
				return;

		_lastCloseAllTime = candle.OpenTime;

		CancelTrackedOrders();

		if (Position > 0)
				SellMarket();
		else if (Position < 0)
				BuyMarket();
	}

	private void UpdateAtrState(decimal atrValue)
	{
		if (!_atr.IsFormed)
				return;

		if (_hasAtrValue)
		{
				_prevAtrValue = _lastAtrValue;
				_hasPrevAtrValue = true;
		}

		_lastAtrValue = atrValue;
		_hasAtrValue = true;
	}

	private void UpdateHeikinAshiState(ICandleMessage candle, ref bool hasCurrent, ref bool hasPrevious, ref HeikinAshiCandle current, ref HeikinAshiCandle previous)
	{
		var hadCurrent = hasCurrent;
		var last = current;

		var haOpen = hadCurrent ? (last.Open + last.Close) / 2m : (candle.OpenPrice + candle.ClosePrice) / 2m;
		var haClose = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
		var haHigh = Math.Max(Math.Max(candle.HighPrice, haOpen), haClose);
		var haLow = Math.Min(Math.Min(candle.LowPrice, haOpen), haClose);

		var haCandle = new HeikinAshiCandle(haOpen, haHigh, haLow, haClose);

		if (hadCurrent)
		{
				previous = last;
				hasPrevious = true;
		}

		current = haCandle;
		hasCurrent = true;
	}

	private void TryPlaceOrders(ICandleMessage candle)
	{
		if (DistancePoints <= 0m || StopLossPoints < 0m || TakeProfitPoints < 0m)
				return;

		var timeOfDay = candle.OpenTime.TimeOfDay;
		if (!IsWithinTradeHours(timeOfDay))
				return;

		if (!_baseHasPrevious || !_higherHasPrevious)
				return;

		if (UseAtrFilter)
		{
				if (!_hasPrevAtrValue || _lastAtrValue <= _prevAtrValue)
						return;
		}

		UpdateOrderReferences();

		var longSignal = IsHeikinBullishBreakout(_baseCurrentHa, _basePreviousHa) && IsHeikinBullishBreakout(_higherCurrentHa, _higherPreviousHa);
		var shortSignal = IsHeikinBearishBreakout(_baseCurrentHa, _basePreviousHa) && IsHeikinBearishBreakout(_higherCurrentHa, _higherPreviousHa);

		var offset = DistancePoints * _priceStep;

		if (longSignal && Position <= 0m)
		{
				if (_sellOrder != null && _sellOrder.State == OrderStates.Active)
				{
						CancelOrder(_sellOrder);
						_sellOrder = null;
				}

				if (_buyOrder == null || _buyOrder.State != OrderStates.Active)
				{
						var price = candle.ClosePrice - offset;

						if (price <= 0m)
								price = _priceStep;

						var volume = Volume + (Position < 0m ? Math.Abs(Position) : 0m);

						if (volume > 0m)
								_buyOrder = BuyLimit(price, volume);
				}
		}
		else if (shortSignal && Position >= 0m)
		{
				if (_buyOrder != null && _buyOrder.State == OrderStates.Active)
				{
						CancelOrder(_buyOrder);
						_buyOrder = null;
				}

				if (_sellOrder == null || _sellOrder.State != OrderStates.Active)
				{
						var price = candle.ClosePrice + offset;
						var volume = Volume + (Position > 0m ? Math.Abs(Position) : 0m);

						if (volume > 0m)
								_sellOrder = SellLimit(price, volume);
				}
		}
	}

	private void UpdateOrderReferences()
	{
		if (_buyOrder != null && _buyOrder.State != OrderStates.Active)
				_buyOrder = null;

		if (_sellOrder != null && _sellOrder.State != OrderStates.Active)
				_sellOrder = null;
	}

	private void CancelTrackedOrders()
	{
		if (_buyOrder != null)
		{
				if (_buyOrder.State == OrderStates.Active)
						CancelOrder(_buyOrder);

				_buyOrder = null;
		}

		if (_sellOrder != null)
		{
				if (_sellOrder.State == OrderStates.Active)
						CancelOrder(_sellOrder);

				_sellOrder = null;
		}
	}

	private bool IsWithinTradeHours(TimeSpan time)
	{
		var start = TimeSpan.FromHours(StartHour);
		var end = TimeSpan.FromHours(EndHour);

		if (end < start)
				return time >= start || time <= end;

		return time >= start && time <= end;
	}

	private bool IsHeikinBullishBreakout(HeikinAshiCandle current, HeikinAshiCandle previous)
	{
		return IsBullish(current) && HasNoLowerShadow(current) && IsBullish(previous) && HasLowerShadow(previous);
	}

	private bool IsHeikinBearishBreakout(HeikinAshiCandle current, HeikinAshiCandle previous)
	{
		return IsBearish(current) && HasNoUpperShadow(current) && IsBearish(previous) && HasUpperShadow(previous);
	}

	private static bool IsBullish(HeikinAshiCandle candle)
	{
		return candle.Close > candle.Open;
	}

	private static bool IsBearish(HeikinAshiCandle candle)
	{
		return candle.Close < candle.Open;
	}

	private bool HasNoLowerShadow(HeikinAshiCandle candle)
	{
		return Math.Abs(candle.Open - candle.Low) <= _comparisonTolerance;
	}

	private bool HasLowerShadow(HeikinAshiCandle candle)
	{
		return Math.Abs(candle.Open - candle.Low) > _comparisonTolerance;
	}

	private bool HasNoUpperShadow(HeikinAshiCandle candle)
	{
		return Math.Abs(candle.Open - candle.High) <= _comparisonTolerance;
	}

	private bool HasUpperShadow(HeikinAshiCandle candle)
	{
		return Math.Abs(candle.Open - candle.High) > _comparisonTolerance;
	}

	private readonly struct HeikinAshiCandle
	{
		public HeikinAshiCandle(decimal open, decimal high, decimal low, decimal close)
		{
				Open = open;
				High = high;
				Low = low;
				Close = close;
		}

		public decimal Open { get; }

		public decimal High { get; }

		public decimal Low { get; }

		public decimal Close { get; }
	}
}