Открыть на GitHub

Стратегия Evening Star Reversal

Общее описание

Стратегия является прямой конверсией советника EveningStar.mq5 (MQL5 id 18507). Алгоритм отслеживает классическую свечную модель "вечерняя звезда" и открывает сделку сразу после появления новой свечи. Вся логика переписана под высокоуровневый API StockSharp с сохранением исходных фильтров и управления рисками.

Логика торговли

  1. Подписка выполняется на таймфрейм, выбранный параметром CandleType. Обработка ведётся только по закрытым свечам.
  2. После закрытия каждой свечи сохраняется локальное окно, чтобы можно было проанализировать три свечи согласно сдвигу Shift.
  3. Модель считается сформированной, если соблюдаются условия:
    • Свеча N-2 (самая старая) бычья (open < close).
    • Свеча N-1 (средняя) соответствует параметру Candle2Bullish (по умолчанию тоже бычья).
    • Свеча N (самая новая) медвежья (open > close).
    • При включённом CheckCandleSizes тело средней свечи должно быть самым маленьким из трёх.
    • При включённом ConsiderGap между телами свечей обязателен разрыв – расстояние такое же, как в оригинальном роботе (один пункт, рассчитанный по шагу цены инструмента).
  4. После подтверждения модели проверяется выбранное направление Direction:
    • Значение Short (по умолчанию) открывает короткую позицию, что соответствует классической интерпретации вечерней звезды.
    • Значение Long позволяет торговать противоположную сторону (опция сохранена для функционального соответствия MQL-версии).
  5. Перед открытием позиции стратегия при необходимости закрывает встречную позицию, если CloseOppositePositions = true.
  6. Stop-loss и take-profit рассчитываются по расстояниям в пунктах (StopLossPips, TakeProfitPips) с тем же корректирующим коэффициентом для инструментов с 3/5 знаками после запятой.
  7. Объём позиции определяется из текущей стоимости портфеля и параметра RiskPercent. Если рассчитанный объём меньше минимально допустимого, сигнал пропускается.

Управление позицией

  • При открытой длинной позиции каждая новая свеча проверяется: если минимум пробивает стоп или максимум достигает цели, вся позиция закрывается по рынку.
  • Для короткой позиции действует тот же принцип, но с зеркальными условиями.
  • Если стоимость портфеля или расстояние до стопа равны нулю, объём рассчитать невозможно и сделка игнорируется.

Параметры

Имя Значение по умолчанию Описание
Direction Short Выбор направления сделки при появлении модели.
TakeProfitPips 150 Расстояние до цели в пунктах. Ноль отключает take-profit.
StopLossPips 50 Расстояние до защитного стопа в пунктах. Неположительное значение блокирует вход.
RiskPercent 5 Доля капитала, которая рискуется в одной сделке, используется при расчёте объёма.
Shift 1 Количество баров, пропускаемых от последней свечи перед проверкой модели.
ConsiderGap true Требует ценовой разрыв между свечами, как в оригинальном советнике.
Candle2Bullish true Требует бычью среднюю свечу. При отключении ожидается медвежья свеча.
CheckCandleSizes true Контролирует, что средняя свеча имеет минимальное тело.
CloseOppositePositions true Закрывает противоположную позицию перед открытием новой.
CandleType таймфрейм 1H Свечной источник данных для анализа.

Примечания

  • Размер пункта вычисляется из шага цены. Для инструментов с 3 или 5 знаками после запятой один пункт равен десяти шагам цены, что повторяет поведение MQL5.
  • Если 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; }
	}
}