Открыть на GitHub

Прорыв по полосам Боллинджера DC2008

Портирование эксперта Сергея Павлова (DC2008) для MetaTrader в среду StockSharp. Стратегия отслеживает закрытые свечи выбранного таймфрейма, строит полосы Боллинджера по указанной цене и открывает/разворачивает позиции только тогда, когда текущая сделка не убыточна.

Краткое описание

  • Формирует оболочку из полос Боллинджера на выбранном типе свечей и источнике цены (закрытие, открытие, максимум, минимум, медиана, типичная, взвешенная или средняя цена).
  • Покупка происходит, когда минимум свечи пробивает нижнюю полосу, а максимум остаётся ниже средней линии (сильное движение вниз, ожидается возврат).
  • Продажа выполняется, когда максимум свечи пробивает верхнюю полосу, а минимум находится выше средней линии (сильное движение вверх, ожидается возврат).
  • В оригинале сигналы проверялись на каждом тике; в этой версии расчёты выполняются на закрытии свечи, что повышает устойчивость и соответствует логике индикатора.
  • Позиции открываются или переворачиваются только при неотрицательной плавающей прибыли, повторяя фильтр исходного робота.

Алгоритм работы

Построение индикатора

  1. Подписка на свечи типа CandleType (по умолчанию часовые свечи).
  2. Передача выбранной цены в индикатор Bollinger Bands (Length = BandsPeriod, Width = BandsDeviation).
  3. До появления валидных значений верхней, средней и нижней линий сигналы игнорируются.

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

  • Лонг: Low < LowerBand и одновременно High < MiddleBand. Вся свеча торгуется ниже средней линии после пробоя нижней полосы.
  • Шорт: High > UpperBand и одновременно Low > MiddleBand. Вся свеча находится выше средней линии после пробоя верхней полосы.

Фильтр позиции и сопровождение

  • При отсутствии позиции выставляется рыночный ордер объёмом Volume сразу после сигнала.
  • Если позиция уже открыта:
    • Для противоположного сигнала вычисляется нереализованная прибыль как Position * (Close - PositionPrice) по цене закрытия свечи.
    • При отрицательной прибыли действия пропускаются (аналог return в MQL).
    • При прибыли ≥ 0 и противоположном сигнале отправляется рыночный ордер объёмом Volume + |Position|, что закрывает текущую позицию и открывает новую в направлении сигнала.
    • Сигналы в сторону текущей позиции не наращивают объём — поведение полностью повторяет оригинал.
  • Стоп-лосс и тейк-профит не задаются; выход осуществляется только по встречному сигналу, прошедшему фильтр прибыли.

Параметры

Имя Значение по умолчанию Описание
BandsPeriod 80 Количество свечей для расчёта средней и отклонений полос Боллинджера. Положительное, доступно для оптимизации.
BandsDeviation 3.0 Множитель стандартного отклонения, задающий ширину полос. Положительный, доступен для оптимизации.
AppliedPrice Close Источник цены для расчёта индикатора: Close, Open, High, Low, Median, Typical, Weighted или Average (OHLC/4). Аналог ENUM_APPLIED_PRICE из MetaTrader.
CandleType Часовой таймфрейм Тип свечей, используемый в анализе. Можно заменить на любой другой поддерживаемый StockSharp.
Volume (унаследованный) зависит от брокера Объём новых входов. При развороте автоматически добавляется текущий модуль позиции.

Отличия от эксперта MetaTrader

  • В MQL стратегия работала на тиках; здесь используется закрытие свечей, чтобы не реагировать на незавершённые бары.
  • Параметр сдвига индикатора в исходнике всегда равнялся нулю, поэтому отдельно не выводится.
  • В MetaTrader плавающая прибыль берётся из торгового ядра, в порте она оценивается через PositionPrice и цену закрытия, что достаточно для проверки знака.
  • Убраны текстовые комментарии и управление ордерами, не влияющие на торговую логику.

Технические детали

  • Используются высокоуровневые методы StockSharp: подписка на свечи, индикаторы, BuyMarket/SellMarket.
  • При наличии области графика отображаются свечи, индикатор и сделки для визуального контроля.
  • При перезапуске стратегия пересоздаёт индикатор, поэтому изменения параметров учитываются с нового запуска.
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 breakout strategy inspired by DC2008 implementation.
/// </summary>
public class BollingerBreakoutDc2008Strategy : Strategy
{
	public enum AppliedPriceTypes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Average
	}

	private readonly StrategyParam<int> _bandsPeriod;
	private readonly StrategyParam<decimal> _bandsDeviation;
	private readonly StrategyParam<AppliedPriceTypes> _appliedPrice;
	private readonly StrategyParam<DataType> _candleType;

	private BollingerBands _bollinger;
	private decimal _entryPrice;

	public int BandsPeriod
	{
		get => _bandsPeriod.Value;
		set => _bandsPeriod.Value = value;
	}

	public decimal BandsDeviation
	{
		get => _bandsDeviation.Value;
		set => _bandsDeviation.Value = value;
	}

	public AppliedPriceTypes AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

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

	public BollingerBreakoutDc2008Strategy()
	{
		_bandsPeriod = Param(nameof(BandsPeriod), 80)
			.SetDisplay("Bands Period", "Number of candles for Bollinger Bands", "Indicators")
			.SetGreaterThanZero()
			
			.SetOptimize(10, 200, 10);

		_bandsDeviation = Param(nameof(BandsDeviation), 3m)
			.SetDisplay("Deviation", "Standard deviation multiplier", "Indicators")
			.SetGreaterThanZero()
			
			.SetOptimize(1m, 5m, 0.5m);

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceTypes.Close)
			.SetDisplay("Applied Price", "Candle price source for Bollinger Bands", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to analyze", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_bollinger = null;
		_entryPrice = 0m;
	}

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

		// Create Bollinger Bands indicator with the configured parameters.
		_bollinger = new BollingerBands
		{
			Length = BandsPeriod,
			Width = BandsDeviation
		};

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished || _bollinger is null)
			return;

		// Calculate Bollinger Bands for the selected price source.
		var indicatorValue = _bollinger.Process(new DecimalIndicatorValue(_bollinger, GetAppliedPrice(candle), candle.OpenTime) { IsFinal = true });

		if (!indicatorValue.IsFinal)
			return;

		if (indicatorValue is not BollingerBandsValue bands)
			return;

		if (bands.UpBand is not decimal upper || bands.LowBand is not decimal lower || bands.MovingAverage is not decimal middle)
			return;

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		// Determine breakout conditions based on Bollinger structure.
		var buySignal = low < lower && high < middle;
		var sellSignal = high > upper && low > middle;

		if (!buySignal && !sellSignal)
			return;

		// Compute unrealized profit to mimic original position filter.
		var unrealizedPnL = Position == 0 ? 0m : Position * (close - _entryPrice);

		if (buySignal)
		{
			if (Position == 0)
			{
				// No position open, start a new long.
				BuyMarket();
				_entryPrice = close;
			}
			else
			{
				if (unrealizedPnL < 0m)
					return;

				if (Position < 0)
				{
					// Reverse from short to long while preserving target volume.
					BuyMarket();
					_entryPrice = close;
				}
			}

			return;
		}

		if (sellSignal)
		{
			if (Position == 0)
			{
				// No position open, start a new short.
				SellMarket();
				_entryPrice = close;
			}
			else
			{
				if (unrealizedPnL < 0m)
					return;

				if (Position > 0)
				{
					// Reverse from long to short while preserving target volume.
					SellMarket();
					_entryPrice = close;
				}
			}
		}
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			AppliedPriceTypes.Close => candle.ClosePrice,
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
			AppliedPriceTypes.Average => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}