Ver no GitHub

Estratégia Bollinger Bands N Posições

Visão geral

Esta estratégia é um port para StockSharp do expert advisor MetaTrader Bollinger Bands N positions. Ela monitora os preços de fechamento em relação a um envelope de Bandas de Bollinger e entra em uma posição sempre que o mercado finaliza uma barra fora do canal. O gerenciamento de posição replica o expert original impondo um limite na exposição total, colocando offsets fixos de stop-loss e take-profit, e ativando um trailing stop assim que a operação está suficientemente no lucro.

Lógica de negociação

  1. Assinar o tipo de candle configurado e calcular Bandas de Bollinger com o período e a largura selecionados.
  2. Em cada candle finalizado, a estratégia primeiro verifica se uma posição existente deve ser fechada:
    • Posições compradas saem quando o preço atinge o stop-loss fixo, o take-profit fixo, ou quando o nível de trailing stop é violado.
    • Posições vendidas aplicam a lógica simétrica.
  3. Se o trading for permitido e nenhuma saída tiver ocorrido na barra atual, os sinais de entrada são avaliados:
    • Quando o preço de fechamento estiver acima da banda superior, a estratégia achata qualquer exposição vendida e, se estiver dentro do limite de posição, abre uma nova posição comprada com o volume solicitado.
    • Quando o preço de fechamento estiver abaixo da banda inferior, achata qualquer exposição comprada e abre uma posição vendida da mesma forma.
  4. Os trailing stops se movem em incrementos definidos pelo parâmetro de passo do trailing assim que a operação está à frente pela distância do trailing mais o passo. O nível do trailing fica atrás do preço pela distância do trailing e só avança quando o lucro aumenta pelo menos um passo do trailing.

Gestão de posição

  • Max Positions define a exposição líquida máxima medida como MaxPositions × Volume. Como o StockSharp opera em modo de netting, a estratégia pode manter apenas uma posição líquida por vez. O parâmetro atua como um limite de segurança que impede a estratégia de reentrar quando a posição absoluta atual já atinge o limite configurado.
  • As distâncias de stop-loss e take-profit são especificadas em pips. A estratégia as converte em preços usando o PriceStep do ativo. Se o instrumento usa preços de pip fracionário, pode ser necessário ajustar os valores de acordo.
  • Os trailing stops exigem que tanto a distância quanto o passo sejam positivos. Quando a distância do trailing stop é definida como zero, o módulo de trailing é desabilitado.

Parâmetros

Parâmetro Descrição Padrão
Volume Tamanho da ordem em lotes usado para cada entrada. 0.1
MaxPositions Limite de posição líquida expresso em múltiplos de Volume. 9
BollingerPeriod Período de retrospectiva para a média móvel de Bollinger. 20
BollingerWidth Multiplicador de desvio padrão para as Bandas de Bollinger. 2
StopLossPips Distância stop-loss em pips. 50
TakeProfitPips Distância take-profit em pips. 50
TrailingStopPips Distância do trailing stop em pips. Defina como 0 para desabilitar. 5
TrailingStepPips Incremento mínimo de lucro necessário antes que o trailing stop avance. 5
CandleType Período ou tipo de candle personalizado usado para construir as Bandas de Bollinger. Período de 1 minuto

Diferenças do expert MQL5

  • O expert original opera no modo de hedging do MetaTrader e pode manter posições compradas e vendidas simultâneas. As estratégias do StockSharp são de netting, portanto este port achata a exposição contrária antes de entrar em uma nova operação. O parâmetro MaxPositions limita portanto o tamanho absoluto da posição líquida em vez do número de tickets independentes.
  • Os stops de ordens são simulados dentro da estratégia em vez de serem enviados como ordens stop adjuntas. Isso corresponde à lógica de trailing da implementação MQL, mas significa que as saídas ocorrem no próximo candle finalizado.
  • A configuração do trailing é validada na inicialização. Habilitar um trailing stop com um passo de trailing zero lança uma exceção para imitar a verificação de inicialização original.

Notas de uso

  1. Configure Volume, MaxPositions e os parâmetros de risco para corresponder ao tamanho do contrato do instrumento e ao valor do tick.
  2. Certifique-se de que o ativo expõe um PriceStep válido. Se o passo for zero ou ausente, a estratégia usa 1 como fallback, que pode não se encaixar em todos os mercados.
  3. Inicie a estratégia somente após o período de aquecimento do indicador (período de Bollinger) ter sido concluído para evitar agir sobre dados incompletos.
  4. Monitore os registros em busca de erros de validação do passo de trailing ao personalizar as configurações de risco.
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;
	}
}