Открыть на GitHub

Стратегия Rubber Bands Grid

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

  • Конверсия советника MetaTrader 4 RUBBERBANDS_2.mq4.
  • Строит симметричную сетку вокруг текущей цены, используя лучшие bid/ask котировки вместо свечей.
  • Ведёт раздельный учёт длинных и коротких позиций, чтобы повторить хеджирующее поведение оригинального робота.
  • Поддерживает сессионные тейк-профит и стоп-лосс, а также ручные флаги паузы/остановки, совпадающие с параметрами MQL.

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

  1. Стратегия подписывается на SubscribeLevel1() и реагирует на каждое изменение лучших bid/ask цен.
  2. Две плавающие экстремумы (_upperExtreme и _lowerExtreme) фиксируют максимальную и минимальную цену Ask с момента последнего сброса. При включённом UseInitialValues значения берутся из параметров, иначе используются первые полученные данные.
  3. Если открытых сделок нет и время сервера попадает на первую секунду минуты, стратегия выставляет рыночный buy и рыночный sell. Это повторяет логику MT4, где флаги buy/sell активируются каждую минуту при пустом портфеле.
  4. При росте Ask на GridStepPoints пунктов выше зафиксированного максимума отправляется новая продажа. При падении Ask на такое же расстояние ниже минимума размещается новая покупка. После каждого срабатывания экстремумы обновляются, поэтому сетка «растягивается» вместе с ценой.
  5. Общее число одновременно открытых сделок (длинных плюс коротких) ограничивается параметром MaxTrades.
  6. Текущая плавающая прибыль считается по bid/ask: для лонга используется Bid минус средняя цена покупки, для шорта — средняя цена продажи минус Ask. Помощник PriceToMoney переводит разницу цены в валюту счёта с учётом PriceStep/StepPrice.
  7. Если прибыль достигает SessionTakeProfitPerLot * OrderVolume и включён UseSessionTakeProfit, стратегия закрывает все позиции. Аналогично убыток ниже -SessionStopLossPerLot * OrderVolume приводит к полному выходу при включённом UseSessionStopLoss.
  8. Ручные флаги повторяют оригинальные опции: CloseNow закрывает позиции при старте, QuiesceMode переводит стратегию в ожидание при отсутствии сделок, StopNow запрещает новые входы без вмешательства в текущие позиции.

Параметры

Параметр Описание
OrderVolume Объём каждой рыночной заявки (аналог Lots).
MaxTrades Максимальное количество одновременно открытых ордеров (maxcount).
GridStepPoints Расстояние в пунктах между уровнями сетки (pipstep).
QuiesceMode Пауза при пустом портфеле (quiescenow).
TriggerImmediateEntries Немедленно открыть buy и sell после старта (donow).
StopNow Остановить автоматические входы, сохранив текущие позиции (stopnow).
CloseNow Принудительно закрыть все позиции при запуске (closenow).
UseSessionTakeProfit, SessionTakeProfitPerLot Сессионный тейк-профит в пересчёте на один лот.
UseSessionStopLoss, SessionStopLossPerLot Сессионный стоп-лосс в пересчёте на один лот.
UseInitialValues, InitialMax, InitialMin Поддержка перезапуска с восстановлением экстремумов (useinvalues, inmax, inmin).

Особенности реализации

  • Вся логика построена на полях класса и табулированных отступах согласно правилам репозитория.
  • Дублирование заявок предотвращается хранением ссылок на активные рыночные ордера _activeBuyOrder и _activeSellOrder.
  • Учёт длинных и коротких позиций выполняется в OnOwnTradeReceived, где пересчитываются средние цены и объёмы каждой стороны.
  • Метод TryCloseAll() повторяет MT4-функцию close1by1(): выставляет встречные ордера до полной ликвидации и затем сбрасывает экстремумы к последнему Ask.
  • Используется только высокоуровневый API (SubscribeLevel1(), BuyMarket, SellMarket), индикаторы и «ручные» коллекции не применяются.
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Rubber Bands Grid: Mean reversion grid using SMA+ATR bands.
/// </summary>
public class RubberBandsGridStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _smaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _entryPrice;
	private int _gridCount;

	public RubberBandsGridStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_smaLength = Param(nameof(SmaLength), 20)
			.SetDisplay("SMA Length", "SMA period.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

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

	public int SmaLength
	{
		get => _smaLength.Value;
		set => _smaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

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

		_entryPrice = 0;
		_gridCount = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_entryPrice = 0;
		_gridCount = 0;

		var sma = new SimpleMovingAverage { Length = SmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

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

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

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

		if (atrVal <= 0)
			return;

		var close = candle.ClosePrice;
		var lower = smaVal - atrVal * 2m;
		var upper = smaVal + atrVal * 2m;

		if (Position > 0)
		{
			if (close >= smaVal)
			{
				SellMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (close <= _entryPrice - atrVal * 4m)
			{
				SellMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (_gridCount < 3 && close <= _entryPrice - atrVal)
			{
				_entryPrice = (_entryPrice + close) / 2m;
				_gridCount++;
				BuyMarket();
			}
		}
		else if (Position < 0)
		{
			if (close <= smaVal)
			{
				BuyMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (close >= _entryPrice + atrVal * 4m)
			{
				BuyMarket();
				_entryPrice = 0;
				_gridCount = 0;
			}
			else if (_gridCount < 3 && close >= _entryPrice + atrVal)
			{
				_entryPrice = (_entryPrice + close) / 2m;
				_gridCount++;
				SellMarket();
			}
		}

		if (Position == 0)
		{
			if (close <= lower)
			{
				_entryPrice = close;
				_gridCount = 0;
				BuyMarket();
			}
			else if (close >= upper)
			{
				_entryPrice = close;
				_gridCount = 0;
				SellMarket();
			}
		}
	}
}