GitHub で見る

三本ローソク足リバーサル戦略

この戦略はMQL5エキスパートアドバイザーExp_ThreeCandlesの忠実なStockSharpポートです。古典的な三本ローソク足リバーサルを探します:

  1. 同じ方向への2本の連続したローソク足。
  2. 方向を反転させて中間バーを超えて閉じる3本目のローソク足。
  3. パターンの最古のバーが例外的に大きくない限り、オプションのボリューム確認。

強気の構成が現れると、アルゴリズムはショートエクスポージャーを閉じてロングポジションに入ることができます。弱気の構成はその逆を行います。保護的なストップロスとテイクプロフィットレベルは現在のインストゥルメント価格ステップを使用して適用されます。

パターン検出

戦略は最新のSignalBar + 3本の完成したローソク足のローリングウィンドウを維持します。新しいバーごとにSignalBarオフセット(デフォルト:1バー前)のローソク足と3本古いローソク足を確認します:

  • 強気のリバーサル(潜在的なロング):
    • 2本の古いローソク足(SignalBar + 3SignalBar + 2)が弱気。
    • 中間のローソク足が最古バーの安値より上で閉じる。
    • シグナルの直前の最新ローソク足(SignalBar + 1)が強気で中間バーの始値より上で閉じる。
  • 弱気のリバーサル(潜在的なショート):
    • 強気ケースのミラーロジック。

ボリュームフィルターは元のインジケーターを反映します。最古ローソク足の範囲(価格ステップ単位)がMaxBarSizeを超えるか、VolumeFilterNoneに設定されている場合、フィルターはスキップされます。そうでない場合、リバーサルは古いボリューム < 中間ボリューム または 最新ボリューム > 中間ボリューム または 最新ボリューム > 最古ボリュームを満たす必要があります。StockSharpは高レベルのローソク足ストリームでは2つを区別しないため、ティックとリアルボリュームはローソク足の集計ボリュームにマップされます。

取引管理

  • AllowSellExitが有効な場合、強気のパターンはロングエントリーを検討する前に直ちに任意のショートポジションをカバーします。AllowBuyExitは弱気パターンのロングについても同様に動作します。
  • 新しいポジションは現在のポジションがフラットで対応するAllow*Entryフラグが真の場合にのみ開かれます。注文サイズは戦略の標準ボリューム設定を使用します。
  • ストップロスとテイクプロフィットの距離(StopLossPipsTakeProfitPips)は価格ステップで表現され、完成した各ローソク足で監視されます。
  • ローソク足がティックを発生し続けている間の重複アクションを避けるため、最後に処理された強気/弱気シグナル時刻がキャッシュされます。

パラメーター

名前 デフォルト 説明
CandleType 4時間時間軸 戦略で処理されるローソク足シリーズ。
SignalBar 1 シグナルが評価される何バー前か。≥ 0でなければなりません。
MaxBarSize 300 最古バーの範囲(PriceStepで変換)がこの値を超えるとボリュームフィルターがスキップされます。常にスキップするには0に設定。
VolumeFilter Tick ボリュームモード(TickReal、またはNone)。TickRealの両方がローソク足からTotalVolumeを使用します。
AllowBuyEntry true 強気パターンでのロングエントリーを有効にする。
AllowSellEntry true 弱気パターンでのショートエントリーを有効にする。
AllowBuyExit true 弱気パターンでロングポジションを閉じることを許可する。
AllowSellExit true 強気パターンでショートポジションを閉じることを許可する。
StopLossPips 1000 価格ステップでのストップロス距離(0で無効)。
TakeProfitPips 2000 価格ステップでのテイクプロフィット距離(0で無効)。

変換に関する注意

  • 元のMQL5インクルードファイルの資金管理ルーティンはStockSharpのBuyMarket/SellMarket呼び出しに置き換えられました。したがって、ポジションサイズはエンジンのデフォルトボリュームに従います。
  • シグナルタイミングはSignalBarオフセットのバーを評価し、前のシグナルタイムスタンプを保持することでエキスパートアドバイザーを反映します。
  • MQLインジケーターのメール、プッシュ、サウンドアラートは意図的に省略されています。
  • ボリュームモードは保持されますが、高レベルAPIでは個別のティックとリアルボリュームが利用できないため、両方ともローソク足の集計ボリュームにマップされます。
  • すべてのコメントはプロジェクトガイドラインで要求されているとおり英語で書き直されました。

この実装はStockSharpの高レベルサブスクリプションモデルに準拠しながら元の動作に近く保たれています。

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>
/// Translates the classic Three Candles reversal expert advisor from MQL5.
/// The strategy searches for two candles in one direction followed by a strong opposite candle and trades the expected reversal.
/// </summary>
public class ThreeCandlesReversalStrategy : Strategy
{
	public enum ThreeCandlesVolumeTypes
	{
		Tick,
		Real,
		None,
	}

	private readonly List<CandleSample> _candles = new();
	private static readonly object _sync = new();

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<int> _maxBarSize;
	private readonly StrategyParam<ThreeCandlesVolumeTypes> _volumeFilter;
	private readonly StrategyParam<bool> _allowBuyEntry;
	private readonly StrategyParam<bool> _allowSellEntry;
	private readonly StrategyParam<bool> _allowBuyExit;
	private readonly StrategyParam<bool> _allowSellExit;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;

	private DateTimeOffset? _lastBullishSignalTime;
	private DateTimeOffset? _lastBearishSignalTime;
	private decimal _entryPrice;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int SignalBar { get => _signalBar.Value; set => _signalBar.Value = value; }
	public int MaxBarSize { get => _maxBarSize.Value; set => _maxBarSize.Value = value; }
	public ThreeCandlesVolumeTypes VolumeFilter { get => _volumeFilter.Value; set => _volumeFilter.Value = value; }
	public bool AllowBuyEntry { get => _allowBuyEntry.Value; set => _allowBuyEntry.Value = value; }
	public bool AllowSellEntry { get => _allowSellEntry.Value; set => _allowSellEntry.Value = value; }
	public bool AllowBuyExit { get => _allowBuyExit.Value; set => _allowBuyExit.Value = value; }
	public bool AllowSellExit { get => _allowSellExit.Value; set => _allowSellExit.Value = value; }
	public decimal StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public decimal TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }

	public ThreeCandlesReversalStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for the candle subscription", "General");
		_signalBar = Param(nameof(SignalBar), 1)
			.SetRange(0, 20)
			.SetDisplay("Signal Bar", "Historical offset where the signal is evaluated", "Pattern");
		_maxBarSize = Param(nameof(MaxBarSize), 300)
			.SetRange(0, 100000)
			.SetDisplay("Max Bar Size", "Disable the volume filter when the oldest candle range exceeds this value (in price steps)", "Pattern");
		_volumeFilter = Param(nameof(VolumeFilter), ThreeCandlesVolumeTypes.Tick)
			.SetDisplay("Volume Filter", "Volume filter used to confirm the reversal", "Pattern");
		_allowBuyEntry = Param(nameof(AllowBuyEntry), true)
			.SetDisplay("Allow Buy Entry", "Enable long entries on bullish signals", "Trading");
		_allowSellEntry = Param(nameof(AllowSellEntry), true)
			.SetDisplay("Allow Sell Entry", "Enable short entries on bearish signals", "Trading");
		_allowBuyExit = Param(nameof(AllowBuyExit), true)
			.SetDisplay("Allow Buy Exit", "Close long positions when a bearish pattern appears", "Trading");
		_allowSellExit = Param(nameof(AllowSellExit), true)
			.SetDisplay("Allow Sell Exit", "Close short positions when a bullish pattern appears", "Trading");
		_stopLossPips = Param(nameof(StopLossPips), 1000m)
			.SetRange(0m, 100000m)
			.SetDisplay("Stop Loss", "Distance to the protective stop in price steps", "Risk");
		_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
			.SetRange(0m, 100000m)
			.SetDisplay("Take Profit", "Distance to the profit target in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	protected override void OnReseted()
	{
		base.OnReseted();

		_candles.Clear();
		_lastBullishSignalTime = null;
		_lastBearishSignalTime = null;
		_entryPrice = 0m;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

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

		lock (_sync)
		{
			var closeTime = candle.CloseTime != default
				? candle.CloseTime
				: candle.OpenTime + (CandleType.Arg is TimeSpan tf ? tf : TimeSpan.Zero);

			_candles.Add(new CandleSample(
				candle.OpenTime,
				closeTime,
				candle.OpenPrice,
				candle.HighPrice,
				candle.LowPrice,
				candle.ClosePrice,
				candle.TotalVolume));

			var required = SignalBar + 5;
			while (_candles.Count > required)
				_candles.RemoveAt(0);

			if (_candles.Count < required)
				return;

			var priceStep = Security?.PriceStep ?? 1m;
			if (priceStep <= 0m)
				priceStep = 1m;

			if (CheckRiskManagement(candle, priceStep))
				return;

			var buffer = _candles.ToArray();
			var bullishSignal = IsBullishSignal(buffer, priceStep);
			var bearishSignal = IsBearishSignal(buffer, priceStep);

			if (bullishSignal)
			{
				var signalCandle = GetSeries(buffer, SignalBar);
				HandleBullish(signalCandle);
			}

			if (bearishSignal)
			{
				var signalCandle = GetSeries(buffer, SignalBar);
				HandleBearish(signalCandle);
			}
		}
	}

	private bool CheckRiskManagement(ICandleMessage candle, decimal priceStep)
	{
		if (Position == 0m || _entryPrice == 0m)
		return false;

		var stopDistance = StopLossPips > 0m ? StopLossPips * priceStep : 0m;
		var takeDistance = TakeProfitPips > 0m ? TakeProfitPips * priceStep : 0m;

		if (Position > 0m)
		{
		var stopTriggered = stopDistance > 0m && candle.LowPrice <= _entryPrice - stopDistance;
		var takeTriggered = takeDistance > 0m && candle.HighPrice >= _entryPrice + takeDistance;

		if (stopTriggered || takeTriggered)
		{
		SellMarket();
		ResetTradeState();
		return true;
		}
		}
		else if (Position < 0m)
		{
		var stopTriggered = stopDistance > 0m && candle.HighPrice >= _entryPrice + stopDistance;
		var takeTriggered = takeDistance > 0m && candle.LowPrice <= _entryPrice - takeDistance;

		if (stopTriggered || takeTriggered)
		{
		BuyMarket();
		ResetTradeState();
		return true;
		}
		}

		return false;
	}

	private void HandleBullish(CandleSample signalCandle)
	{
		var signalTime = signalCandle.CloseTime;
		if (_lastBullishSignalTime == signalTime)
			return;

		if (AllowSellExit && Position < 0m)
		{
			BuyMarket();
			ResetTradeState();
		}

		if (AllowBuyEntry && Position == 0m)
		{
			BuyMarket();
			_entryPrice = signalCandle.ClosePrice;
		}

		_lastBullishSignalTime = signalTime;
	}

	private void HandleBearish(CandleSample signalCandle)
	{
		var signalTime = signalCandle.CloseTime;
		if (_lastBearishSignalTime == signalTime)
			return;

		if (AllowBuyExit && Position > 0m)
		{
			SellMarket();
			ResetTradeState();
		}

		if (AllowSellEntry && Position == 0m)
		{
			SellMarket();
			_entryPrice = signalCandle.ClosePrice;
		}

		_lastBearishSignalTime = signalTime;
	}

	private bool IsBullishSignal(IReadOnlyList<CandleSample> candles, decimal priceStep)
	{
		var last = GetSeries(candles, SignalBar + 1);
		var middle = GetSeries(candles, SignalBar + 2);
		var oldest = GetSeries(candles, SignalBar + 3);

		if (!(oldest.OpenPrice > oldest.ClosePrice &&
			middle.OpenPrice > middle.ClosePrice &&
			middle.ClosePrice > oldest.LowPrice &&
			last.OpenPrice < last.ClosePrice &&
			last.ClosePrice > middle.OpenPrice))
		{
			return false;
		}

		if (!ShouldApplyVolumeFilter(oldest, priceStep))
			return true;

		var volOldest = oldest.Volume;
		var volMiddle = middle.Volume;
		var volLast = last.Volume;

		return volOldest < volMiddle || volLast > volMiddle || volLast > volOldest;
	}

	private bool IsBearishSignal(IReadOnlyList<CandleSample> candles, decimal priceStep)
	{
		var last = GetSeries(candles, SignalBar + 1);
		var middle = GetSeries(candles, SignalBar + 2);
		var oldest = GetSeries(candles, SignalBar + 3);

		if (!(oldest.OpenPrice < oldest.ClosePrice &&
			middle.OpenPrice < middle.ClosePrice &&
			middle.ClosePrice < oldest.HighPrice &&
			last.OpenPrice > last.ClosePrice &&
			last.ClosePrice < middle.OpenPrice))
		{
			return false;
		}

		if (!ShouldApplyVolumeFilter(oldest, priceStep))
			return true;

		var volOldest = oldest.Volume;
		var volMiddle = middle.Volume;
		var volLast = last.Volume;

		return volOldest < volMiddle || volLast > volMiddle || volLast > volOldest;
	}

	private bool ShouldApplyVolumeFilter(CandleSample oldest, decimal priceStep)
	{
		if (VolumeFilter == ThreeCandlesVolumeTypes.None)
			return false;

		if (MaxBarSize <= 0)
			return false;

		var range = oldest.HighPrice - oldest.LowPrice;
		var threshold = MaxBarSize * priceStep;

		if (range > threshold)
			return false;

		return true;
	}

	private static CandleSample GetSeries(IReadOnlyList<CandleSample> candles, int index)
	{
		var idx = candles.Count - 1 - index;
		return candles[idx];
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
	}

	private readonly record struct CandleSample(
		DateTimeOffset OpenTime,
		DateTimeOffset CloseTime,
		decimal OpenPrice,
		decimal HighPrice,
		decimal LowPrice,
		decimal ClosePrice,
		decimal Volume);
}