Открыть на GitHub

Стратегия Pipso Range Reversal

Эта стратегия — портирование советника Pipso с платформы MQL5 в StockSharp. Алгоритм работает как контртрендовая система: продаёт на пробое верхней границы недавнего диапазона и покупает на пробое нижней границы, ограничивая активность заданным торговым окном.

Основная идея

  • Формируется канал в стиле Дончиана из максимума и минимума последних LookbackPeriod завершённых свечей (по умолчанию 36).
  • Верхняя граница используется для продажи против роста, нижняя — для покупок против падения.
  • Сделки разрешены только тогда, когда текущая свеча стартует внутри торгового интервала, заданного параметрами StartHour и EndHour.

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

Условия входа

  • Короткая позиция: если максимум свечи достигает либо превышает предыдущий максимум канала, сначала закрывается длинная позиция, после чего (при активном торговом окне) открывается шорт объёмом OrderVolume по рынку. Цена входа фиксируется равной максимуму канала.
  • Длинная позиция: если минимум свечи касается либо пробивает предыдущий минимум канала, закрывается существующий шорт и, при разрешении торгов, открывается лонг объёмом OrderVolume по рынку с ценой входа на уровне минимума канала.

Условия выхода

  • Позиция закрывается немедленно, когда цена касается противоположной границы канала — это повторяет поведение оригинального советника.
  • Дополнительно выставляется защитный стоп на фиксированном расстоянии от цены входа. Расстояние рассчитывается как (channelHigh - channelLow) * (1 + StopRangePercent / 100); при стандартном значении StopRangePercent = 300 стоп отстоит на четыре ширины канала.
  • Контроль стопа выполняется по экстремумам свечи: длинная позиция закрывается, если минимум свечи упал ниже стопа; короткая — если максимум поднялся выше стопа.

Торговое окно

  • Параметры StartHour и EndHour задаются во времени площадки. Если StartHour < EndHour, сделки выполняются только внутри этого дневного интервала. Если StartHour > EndHour, окно «заворачивается» через полночь, что позволяет торговать ночные сессии (например, с 21 до 9 часов).
  • Если параметры совпадают (StartHour == EndHour), стратегия остаётся вне рынка.

Параметры

  • OrderVolume (по умолчанию 0.1) — торговый объём каждой заявки.
  • LookbackPeriod (по умолчанию 36) — количество свечей для расчёта канала.
  • StartHour (по умолчанию 21) — час (0–23), когда открывается торговое окно.
  • EndHour (по умолчанию 9) — час (0–23), когда окно закрывается.
  • StopRangePercent (по умолчанию 300) — дополнительный процент к ширине канала при вычислении дистанции стопа.
  • CandleType (по умолчанию часовые свечи) — таймфрейм для анализа.

Индикаторы и данные

  • Используются индикаторы Highest и Lowest из StockSharp для отслеживания границ канала.
  • Подходит для любых инструментов, по которым доступен непрерывный поток свечей выбранного таймфрейма.
  • Оригинальный советник ориентируется на таймфрейм графика; параметр CandleType позволяет воспроизвести эти условия.

Примечания

  • Логика работает с завершёнными свечами, чтобы избежать внутридневного шума; в реальном времени цены входа и выхода аппроксимируют тиковое поведение версии MQL5.
  • Тейк-профит не используется: фиксация прибыли происходит при возврате цены к противоположной границе или по стопу.
  • Рекомендуется адаптировать торговое окно, длину канала и множитель стопа под волатильность конкретного инструмента.
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>
/// Range-reversal strategy translated from the Pipso MQL5 expert advisor.
/// The system fades breakouts of the recent high/low range during a configurable trading session.
/// </summary>
public class PipsoStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _lookbackPeriod;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _stopRangePercent;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highest = null!;
	private Lowest _lowest = null!;
	private decimal _previousHighest;
	private decimal _previousLowest;
	private bool _isChannelInitialized;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private Sides? _entrySide;

	/// <summary>
	/// Trade volume expressed in lots or contracts.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Number of candles used to compute the high/low channel.
	/// </summary>
	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}

	/// <summary>
	/// Hour when the strategy is allowed to start trading.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when trading should stop.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the channel width to compute the stop distance.
	/// </summary>
	public decimal StopRangePercent
	{
		get => _stopRangePercent.Value;
		set => _stopRangePercent.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public PipsoStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume per trade", "General");

		_lookbackPeriod = Param(nameof(LookbackPeriod), 36)
			.SetGreaterThanZero()
			.SetDisplay("Lookback Period", "Number of candles used for high/low extremes", "Channel");

		_startHour = Param(nameof(StartHour), 21)
			.SetDisplay("Start Hour", "Session start hour (0-23)", "Session");

		_endHour = Param(nameof(EndHour), 9)
			.SetDisplay("End Hour", "Session end hour (0-23)", "Session");

		_stopRangePercent = Param(nameof(StopRangePercent), 300m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Range %", "Extra percentage of the channel width for stop distance", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for calculations", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousHighest = 0m;
		_previousLowest = 0m;
		_isChannelInitialized = false;
		ResetTradeState();
	}

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

		Volume = OrderVolume;

		_highest = new Highest { Length = LookbackPeriod };
		_lowest = new Lowest { Length = LookbackPeriod };

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

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

		if (!_highest.IsFormed || !_lowest.IsFormed)
		{
			_previousHighest = highestValue;
			_previousLowest = lowestValue;
			return;
		}

		if (!_isChannelInitialized)
		{
			_previousHighest = highestValue;
			_previousLowest = lowestValue;
			_isChannelInitialized = true;
			return;
		}

		// Indicators are bound via .Bind, no need for IsFormedAndOnlineAndAllowTrading.

		var channelHigh = _previousHighest;
		var channelLow = _previousLowest;

		ManageStopLoss(candle);

		var channelRange = channelHigh - channelLow;
		var breakoutHigh = candle.HighPrice >= channelHigh && channelRange > 0m;
		var breakoutLow = candle.LowPrice <= channelLow && channelRange > 0m;
		var canTrade = IsWithinTradingWindow(candle.OpenTime);

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

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

		if (channelRange > 0m)
		{
			var stopDistance = channelRange * (1m + StopRangePercent / 100m);

			if (breakoutHigh && Position == 0 && canTrade)
			{
				SellMarket();
				_entrySide = Sides.Sell;
				_entryPrice = channelHigh;
				_stopPrice = _entryPrice + stopDistance;
			}
			else if (breakoutLow && Position == 0 && canTrade)
			{
				BuyMarket();
				_entrySide = Sides.Buy;
				_entryPrice = channelLow;
				_stopPrice = _entryPrice - stopDistance;
			}
		}

		if (Position == 0)
			ResetTradeState();

		_previousHighest = highestValue;
		_previousLowest = lowestValue;
	}

	private void ManageStopLoss(ICandleMessage candle)
	{
		if (_entrySide is null || _stopPrice is null)
			return;

		if (_entrySide == Sides.Buy)
		{
			if (Position <= 0)
			{
				ResetTradeState();
				return;
			}

			if (candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket();
				ResetTradeState();
			}
		}
		else if (_entrySide == Sides.Sell)
		{
			if (Position >= 0)
			{
				ResetTradeState();
				return;
			}

			if (candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket();
				ResetTradeState();
			}
		}
	}

	private bool IsWithinTradingWindow(DateTimeOffset time)
	{
		var normalizedStart = ((StartHour % 24) + 24) % 24;
		var normalizedEnd = ((EndHour % 24) + 24) % 24;

		if (normalizedStart == normalizedEnd)
			return false;

		var start = new TimeSpan(normalizedStart, 0, 0);
		var end = new TimeSpan(normalizedEnd, 0, 0);
		var current = time.TimeOfDay;

		return normalizedStart < normalizedEnd
			? current >= start && current <= end
			: current >= start || current <= end;
	}

	private void ResetTradeState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_entrySide = null;
	}
}