Открыть на GitHub

Стратегия "Bullish & Bearish Engulfing"

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

Данная стратегия переносит на StockSharp эксперта MetaTrader «Bullish and Bearish Engulfing». Алгоритм анализирует завершённые свечи выбранного таймфрейма, позволяет пропускать несколько последних баров и реагирует только тогда, когда сформировавшийся паттерн «бычьего/медвежьего поглощения» удовлетворяет дополнительному фильтру по дистанции. Решение предназначено для трейдеров, которые торгуют свечные модели и хотят автоматизировать их обработку, не теряя контроля над направлением сделок, объёмом и управлением текущих позиций.

Условия распознавания паттерна

После учёта параметра смещения стратегия проверяет две последовательные завершённые свечи:

  • Бычье поглощение
    • Текущая оцениваемая свеча закрывается выше открытия (бычье тело).
    • Предыдущая свеча закрывается ниже открытия (медвежье тело).
    • Максимум текущей свечи выше предыдущего максимума, а минимум ниже предыдущего минимума как минимум на величину фильтра.
    • Закрытие бычьей свечи выше открытия предыдущей, а её открытие ниже предыдущего закрытия — также с учётом фильтра.
  • Медвежье поглощение
    • Текущая свеча закрывается ниже открытия (медвежье тело).
    • Предыдущая свеча закрывается выше открытия (бычье тело).
    • Новая свеча делает более высокий максимум, но закрывается значительно ниже предыдущего открытия и открывается выше предыдущего закрытия — все сравнения проводятся с поправкой на фильтр.
    • Минимум текущей свечи ниже предыдущего минимума не менее чем на заданную дистанцию.

Эти критерии полностью повторяют оригинальный код для MetaTrader, где требовалось, чтобы свеча-поглотитель полностью перекрывала тело предыдущей и одновременно пробивала оба экстремума. Параметр дистанции задаётся в пунктах (pips) и автоматически переводится в цену с учётом PriceStep и количества знаков у инструмента (для 5- и 3-значных валютных котировок используется множитель 10).

Логика работы

  1. Через высокоуровневый API выполняется подписка на выбранный тип свечей; обрабатываются только свечи с состоянием Finished.
  2. Ведётся небольшой буфер с OHLC-значениями, достаточный для текущего смещения.
  3. Как только в буфере есть минимум две подходящие свечи, проверяются условия бычьего и медвежьего поглощения.
  4. При бычьем сигнале создаётся рыночная заявка на сторону, указанную в параметре BullishSide; при медвежьем сигнале используется значение BearishSide.
  5. Если включён флаг CloseOppositePositions, наличие противоположной позиции приводит к увеличению объёма заявки на абсолютный размер позиции — таким образом противоположная позиция закрывается и сразу открывается новая в нужном направлении. Когда флаг выключен, сигналы игнорируются до тех пор, пока противоположная позиция не будет закрыта вручную.
  6. Объём сделки задаётся параметром Volume (по умолчанию 1). Стратегия не добавляет стоп-лосс или тейк-профит автоматически — управление рисками должно быть настроено отдельно либо через защитные модули StockSharp (например, StartProtection).

Параметры

Параметр Описание Значение по умолчанию Примечания
CandleType Тип свечей (DataType), используемый для поиска сигналов. Свечи 1 часа Можно выбрать любой поддерживаемый таймфрейм.
Shift Количество завершённых свечей, которые следует пропустить перед оценкой паттерна. 1 Значение 1 анализирует последнюю закрытую свечу; большие значения смещают анализ в прошлое.
DistanceInPips Минимальная дистанция в пунктах между свечами поглощения. 0 Переводится в цену через шаг цены инструмента; помогает отсечь свечи с маленьким телом.
CloseOppositePositions Нужно ли закрывать противоположные позиции перед входом. true При false сигнал пропускается, если открыта позиция противоположного направления.
BullishSide Направление заявки при бычьем поглощении. Buy Можно переключить на Sell для контртрендовой торговли.
BearishSide Направление заявки при медвежьем поглощении. Sell Можно переключить на Buy, чтобы торговать против тренда.
Volume Базовый объём заявки. 1 При закрытии обратной позиции к объёму добавляется abs(Position).

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

  • Сделки исполняются рыночными ордерами без встроенных защитных уровней. Рекомендуется подключать защитные модули или внешние ограничения риска.
  • В оригинальной MQL-версии объём рассчитывался через модуль Money Management. В портированной стратегии используется фиксированное значение Volume, что делает поведение предсказуемым в StockSharp; при необходимости добавьте собственный блок управления капиталом.
  • При активном параметре CloseOppositePositions разворот выполняется одной сделкой: объём равен базовому значению плюс абсолютная текущая позиция, что обеспечивает немедленный переход из старого направления в новое.

Структура файлов

  • CS/BullishBearishEngulfingStrategy.cs — C# реализация, использующая высокоуровневый API стратегий StockSharp.

Важно: Python-версия и соответствующая папка не создавались в рамках данной задачи — предоставлена только C# реализация.

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>
/// Engulfing pattern strategy that reacts to bullish and bearish engulfing candles.
/// </summary>
public class BullishBearishEngulfingStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<decimal> _distanceInPips;
	private readonly StrategyParam<bool> _closeOpposite;
	private readonly StrategyParam<Sides> _bullishSide;
	private readonly StrategyParam<Sides> _bearishSide;
	private readonly List<CandleSnapshot> _candles = new();

	/// <summary>
	/// Initializes a new instance of <see cref="BullishBearishEngulfingStrategy"/>.
	/// </summary>
	public BullishBearishEngulfingStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for analysis", "General");

		_shift = Param(nameof(Shift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Shift", "Number of completed candles to skip", "Pattern")
			
			.SetOptimize(1, 5, 1);

		_distanceInPips = Param(nameof(DistanceInPips), 0m)
			.SetNotNegative()
			.SetDisplay("Distance (pips)", "Additional filter expressed in pips", "Pattern")
			
			.SetOptimize(0m, 10m, 1m);

		_closeOpposite = Param(nameof(CloseOppositePositions), true)
			.SetDisplay("Close Opposite", "Close opposite position before entering", "Risk");

		_bullishSide = Param(nameof(BullishSide), Sides.Buy)
			.SetDisplay("Bullish Action", "Order side for bullish engulfing", "Pattern");

		_bearishSide = Param(nameof(BearishSide), Sides.Sell)
			.SetDisplay("Bearish Action", "Order side for bearish engulfing", "Pattern");
	}

	/// <summary>
	/// Candle type used for pattern detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Number of fully completed candles to skip before pattern evaluation.
	/// </summary>
	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	/// <summary>
	/// Additional price filter expressed in pips.
	/// </summary>
	public decimal DistanceInPips
	{
		get => _distanceInPips.Value;
		set => _distanceInPips.Value = value;
	}

	/// <summary>
	/// Indicates whether opposite positions should be closed before entering a new trade.
	/// </summary>
	public bool CloseOppositePositions
	{
		get => _closeOpposite.Value;
		set => _closeOpposite.Value = value;
	}

	/// <summary>
	/// Side used when a bullish engulfing pattern appears.
	/// </summary>
	public Sides BullishSide
	{
		get => _bullishSide.Value;
		set => _bullishSide.Value = value;
	}

	/// <summary>
	/// Side used when a bearish engulfing pattern appears.
	/// </summary>
	public Sides BearishSide
	{
		get => _bearishSide.Value;
		set => _bearishSide.Value = value;
	}

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

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

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

		_candles.Clear();

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

		// no protection needed
	}

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

		var snapshot = new CandleSnapshot
		{
			Open = candle.OpenPrice,
			High = candle.HighPrice,
			Low = candle.LowPrice,
			Close = candle.ClosePrice
		};

		_candles.Add(snapshot);

		var maxCount = Math.Max(Shift + 2, 3);
		while (_candles.Count > maxCount)
			try { _candles.RemoveAt(0); } catch { break; }

		if (_candles.Count < Shift + 1)
			return;

		var candles = _candles.ToArray();
		var currentIndex = candles.Length - Shift;
		if (currentIndex <= 0)
			return;

		var previousIndex = currentIndex - 1;
		if (previousIndex < 0)
			return;

		var current = candles[currentIndex];
		var previous = candles[previousIndex];
		var distance = CalculateDistanceInPrice();

		var isBullishEngulfing = current.Close > current.Open && previous.Open > previous.Close &&
			current.High > previous.High + distance &&
			current.Close > previous.Open + distance &&
			current.Open < previous.Close - distance &&
			current.Low < previous.Low - distance;

		if (isBullishEngulfing)
		{
			HandleSignal(BullishSide);
			return;
		}

		var isBearishEngulfing = current.Open > current.Close && previous.Open < previous.Close &&
			current.High > previous.High + distance &&
			current.Open > previous.Close + distance &&
			current.Close < previous.Open - distance &&
			current.Low < previous.Low - distance;

		if (isBearishEngulfing)
			HandleSignal(BearishSide);
	}

	private void HandleSignal(Sides side)
	{
		switch (side)
		{
			case Sides.Buy:
				EnterLong();
				break;
			case Sides.Sell:
				EnterShort();
				break;
		}
	}

	private void EnterLong()
	{
		if (Position > 0)
			return;

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

			// Close short first
			BuyMarket();
		}

		BuyMarket();
	}

	private void EnterShort()
	{
		if (Position < 0)
			return;

		if (Position > 0)
		{
			if (!CloseOppositePositions)
				return;

			// Close long first
			SellMarket();
		}

		SellMarket();
	}

	private decimal CalculateDistanceInPrice()
	{
		var priceStep = Security?.PriceStep;
		if (priceStep == null)
			return 0m;

		var decimals = Security?.Decimals ?? 0;
		var multiplier = decimals is 3 or 5 ? 10m : 1m;
		return DistanceInPips * priceStep.Value * multiplier;
	}

	private struct CandleSnapshot
	{
		public decimal Open;
		public decimal High;
		public decimal Low;
		public decimal Close;
	}
}