Ver no GitHub

Estratégia Bollinger Bands N Posições v2

Visão geral

Esta estratégia replica o expert advisor "Bollinger Bands N positions v2" de Vladimir Karputov. Opera em candles completados e procura rompimentos de preço relativos ao envelope das Bollinger Bands. O port para StockSharp mantém o comportamento de piramidação original, controles de risco e lógica de trailing enquanto adapta o gerenciamento de ordens ao modelo de compensação da plataforma.

Lógica de trading

  • Um indicador de Bollinger Bands (período e desvio configuráveis) é calculado na série de candles selecionada.
  • Quando o fechamento do candle termina acima da banda superior, a estratégia fecha qualquer exposição vendida ativa e abre uma posição comprada adicional (até o número máximo configurado de entradas empilhadas).
  • Quando o fechamento do candle termina abaixo da banda inferior, a estratégia fecha qualquer exposição comprada ativa e abre uma posição vendida adicional (também limitada pelo parâmetro de entradas máximas).
  • O tamanho da posição é aumentado em incrementos fixos (o parâmetro Volume) ao piramidizar na mesma direção.
  • O preço de entrada médio da posição empilhada é rastreado para gerenciar os níveis de stop loss, take profit e trailing stop de forma consistente.

Gestão de risco

  • As distâncias de stop loss e take profit são inseridas em pips. Elas são convertidas em offsets de preço absolutos multiplicando pelo passo de preço do instrumento. Instrumentos cotados com 3 ou 5 casas decimais multiplicam automaticamente o passo por 10 para emular o ajuste de tamanho de pip do MetaTrader.
  • O offset do trailing stop e o passo do trailing também são configurados em pips. O mecanismo de trailing atualiza o preço do stop apenas depois que a operação se move TrailingStop + TrailingStep pips a partir da entrada média atual. Cada atualização desloca o stop pelo offset do trailing enquanto respeita o buffer de passo extra para evitar modificações excessivas.
  • As ordens de saída protetoras são simuladas dentro da estratégia: sempre que um candle terminado cruzar o nível de stop ou alvo, a posição inteira é fechada usando ordens de mercado.

Parâmetros

Parâmetro Descrição
Bollinger Period Período de retrocesso para a média móvel das Bollinger Bands.
Bollinger Deviation Multiplicador de desvio padrão para o envelope das Bollinger Bands.
Max Positions Número máximo de entradas empilhadas permitidas por direção.
Volume Volume de ordem para cada entrada individual.
Stop Loss (pips) Distância de stop loss em pips (0 desabilita o stop).
Take Profit (pips) Distância de take profit em pips (0 desabilita o alvo).
Trailing Stop (pips) Distância do trailing stop em pips (0 desabilita o trailing).
Trailing Step (pips) Lucro adicional em pips necessário antes de mover o trailing stop novamente. Deve ser positivo quando o trailing estiver habilitado.
Candle Type Série de candles processada pela estratégia.

Notas de implementação

  • A estratégia usa subscrições de candles de alto nível com vinculação de indicadores, seguindo as diretrizes do StockSharp.
  • Apenas candles terminados são processados para espelhar a lógica original de "nova barra" do MetaTrader.
  • Como o StockSharp opera no modo de compensação, a conversão fecha a exposição oposta antes de abrir uma nova camada de pirâmide na outra direção.
  • O passo do trailing deve permanecer maior que zero sempre que o trailing stop estiver ativo, correspondendo à verificação de segurança do expert advisor original.
  • A implementação em Python não está incluída nesta versão.
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 that can pyramid entries and applies pip-based risk management.
/// </summary>
public class BollingerBandsNPositionsV2Strategy : Strategy
{
	private readonly StrategyParam<int> _bollingerPeriod;
	private readonly StrategyParam<decimal> _bollingerDeviation;
	private readonly StrategyParam<int> _maxPositions;
	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 _pipValue;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingStopDistance;
	private decimal _trailingStepDistance;

	private decimal _longEntryPrice;
	private decimal _shortEntryPrice;
	private int _longEntryCount;
	private int _shortEntryCount;
	private BollingerBands _bollinger = null!;
	private decimal? _longStopPrice;
	private decimal? _longTakeProfitPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfitPrice;

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

	/// <summary>
	/// Bollinger Bands standard deviation multiplier.
	/// </summary>
	public decimal BollingerDeviation
	{
		get => _bollingerDeviation.Value;
		set => _bollingerDeviation.Value = value;
	}

	/// <summary>
	/// Maximum stacked entries per direction.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

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

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

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

	/// <summary>
	/// Additional profit in pips required before trailing stop is moved again.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="BollingerBandsNPositionsV2Strategy"/>.
	/// </summary>
	public BollingerBandsNPositionsV2Strategy()
	{
		_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Bollinger Period", "Period used for Bollinger Bands.", "Indicators")
			;

		_bollingerDeviation = Param(nameof(BollingerDeviation), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands.", "Indicators")
			;

		_maxPositions = Param(nameof(MaxPositions), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum number of stacked entries per direction.", "Trading");

		_stopLossPips = Param(nameof(StopLossPips), 30m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips.", "Risk")
			;

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

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 1m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Extra profit in pips before trailing stop is adjusted.", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for Bollinger analysis.", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_pipValue = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingStopDistance = 0m;
		_trailingStepDistance = 0m;
		ResetLongState();
		ResetShortState();
	}

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

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

		_pipValue = CalculatePipValue();
		UpdateRiskDistances();

		_bollinger = new BollingerBands
		{
			Length = BollingerPeriod,
			Width = BollingerDeviation
		};

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

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

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

		IIndicatorValue indicatorValue;

		try
		{
			indicatorValue = _bollinger.Process(candle);
		}
		catch (IndexOutOfRangeException)
		{
			return;
		}

		if (indicatorValue.IsEmpty || !_bollinger.IsFormed)
			return;

		UpdateRiskDistances();

		var value = (BollingerBandsValue)indicatorValue;

		if (value.UpBand is not decimal upper || value.LowBand is not decimal lower)
			return;

		HandleRiskManagement(candle);

		if (candle.ClosePrice > upper)
		{
			TryEnterLong(candle);
			return;
		}

		if (candle.ClosePrice < lower)
		{
			TryEnterShort(candle);
		}
	}

	private void HandleRiskManagement(ICandleMessage candle)
	{
		if (_longEntryCount > 0 && Position > 0)
		{
			if (_longTakeProfitPrice is decimal takeProfit && candle.HighPrice >= takeProfit)
			{
				SellMarket(Position);
				ResetLongState();
				return;
			}

			if (_longStopPrice is decimal stopLoss && candle.LowPrice <= stopLoss)
			{
				SellMarket(Position);
				ResetLongState();
				return;
			}

			UpdateLongTrailing(candle);
		}
		else if (Position <= 0)
		{
			ResetLongState();
		}

		if (_shortEntryCount > 0 && Position < 0)
		{
			var positionVolume = Math.Abs(Position);

			if (_shortTakeProfitPrice is decimal takeProfit && candle.LowPrice <= takeProfit)
			{
				BuyMarket(positionVolume);
				ResetShortState();
				return;
			}

			if (_shortStopPrice is decimal stopLoss && candle.HighPrice >= stopLoss)
			{
				BuyMarket(positionVolume);
				ResetShortState();
				return;
			}

			UpdateShortTrailing(candle);
		}
		else if (Position >= 0)
		{
			ResetShortState();
		}
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		if (_longEntryCount >= MaxPositions)
			return;

		if (Position < 0)
		{
			var closeVolume = Math.Abs(Position);
			if (closeVolume > 0)
			{
				BuyMarket(closeVolume);
				ResetShortState();
			}
		}

		var tradeVolume = Volume;
		if (tradeVolume <= 0)
			return;

		var existingVolume = _longEntryCount * tradeVolume;
		BuyMarket(tradeVolume);

		var entryPrice = candle.ClosePrice;
		var newVolume = existingVolume + tradeVolume;
		_longEntryPrice = existingVolume <= 0 ? entryPrice : ((_longEntryPrice * existingVolume) + entryPrice * tradeVolume) / newVolume;
		_longEntryCount++;
		_longStopPrice = StopLossPips > 0m ? _longEntryPrice - _stopLossDistance : null;
		_longTakeProfitPrice = TakeProfitPips > 0m ? _longEntryPrice + _takeProfitDistance : null;
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		if (_shortEntryCount >= MaxPositions)
			return;

		if (Position > 0)
		{
			var closeVolume = Position;
			if (closeVolume > 0)
			{
				SellMarket(closeVolume);
				ResetLongState();
			}
		}

		var tradeVolume = Volume;
		if (tradeVolume <= 0)
			return;

		var existingVolume = _shortEntryCount * tradeVolume;
		SellMarket(tradeVolume);

		var entryPrice = candle.ClosePrice;
		var newVolume = existingVolume + tradeVolume;
		_shortEntryPrice = existingVolume <= 0 ? entryPrice : ((_shortEntryPrice * existingVolume) + entryPrice * tradeVolume) / newVolume;
		_shortEntryCount++;
		_shortStopPrice = StopLossPips > 0m ? _shortEntryPrice + _stopLossDistance : null;
		_shortTakeProfitPrice = TakeProfitPips > 0m ? _shortEntryPrice - _takeProfitDistance : null;
	}

	private void UpdateLongTrailing(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m)
			return;

		var moveFromEntry = candle.ClosePrice - _longEntryPrice;
		if (moveFromEntry <= _trailingStopDistance + _trailingStepDistance)
			return;

		var newStop = candle.ClosePrice - _trailingStopDistance;

		if (_longStopPrice is not decimal currentStop || newStop > currentStop + _trailingStepDistance)
			_longStopPrice = newStop;
	}

	private void UpdateShortTrailing(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m)
			return;

		var moveFromEntry = _shortEntryPrice - candle.ClosePrice;
		if (moveFromEntry <= _trailingStopDistance + _trailingStepDistance)
			return;

		var newStop = candle.ClosePrice + _trailingStopDistance;

		if (_shortStopPrice is not decimal currentStop || newStop < currentStop - _trailingStepDistance)
			_shortStopPrice = newStop;
	}

	private void ResetLongState()
	{
		_longEntryPrice = 0m;
		_longEntryCount = 0;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = 0m;
		_shortEntryCount = 0;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
	}

	private void UpdateRiskDistances()
	{
		_stopLossDistance = StopLossPips > 0m ? StopLossPips * _pipValue : 0m;
		_takeProfitDistance = TakeProfitPips > 0m ? TakeProfitPips * _pipValue : 0m;
		_trailingStopDistance = TrailingStopPips > 0m ? TrailingStopPips * _pipValue : 0m;
		_trailingStepDistance = TrailingStepPips > 0m ? TrailingStepPips * _pipValue : 0m;
	}

	private decimal CalculatePipValue()
	{
		var security = Security;
		if (security == null)
			return 1m;

		var step = security.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var decimals = CountDecimals(step);
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private static int CountDecimals(decimal value)
	{
		value = Math.Abs(value);
		var decimals = 0;

		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}

		return decimals;
	}
}