Открыть на GitHub

Стратегия «Three Candles Reversal»

Данная стратегия является переносом советника Exp_ThreeCandles из MQL5 в инфраструктуру StockSharp. Она ищет классический разворот из трёх свечей:

  1. Две подряд свечи в одном направлении.
  2. Третья свеча меняет направление и закрывается за ключевым уровнем предыдущей свечи.
  3. При необходимости проверяется подтверждение объёмом, если диапазон самой ранней свечи не слишком велик.

Появление бычьей конфигурации закрывает короткие позиции (если это разрешено) и может открыть длинную. Медвежий сигнал выполняет зеркальные действия. Стоп-лосс и тейк-профит рассчитываются в шагах цены (PriceStep).

Определение паттерна

Стратегия хранит скользящее окно из SignalBar + 3 завершённых свечей. При закрытии новой свечи проверяется свеча с индексом SignalBar (по умолчанию — одна свеча назад) и три более старых бара:

  • Бычий разворот (возможная покупка):
    • Две более старые свечи (SignalBar + 3 и SignalBar + 2) медвежьи.
    • Средняя свеча закрывается выше минимума самой ранней свечи.
    • Предыдущая свеча (SignalBar + 1) бычья и закрывается выше открытия средней свечи.
  • Медвежий разворот (возможная продажа):
    • Условия зеркально повторяют бычий сценарий.

Фильтр объёма полностью повторяет оригинальный индикатор. Если диапазон самой ранней свечи (переведённый в шаги цены) превышает MaxBarSize либо VolumeFilter установлен в None, фильтр пропускается. В остальных случаях выполняется одно из условий: объём самой ранней свечи < объёма средней ИЛИ объём последней свечи > объёма средней ИЛИ объём последней свечи > объёма самой ранней. В StockSharp для свечей доступен агрегированный объём, поэтому режимы Tick и Real используют одно и то же поле TotalVolume.

Управление сделками

  • При активном AllowSellExit бычий сигнал сначала закрывает шорт, затем, если AllowBuyEntry разрешён и позиция нулевая, открывает лонг. Параметр AllowBuyExit для медвежьего сигнала работает аналогично.
  • Новая позиция открывается только при нулевой текущей позиции и разрешённом флаге Allow*Entry. Размер заявки определяется стандартными настройками стратегии.
  • Стоп-лосс и тейк-профит (StopLossPips, TakeProfitPips) задаются в шагах цены и контролируются на каждой завершённой свече.
  • Для защиты от повторных действий в рамках одной свечи сохраняется время последнего обработанного сигнала.

Параметры

Параметр Значение по умолчанию Описание
CandleType 4‑часовые свечи Тип свечей, по которым строится паттерн.
SignalBar 1 Смещение по истории для оценки сигнала (≥ 0).
MaxBarSize 300 Если диапазон самой ранней свечи (в шагах цены) превышает порог, фильтр объёма отключается. Значение 0 полностью отключает фильтр.
VolumeFilter Tick Режим работы фильтра (Tick, Real или None). В первых двух случаях используется TotalVolume.
AllowBuyEntry true Разрешить открытие длинных позиций по бычьему сигналу.
AllowSellEntry true Разрешить открытие коротких позиций по медвежьему сигналу.
AllowBuyExit true Разрешить закрытие лонгов по медвежьему сигналу.
AllowSellExit true Разрешить закрытие шортов по бычьему сигналу.
StopLossPips 1000 Дистанция стоп-лосса в шагах цены (0 — нет стопа).
TakeProfitPips 2000 Дистанция тейк-профита в шагах цены (0 — нет тейк-профита).

Особенности переноса

  • Модуль управления капиталом из TradeAlgorithms.mqh заменён на прямые вызовы BuyMarket/SellMarket, поэтому объём позиций определяется настройками StockSharp.
  • Сигнал формируется на свече с заданным смещением SignalBar, как и в оригинале; время последнего сигнала используется для предотвращения повторной обработки.
  • Функции звуковых, почтовых и push-уведомлений из индикатора не портировались.
  • Настройки Tick и Real сохранены ради совместимости, но фактически используют одинаковый объём из свечи.
  • Комментарии и документация переведены на английский язык во всех кодовых файлах, согласно правилам репозитория.

Стратегия воспроизводит поведение оригинала и при этом полностью укладывается в высокоуровневую модель подписки 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);
}