Открыть на GitHub

Стратегия Bollinger Bands N Positions

Обзор

Эта стратегия представляет собой перенос эксперта MetaTrader Bollinger Bands N positions на платформу StockSharp. Алгоритм анализирует закрытие свечей относительно полос Боллинджера и открывает сделки, когда цена завершает бар за пределами канала. Для повторения поведения оригинального эксперта реализованы ограничение суммарной позиции, фиксированные стоп-лосс и тейк-профит, а также ступенчатое сопровождение прибыли при помощи трейлинг-стопа.

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

  1. Подписаться на выбранный тип свечей и рассчитать полосы Боллинджера с заданными периодом и коэффициентом ширины.
  2. На каждой завершённой свече в первую очередь выполняется проверка открытой позиции:
    • Лонг закрывается при срабатывании фиксированного стоп-лосса, тейк-профита или при пробое текущего уровня трейлинг-стопа.
    • Шорт обрабатывается зеркально.
  3. Если выход по текущему бару не произошёл и торговля разрешена, оцениваются сигналы на вход:
    • При закрытии выше верхней полосы стратегия закрывает противоположные (шортовые) позиции и при соблюдении лимита по размеру открывает новый лонг требуемым объёмом.
    • При закрытии ниже нижней полосы аналогичным образом закрываются лонги и открывается шорт.
  4. Трейлинг-стоп активируется, когда прибыль превышает сумму «дистанция трейлинга + шаг». Уровень стопа удерживается на расстоянии, равном дистанции трейлинга, и смещается только при росте прибыли минимум на один шаг.

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

  • Параметр Max Positions задаёт максимальную величину чистой позиции в величинах MaxPositions × Volume. StockSharp работает в режиме неттинга, поэтому одновременно возможно держать только одну совокупную позицию. Ограничение предотвращает открытие новой сделки, если текущая абсолютная позиция уже достигла допустимого порога.
  • Стоп-лосс и тейк-профит задаются в пунктах (pip) и переводятся в цену через PriceStep инструмента. При торговле инструментами с дробным пунктом следует скорректировать значения.
  • Для включения трейлинг-стопа необходимо, чтобы и дистанция, и шаг были больше нуля. Значение 0 отключает модуль сопровождения прибыли.

Параметры

Параметр Описание Значение по умолчанию
Volume Объём заявки в лотах. 0.1
MaxPositions Максимально допустимый чистый объём позиции в кратных Volume. 9
BollingerPeriod Период скользящей средней полос Боллинджера. 20
BollingerWidth Множитель стандартного отклонения. 2
StopLossPips Дистанция стоп-лосса в пунктах. 50
TakeProfitPips Дистанция тейк-профита в пунктах. 50
TrailingStopPips Дистанция трейлинг-стопа в пунктах (0 — отключить). 5
TrailingStepPips Минимальное увеличение прибыли для переноса трейлинг-стопа. 5
CandleType Тип свечей для расчёта индикаторов. таймфрейм 1 минута

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

  • В MetaTrader эксперт работает в режиме хеджирования и может держать встречные позиции. В переносе на StockSharp используются неттинговые позиции, поэтому перед открытием новой сделки противоположное плечо закрывается, а MaxPositions ограничивает абсолютную величину нетто-позиции.
  • Стоп-заявки реализованы внутри стратегии, а не выставляются на сервер. Это соответствует оригинальному трейлинг-стопу, но закрытие произойдёт на следующей завершённой свече.
  • При включенном трейлинг-стопе и нулевом шаге стратегия остановится на этапе запуска с сообщением об ошибке, что повторяет проверку параметров в исходном эксперте.

Рекомендации по использованию

  1. Настройте Volume, MaxPositions и рисковые параметры в соответствии с лотностью и стоимостью шага цены выбранного инструмента.
  2. Убедитесь, что у инструмента задан корректный PriceStep. Если значение равно нулю, стратегия использует единицу, что подходит не для всех рынков.
  3. Позвольте индикатору прогреться (по крайней мере на длину периода Боллинджера) перед запуском торговли, чтобы исключить сигналы на неполных данных.
  4. После изменения настроек трейлинг-стопа проверяйте журнал сообщений: стратегия предупредит о некорректных сочетаниях дистанции и шага.
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>
/// Bollinger Bands breakout strategy translated from the MQL5 version with N-position control.
/// Opens positions when price closes outside the Bollinger envelope and manages exits via fixed and trailing stops.
/// </summary>
public class BollingerBandsNPositionsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _volumeTolerance;

	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<int> _bollingerPeriod;
	private readonly StrategyParam<decimal> _bollingerWidth;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;

	/// <summary>
	/// Maximum allowed net position expressed as multiples of <see cref="Volume"/>.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Bollinger Bands period.
	/// </summary>
	public int BollingerPeriod
	{
		get => _bollingerPeriod.Value;
		set => _bollingerPeriod.Value = value;
	}

	/// <summary>
	/// Bollinger Bands width multiplier.
	/// </summary>
	public decimal BollingerWidth
	{
		get => _bollingerWidth.Value;
		set => _bollingerWidth.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing-stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing-step increment in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Net position magnitude treated as flat.
	/// </summary>
	public decimal VolumeTolerance
	{
		get => _volumeTolerance.Value;
		set => _volumeTolerance.Value = value;
	}

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

	/// <summary>
	/// Initializes <see cref="BollingerBandsNPositionsStrategy"/>.
	/// </summary>
	public BollingerBandsNPositionsStrategy()
	{
		_maxPositions = Param(nameof(MaxPositions), 9)
		.SetGreaterThanZero()
		.SetDisplay("Max Positions", "Net position limit in multiples of Volume", "Risk");

		_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Period", "Moving average length", "Indicators");

		_bollingerWidth = Param(nameof(BollingerWidth), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Width", "Standard deviation multiplier", "Indicators");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Step (pips)", "Trailing adjustment increment", "Risk");

		_volumeTolerance = Param(nameof(VolumeTolerance), 0.00000001m)
		.SetNotNegative()
		.SetDisplay("Volume Tolerance", "Minimum net position magnitude treated as flat", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Source candles", "General");
	}

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

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

		ResetLongState();
		ResetShortState();
	}

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

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
		throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");

		var bollinger = new BollingerBands
		{
			Length = BollingerPeriod,
			Width = BollingerWidth
		};

		var subscription = SubscribeCandles(CandleType);
		subscription.BindEx(bollinger, ProcessCandle).Start();
	}

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

		var bb = bbValue as IBollingerBandsValue;
		var upper = bb?.UpBand ?? 0m;
		var lower = bb?.LowBand ?? 0m;

		if (HandleActivePosition(candle))
		return;

		if (!IsFormed)
		return;

		if (TryEnterLong(candle, upper))
		return;

		TryEnterShort(candle, lower);
	}

	private bool HandleActivePosition(ICandleMessage candle)
	{
		if (Position > VolumeTolerance)
		return ManageLong(candle);

		if (Position < -VolumeTolerance)
		return ManageShort(candle);

		if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
		{
			ResetLongState();
			ResetShortState();
		}

		return false;
	}

	private bool ManageLong(ICandleMessage candle)
	{
		if (_longEntryPrice is null)
		_longEntryPrice = candle.ClosePrice;

		var entry = _longEntryPrice.Value;
		var step = GetPriceStep();

		if (StopLossPips > 0m)
		{
			var stopLevel = entry - StopLossPips * step;
			if (candle.LowPrice <= stopLevel)
			{
				SellMarket();
				ResetLongState();
				return true;
			}
		}

		if (TakeProfitPips > 0m)
		{
			var targetLevel = entry + TakeProfitPips * step;
			if (candle.HighPrice >= targetLevel)
			{
				SellMarket();
				ResetLongState();
				return true;
			}
		}

		if (TrailingStopPips > 0m && TrailingStepPips > 0m)
		{
			var trailingDistance = TrailingStopPips * step;
			var trailingStep = TrailingStepPips * step;
			var activationDistance = trailingDistance + trailingStep;

			if (candle.ClosePrice - entry > activationDistance)
			{
				var candidate = candle.ClosePrice - trailingDistance;

				if (_longTrailingStop is null || candidate - _longTrailingStop.Value > trailingStep)
				_longTrailingStop = candidate;
			}

			if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
			{
				SellMarket();
				ResetLongState();
				return true;
			}
		}

		return false;
	}

	private bool ManageShort(ICandleMessage candle)
	{
		if (_shortEntryPrice is null)
		_shortEntryPrice = candle.ClosePrice;

		var entry = _shortEntryPrice.Value;
		var step = GetPriceStep();

		if (StopLossPips > 0m)
		{
			var stopLevel = entry + StopLossPips * step;
			if (candle.HighPrice >= stopLevel)
			{
				BuyMarket();
				ResetShortState();
				return true;
			}
		}

		if (TakeProfitPips > 0m)
		{
			var targetLevel = entry - TakeProfitPips * step;
			if (candle.LowPrice <= targetLevel)
			{
				BuyMarket();
				ResetShortState();
				return true;
			}
		}

		if (TrailingStopPips > 0m && TrailingStepPips > 0m)
		{
			var trailingDistance = TrailingStopPips * step;
			var trailingStep = TrailingStepPips * step;
			var activationDistance = trailingDistance + trailingStep;

			if (entry - candle.ClosePrice > activationDistance)
			{
				var candidate = candle.ClosePrice + trailingDistance;

				if (_shortTrailingStop is null || _shortTrailingStop.Value - candidate > trailingStep)
				_shortTrailingStop = candidate;
			}

			if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
			{
				BuyMarket();
				ResetShortState();
				return true;
			}
		}

		return false;
	}

	private bool TryEnterLong(ICandleMessage candle, decimal upper)
	{
		if (candle.ClosePrice <= upper)
		return false;

		if (!HasCapacity())
		return false;

		if (Position < -VolumeTolerance)
		{
			BuyMarket();
			ResetShortState();
			return true;
		}

		if (Position > VolumeTolerance)
		{
			SellMarket();
			ResetLongState();
			return true;
		}

		BuyMarket();
		_longEntryPrice = candle.ClosePrice;
		_longTrailingStop = null;
		ResetShortState();
		return true;
	}

	private bool TryEnterShort(ICandleMessage candle, decimal lower)
	{
		if (candle.ClosePrice >= lower)
		return false;

		if (!HasCapacity())
		return false;

		if (Position > VolumeTolerance)
		{
			SellMarket();
			ResetLongState();
			return true;
		}

		if (Position < -VolumeTolerance)
		{
			BuyMarket();
			ResetShortState();
			return true;
		}

		SellMarket();
		_shortEntryPrice = candle.ClosePrice;
		_shortTrailingStop = null;
		ResetLongState();
		return true;
	}

	private bool HasCapacity()
	{
		if (Volume <= 0m || MaxPositions <= 0)
		return false;

		var limitVolume = MaxPositions * Volume;
		return Math.Abs(Position) < limitVolume - VolumeTolerance;
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 0m;
		return step <= 0m ? 1m : step;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longTrailingStop = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortTrailingStop = null;
	}
}