GitHub で見る

イブニングスター・リバーサル戦略

概要

この戦略はエキスパートアドバイザー EveningStar.mq5(MQL5 id 18507)の直接ポートです。クラシックなイブニングスターローソク足フォーメーションを監視し、次のバーが取引を開始するとすぐにポジションを建てます。ロジックはStockSharpの高レベルAPIをベースに書き直されており、オリジナルのリスク管理とパターンフィルターを維持しています。

トレードロジック

  1. 戦略はCandleTypeパラメーターで選択された時間軸にサブスクライブします。すべての処理は完成したローソク足のみで行われます。
  2. 新しいローソク足が閉じるたびに最新のスナップショットがキャッシュされ、Shiftで定義された3本ローソク足のウィンドウを評価できるようになります。
  3. イブニングスターパターンは以下の場合に有効とみなされます:
    • ローソク足 N-2(最も古い)が強気(open < close)。
    • ローソク足 N-1(中央)がCandle2Bullishの設定を満たす(デフォルトは強気)。
    • ローソク足 N(最新)が弱気(open > close)。
    • CheckCandleSizesが有効な場合、中央のローソク足が3本の中で最も小さいボディを持つ必要がある。
    • ConsiderGapが有効な場合、元のロボットと同様にローソク足ボディ間にギャップが存在する必要がある(ギャップサイズは銘柄の価格刻みから計算した1pipsに等しい)。
  4. パターンが確認されたら、戦略はDirectionで選択された方向を確認します:
    • Short(デフォルト)は売り注文を開き、元のイブニングスターの動作に一致します。
    • Longはまったく逆のエクスポージャーを実行することを可能にします(MQLバージョンとの機能パリティのために維持)。
  5. ポジションを開く前に、CloseOppositePositionstrueに設定されている場合、アルゴリズムはオプションで反対のエクスポージャーを閉じます。
  6. ストップロスとテイクプロフィットの価格は、pip距離(StopLossPipsTakeProfitPips)からMetaTraderにあったのと同じ3/5桁調整を使用して計算されます。
  7. ポジションサイズは現在のポートフォリオ価値とRiskPercentから導出されます。計算されたボリュームが最小取引可能サイズより小さい場合、シグナルは無視されます。

ポジション管理

  • ロングポジションがアクティブな場合、戦略は各新しいローソク足を監視します。安値がストップレベルを下回るか、高値がテイクプロフィットレベルに達した場合、ポジション全体が成行で閉じられます。
  • ショートポジションがアクティブな場合、逆の比較で同じロジックが適用されます。
  • ポートフォリオ価値またはストップ距離がゼロの場合、注文サイズを計算できないため、エントリーはスキップされます。

パラメーター

名前 デフォルト値 説明
Direction Short パターンがロングかショートのポジションを建てるかを選択。
TakeProfitPips 150 pipsで表したプロフィットターゲットまでの距離。無効にするにはゼロに設定。
StopLossPips 50 pipsでの保護ストップまでの距離。非正の値はトレードを無効にする。
RiskPercent 5 1トレードあたりに許容するポートフォリオ資産の割合。注文ボリュームの計算に使用。
Shift 1 パターン評価の前に最新のローソク足からスキップするバーの数。
ConsiderGap true 元のエキスパートアドバイザーと同様にローソク足ボディ間のギャップを要求。
Candle2Bullish true 中央のローソク足を強気にするよう強制。無効にすると弱気の中央ローソク足を要求。
CheckCandleSizes true 中央のローソク足が最小の絶対ボディを持つことを確認。
CloseOppositePositions true 新しい注文を送信する前に反対のエクスポージャーを閉じる。
CandleType 1H時間軸 分析に使用するローソク足シリーズ。

注意事項

  • pipサイズは銘柄の価格刻みから導出されます。3桁および5桁の外国為替シンボルでは、1pipは10価格刻みに相当し、元のEAの動作を再現します。
  • StopLossPipsがゼロの場合、ポジションサイズを計算できず、無制限のリスクを防ぐためにシグナルは無視されます。
  • 戦略はキャッシュされた履歴を自動的に整理するため、長いセッションでもメモリ使用量は一定に保たれます。
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>
/// Evening Star candlestick pattern strategy converted from MQL5 implementation.
/// </summary>
public class EveningStarReversalStrategy : Strategy
{
	public enum PatternDirections
	{
		Long,
		Short
	}

	private readonly StrategyParam<PatternDirections> _direction;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<bool> _considerGap;
	private readonly StrategyParam<bool> _candle2Bullish;
	private readonly StrategyParam<bool> _checkCandleSizes;
	private readonly StrategyParam<bool> _closeOpposite;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<CandleSnapshot> _history = new();

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;

	public PatternDirections Direction
	{
		get => _direction.Value;
		set => _direction.Value = value;
	}

	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	public bool ConsiderGap
	{
		get => _considerGap.Value;
		set => _considerGap.Value = value;
	}

	public bool Candle2Bullish
	{
		get => _candle2Bullish.Value;
		set => _candle2Bullish.Value = value;
	}

	public bool CheckCandleSizes
	{
		get => _checkCandleSizes.Value;
		set => _checkCandleSizes.Value = value;
	}

	public bool CloseOppositePositions
	{
		get => _closeOpposite.Value;
		set => _closeOpposite.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public EveningStarReversalStrategy()
	{
		_direction = Param(nameof(Direction), PatternDirections.Short)
			.SetDisplay("Signal Direction", "Side to trade when the pattern appears", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk Management")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk Management")
			.SetGreaterThanZero();

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetDisplay("Risk (%)", "Risk per trade as percentage of equity", "Risk Management")
			.SetGreaterThanZero();

		_shift = Param(nameof(Shift), 1)
			.SetDisplay("Shift", "Offset for the bar sequence", "Pattern")
			.SetGreaterThanZero();

		_considerGap = Param(nameof(ConsiderGap), true)
			.SetDisplay("Consider Gap", "Require price gaps between candles", "Pattern");

		_candle2Bullish = Param(nameof(Candle2Bullish), true)
			.SetDisplay("Middle Candle Bullish", "Should the second candle close above its open", "Pattern");

		_checkCandleSizes = Param(nameof(CheckCandleSizes), true)
			.SetDisplay("Check Candle Sizes", "Ensure the middle candle has the smallest body", "Pattern");

		_closeOpposite = Param(nameof(CloseOppositePositions), true)
			.SetDisplay("Close Opposite", "Close the existing opposite position before entry", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series to process", "General");
	}

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

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

		_history.Clear();
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
	}

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

		_pipSize = CalculatePipSize();

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

		// no protection needed
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Ensure we only process finished candles.
		if (candle.State != CandleStates.Finished)
		return;

		// Store the candle snapshot for pattern evaluation.
		_history.Add(new CandleSnapshot(candle.OpenPrice, candle.ClosePrice, candle.HighPrice, candle.LowPrice));
		TrimHistory();

		// Manage any open trade before searching for a new signal.
		HandleActivePosition(candle);

		//if (!IsFormedAndOnlineAndAllowTrading())
		//return;

		// The pattern requires three completed candles with the configured shift.
		var requiredCount = Shift + 2;
		if (_history.Count < requiredCount)
		return;

		var lastIndex = _history.Count - Shift;
		if (lastIndex < 2 || lastIndex >= _history.Count)
		return;

		var recent = _history[lastIndex];
		var middle = _history[lastIndex - 1];
		var first = _history[lastIndex - 2];

		// Validate the Evening Star structure and optional filters.
		if (!IsPatternValid(first, middle, recent))
		return;

		var isLong = Direction == PatternDirections.Long;
		var entryPrice = recent.Close;
		var stopPrice = CalculateStop(entryPrice, isLong);
		var takeProfitPrice = CalculateTake(entryPrice, isLong);

		// Size the position using the risk percentage from the portfolio value.
		var volume = CalculatePositionSize(entryPrice, stopPrice);
		if (volume <= 0m)
		return;

		if (isLong)
		{
		if (Position < 0 && !CloseOppositePositions)
		return;

		if (Position < 0 && CloseOppositePositions)
		BuyMarket();

		BuyMarket();

		_entryPrice = entryPrice;
		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfitPrice;
		}
		else
		{
		if (Position > 0 && !CloseOppositePositions)
		return;

		if (Position > 0 && CloseOppositePositions)
		SellMarket();

		SellMarket();

		_entryPrice = entryPrice;
		_stopPrice = stopPrice;
		_takeProfitPrice = takeProfitPrice;
		}
	}

	private void HandleActivePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
		// Nothing is open, so cached targets must be cleared.
		ResetTargets();
		return;
		}

		if (Position > 0)
		{
		var stopHit = _stopPrice > 0m && candle.LowPrice <= _stopPrice;
		var takeHit = _takeProfitPrice > 0m && candle.HighPrice >= _takeProfitPrice;

		if (stopHit || takeHit)
		{
		SellMarket();
		ResetTargets();
		}
		}
		else if (Position < 0)
		{
		var stopHit = _stopPrice > 0m && candle.HighPrice >= _stopPrice;
		var takeHit = _takeProfitPrice > 0m && candle.LowPrice <= _takeProfitPrice;

		if (stopHit || takeHit)
		{
		BuyMarket();
		ResetTargets();
		}
		}
	}

	private bool IsPatternValid(CandleSnapshot first, CandleSnapshot middle, CandleSnapshot recent)
	{
		// Evening Star requires a bullish candle, a small-bodied candle, then a bearish candle.
		if (!(recent.Open > recent.Close && first.Open < first.Close))
		return false;

		if (CheckCandleSizes)
		{
		var lastBody = Math.Abs(recent.Open - recent.Close);
		var middleBody = Math.Abs(middle.Open - middle.Close);
		var firstBody = Math.Abs(first.Open - first.Close);

		if (lastBody < middleBody || firstBody < middleBody)
		return false;
		}

		if (Candle2Bullish)
		{
		if (middle.Open > middle.Close)
		return false;
		}
		else
		{
		if (middle.Close > middle.Open)
		return false;
		}

		if (ConsiderGap && _pipSize > 0m)
		{
		var gap = _pipSize;
		if (recent.Open >= middle.Close - gap || middle.Open <= first.Close + gap)
		return false;
		}

		return true;
	}

	private decimal CalculateStop(decimal entryPrice, bool isLong)
	{
		var distance = StopLossPips * _pipSize;
		if (distance <= 0m)
		return 0m;

		return isLong ? entryPrice - distance : entryPrice + distance;
	}

	private decimal CalculateTake(decimal entryPrice, bool isLong)
	{
		var distance = TakeProfitPips * _pipSize;
		if (distance <= 0m)
		return 0m;

		return isLong ? entryPrice + distance : entryPrice - distance;
	}

	private decimal CalculatePositionSize(decimal entryPrice, decimal stopPrice)
	{
		// Simplified: always return Volume (from base Strategy)
		return Volume;
	}

	private decimal CalculatePipSize()
	{
		var step = Security.PriceStep ?? 0m;
		if (step <= 0m)
		return 0m;

		var decimals = Security.Decimals;
		// Forex symbols use fractional pips; replicate the 3/5 digit adjustment from MQL.
		return decimals is 3 or 5 ? step * 10m : step;
	}

	private void TrimHistory()
	{
		// Keep only the most recent candles needed for pattern detection.
		var maxCount = Math.Max(Shift + 5, 10);
		if (_history.Count <= maxCount)
		return;

		while (_history.Count > maxCount)
			try { _history.RemoveAt(0); } catch { break; }
	}

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takeProfitPrice = 0m;
	}

	// Lightweight snapshot to keep only the data required for pattern checks.
	private readonly struct CandleSnapshot
	{
		public CandleSnapshot(decimal open, decimal close, decimal high, decimal low)
		{
			Open = open;
			Close = close;
			High = high;
			Low = low;
		}

		public decimal Open { get; }
		public decimal Close { get; }
		public decimal High { get; }
		public decimal Low { get; }
	}
}