Открыть на GitHub

Стратегия Rectangle Test

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

Стратегия Rectangle Test переносит эксперта MetaTrader «RectangleTest» на высокоуровневый API StockSharp. Она выявляет консолидации в виде прямоугольника на выбранном таймфрейме, проверяет, что две скользящие средние и текущая цена остаются внутри диапазона, и открывает сделки при выходе из боковика по направлению быстрой EMA. Вся логика рассчитывается на закрытых свечах из настраиваемого источника.

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

  1. Подписка на основной поток свечей (по умолчанию часовые) и передача их в индикаторы:
    • ExponentialMovingAverage (EMA) с периодом EmaPeriod.
    • SimpleMovingAverage (SMA) с периодом SmaPeriod.
    • Highest и Lowest длиной RangeCandles, настроенные на чтение максимумов и минимумов свечей. Они формируют верхнюю и нижнюю границы прямоугольника и повторяют массивные расчёты из MQL.
  2. После формирования индикаторов вычисляется высота прямоугольника в процентах относительно верхней границы. Только случаи, когда высота меньше RectangleSizePercent, считаются допустимыми консолидациями.
  3. Дополнительно требуется, чтобы EMA, SMA и цена закрытия находились внутри диапазона — это реализует фильтр «бокового рынка» из оригинального эксперта.
  4. Условия для продажи:
    • EMA располагается выше SMA.
    • Цена закрытия выше EMA (аналог условия Ask > EMA в MetaTrader).
    • Если открыта длинная позиция, она закрывается перед открытием шорта.
  5. Условия для покупки:
    • EMA ниже SMA.
    • Цена закрытия ниже EMA (Bid < EMA в исходном коде).
    • Перед открытием лонга закрываются активные короткие позиции.
  6. При каждом входе запоминаются ожидаемая цена и объём. Когда позиция возвращается к нулю, стратегия сравнивает цену выхода с ценой входа; убыточные сделки увеличивают дневной счётчик и блокируют новые сигналы после достижения MaxLosingTradesPerDay, что повторяет функцию Loss() из MQL.

Управление рисками и объёмом

  • Доступны два режима:
    • Риск-менеджмент (UseRiskMoneyManagement = true): объём рассчитывается по балансу портфеля, параметру RiskPercent и величине стоп-лосса StopLossPoints. Используются свойства инструмента PriceStep, StepPrice и VolumeStep, что повторяет лот-менеджмент MetaTrader.
    • Фиксированный объём (UseRiskMoneyManagement = false): заявки отправляются объёмом FixedVolume.
  • После перехода из нулевой позиции в открытую вызываются SetStopLoss и SetTakeProfit, которые выставляют защитные ордера на расстоянии StopLossPoints и TakeProfitPoints (в шагах цены), аналогично передаче SL/TP в m_trade.Sell/Buy.
  • Параметр MaxLosingTradesPerDay запрещает новые входы после заданного числа убыточных сделок в текущей дате.

Временные фильтры

  • Торговля разрешена только в интервале TradeStartTimeTradeEndTime. Помощник корректно обрабатывает как дневные интервалы, так и окна через полночь.
  • Если EnableTimeClose = true, все позиции закрываются после наступления TimeClose, что соответствует входным параметрам TimeCloseTrue и TimeClose в MetaTrader.

Отличия от версии MetaTrader

  • Исходный советник рисовал прямоугольники на графике. В StockSharp расчёт диапазона выполняется индикаторами Highest/Lowest без создания графических объектов.
  • Учёт убыточных сделок выполняется по ценам закрытия свечей, что передаёт смысл функции Loss() (количество убыточных сделок в день), оставаясь в рамках высокоуровневого API.
  • Настройки типов исполнения ордеров (FOK/IOC) предоставляет инфраструктура StockSharp, поэтому дополнительной логики не требуется.

Параметры

Параметр Значение по умолчанию Описание
EmaPeriod 45 Период быстрой EMA.
SmaPeriod 200 Период медленной SMA.
RangeCandles 10 Количество свечей в прямоугольнике.
RectangleSizePercent 0.5 Максимальная высота прямоугольника (в процентах).
StopLossPoints 250 Стоп-лосс в шагах цены.
TakeProfitPoints 750 Тейк-профит в шагах цены.
UseRiskMoneyManagement true Переключение между риск- и фиксированным объёмом.
RiskPercent 1 Процент капитала в риске на сделку.
FixedVolume 1 Объём сделок в фиксированном режиме.
MaxLosingTradesPerDay 1 Максимум убыточных сделок в день.
TradeStartTime 03:00 Время начала торговли.
TradeEndTime 22:50 Время окончания генерации новых сигналов.
EnableTimeClose false Включение закрытия по времени.
TimeClose 23:00 Время принудительного закрытия позиций.
CandleType часовые свечи Основной источник свечей.

Визуализация

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

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Rectangle breakout strategy: detects tight consolidation ranges and trades breakouts
/// using EMA/SMA trend direction as a filter.
/// </summary>
public class RectangleTestStrategy : Strategy
{
	private readonly StrategyParam<int> _emaPeriod;
	private readonly StrategyParam<int> _smaPeriod;
	private readonly StrategyParam<int> _rangeCandles;
	private readonly StrategyParam<decimal> _rectangleSizePercent;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _highs = new();
	private readonly List<decimal> _lows = new();

	public int EmaPeriod
	{
		get => _emaPeriod.Value;
		set => _emaPeriod.Value = value;
	}

	public int SmaPeriod
	{
		get => _smaPeriod.Value;
		set => _smaPeriod.Value = value;
	}

	public int RangeCandles
	{
		get => _rangeCandles.Value;
		set => _rangeCandles.Value = value;
	}

	public decimal RectangleSizePercent
	{
		get => _rectangleSizePercent.Value;
		set => _rectangleSizePercent.Value = value;
	}

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

	public RectangleTestStrategy()
	{
		_emaPeriod = Param(nameof(EmaPeriod), 20)
			.SetDisplay("Fast EMA Period", "Length of the fast EMA", "Indicators");

		_smaPeriod = Param(nameof(SmaPeriod), 50)
			.SetDisplay("Slow SMA Period", "Length of the slow SMA", "Indicators");

		_rangeCandles = Param(nameof(RangeCandles), 10)
			.SetDisplay("Rectangle Candles", "Number of candles for range detection", "Logic");

		_rectangleSizePercent = Param(nameof(RectangleSizePercent), 10m)
			.SetDisplay("Rectangle Size (%)", "Maximum range height in percent", "Logic");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle source", "General");
	}

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

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

		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var sma = new SimpleMovingAverage { Length = SmaPeriod };

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

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent));

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawIndicator(area, sma);
			DrawOwnTrades(area);
		}
	}

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

		if (_highs.Count >= RangeCandles)
		{
			// Use PREVIOUS window (excluding current candle) for rectangle detection
			var highestValue = decimal.MinValue;
			var lowestValue = decimal.MaxValue;
			var startIdx = _highs.Count - RangeCandles;
			for (var i = startIdx; i < _highs.Count; i++)
			{
				if (_highs[i] > highestValue) highestValue = _highs[i];
				if (_lows[i] < lowestValue) lowestValue = _lows[i];
			}

			if (highestValue > 0m && lowestValue > 0m)
			{
				var rangePercent = (highestValue - lowestValue) / highestValue * 100m;
				if (rangePercent > 0 && rangePercent < RectangleSizePercent)
				{
					var close = candle.ClosePrice;

					// Breakout above rectangle with bullish trend (EMA > SMA)
					if (close > highestValue && emaValue > smaValue && Position == 0)
					{
						BuyMarket();
					}
					// Breakout below rectangle with bearish trend (EMA < SMA)
					else if (close < lowestValue && emaValue < smaValue && Position == 0)
					{
						SellMarket();
					}
				}
			}
		}

		_highs.Add(candle.HighPrice);
		_lows.Add(candle.LowPrice);

		if (_highs.Count > RangeCandles + 1)
		{
			_highs.RemoveAt(0);
			_lows.RemoveAt(0);
		}
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_highs.Clear();
		_lows.Clear();

		base.OnReseted();
	}
}