Ver en GitHub

Bollinger Breakout DC2008

Reimplementation of Sergey Pavlov's (DC2008) MetaTrader Bollinger breakout expert advisor for the StockSharp high-level strategy API. The strategy watches completed candles, evaluates Bollinger Bands breakouts on the selected price source and opens or reverses positions only when the current trade is not losing.

Overview

  • Calculates a Bollinger Bands envelope on the configured timeframe and applied price (close, open, high, low, median, typical, weighted, or average).
  • Generates long setups when the candle low closes below the lower band while the high remains under the middle band (strong downside stretch that should revert).
  • Generates short setups when the candle high exceeds the upper band while the low stays above the middle band (strong upside stretch expected to revert).
  • The original MQL expert traded on ticks; in this port signals are processed once per finished candle for stability and indicator coherence.
  • Positions are only entered or reversed if the existing position shows a non-negative unrealized profit, replicating the original risk filter.

Trading Logic

Indicator pipeline

  1. Subscribe to candles of the chosen CandleType (default: 1-hour time frame).
  2. Feed the selected applied price into a Bollinger Bands indicator (Length = BandsPeriod, Width = BandsDeviation).
  3. Ignore candles until the indicator produces valid upper, middle, and lower values.

Entry conditions

  • Buy: Low < LowerBand and High < MiddleBand. Indicates the entire candle traded below the middle line after piercing the lower band.
  • Sell: High > UpperBand and Low > MiddleBand. Indicates the entire candle traded above the middle line after piercing the upper band.

Position filter and management

  • If there is no position, the strategy opens one market order with the configured Volume when a signal appears.
  • If a position already exists:
    • When the signal is opposite to the current direction, compute unrealized profit as Position * (Close - PositionPrice) using the candle close.
    • If unrealized profit is negative, skip all actions for this candle (identical to the original early return).
    • If unrealized profit is non-negative and the signal is opposite, send a reversing market order sized Volume + |Position| to both flatten the current position and establish a new one in the signal direction.
    • Signals that match the current direction do not add to the position (same as the MQL version).
  • There are no explicit stop-loss or take-profit orders; trade exits happen only via opposing signals that satisfy the profit filter.

Parameters

Name Default Description
BandsPeriod 80 Number of candles used to compute the Bollinger moving average and deviations. Must be positive and is exposed for optimization.
BandsDeviation 3.0 Standard deviation multiplier applied to the Bollinger Bands width. Positive, optimizable.
AppliedPrice Close Price source for the indicator: Close, Open, High, Low, Median, Typical, Weighted, or Average (OHLC/4). Mirrors ENUM_APPLIED_PRICE from MetaTrader.
CandleType 1-hour time frame Candle type (time frame) used for analysis and orders. Can be switched to any other data type supported by StockSharp.
Volume (inherited) broker-dependent Order size for new entries. Reversals automatically add the existing absolute position size.

Differences versus the original MQL expert

  • The MetaTrader EA evaluated conditions on every tick; this C# port waits for finished candles to avoid acting on incomplete data.
  • Indicator shift was fixed to zero in the source EA and remains implicit here; additional shifts are not exposed.
  • MetaTrader reported floating profit directly; the port approximates it via candle close and PositionPrice, which is sufficient for sign comparison used by the filter.
  • Trade management, string messages, and order comments from the MQL version are omitted, focusing purely on signal generation.

Implementation notes

  • Candles, indicators, and trading calls rely on StockSharp high-level APIs (SubscribeCandles().Bind(...), BuyMarket, SellMarket).
  • The indicator is drawn automatically if a chart area is available in the UI; trades are also plotted for debugging.
  • The strategy resets and rebuilds the indicator on every start, so parameter changes take immediate effect on the next run.
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
		};
	}
}