Ver en GitHub

Estrategia Bollinger Bands N Posiciones

Descripción general

Esta estrategia es un port de StockSharp del asesor experto MetaTrader Bollinger Bands N positions. Monitorea los precios de cierre relativos a un envelope de Bandas de Bollinger y entra en una posición siempre que el mercado finaliza una barra fuera del canal. La gestión de posición replica al experto original imponiendo un límite en la exposición total, colocando offsets fijos de stop-loss y take-profit, y activando un trailing stop una vez que la operación está suficientemente en ganancias.

Lógica de trading

  1. Suscribirse al tipo de vela configurado y calcular Bandas de Bollinger con el período y el ancho seleccionados.
  2. En cada vela finalizada, la estrategia primero verifica si una posición existente debe cerrarse:
    • Las posiciones largas salen cuando el precio toca el stop-loss fijo, el take-profit fijo, o cuando el nivel de trailing stop es violado.
    • Las posiciones cortas aplican la lógica simétrica.
  3. Si el trading está permitido y no se produjo ninguna salida en la barra actual, se evalúan las señales de entrada:
    • Cuando el precio de cierre está por encima de la banda superior, la estrategia aplana cualquier exposición corta y, si está dentro del límite de posición, abre una nueva posición larga con el volumen solicitado.
    • Cuando el precio de cierre está por debajo de la banda inferior, aplana cualquier exposición larga y abre una posición corta de la misma manera.
  4. Los trailing stops se mueven en incrementos definidos por el parámetro de paso del trailing una vez que la operación está adelante por la distancia del trailing más el paso. El nivel del trailing se queda detrás del precio por la distancia del trailing y solo avanza cuando la ganancia aumenta al menos un paso del trailing.

Gestión de posición

  • Max Positions define la exposición neta máxima medida como MaxPositions × Volume. Dado que StockSharp opera en modo de netting, la estrategia puede mantener solo una posición neta a la vez. El parámetro actúa como un límite de seguridad que impide a la estrategia volver a entrar cuando la posición absoluta actual ya alcanza el límite configurado.
  • Las distancias de stop-loss y take-profit se especifican en pips. La estrategia las convierte en precios usando el PriceStep de la seguridad. Si el instrumento usa precios de pip fraccionario, puede que necesite ajustar los valores en consecuencia.
  • Los trailing stops requieren que tanto la distancia como el paso sean positivos. Cuando la distancia del trailing stop se establece en cero, el módulo de trailing se deshabilita.

Parámetros

Parámetro Descripción Predeterminado
Volume Tamaño de orden en lotes usado para cada entrada. 0.1
MaxPositions Límite de posición neta expresado en múltiplos de Volume. 9
BollingerPeriod Período de retrospectiva para la media móvil de Bollinger. 20
BollingerWidth Multiplicador de desviación estándar para las Bandas de Bollinger. 2
StopLossPips Distancia stop-loss en pips. 50
TakeProfitPips Distancia take-profit en pips. 50
TrailingStopPips Distancia del trailing stop en pips. Establecer en 0 para deshabilitar. 5
TrailingStepPips Incremento mínimo de ganancia requerido antes de que el trailing stop avance. 5
CandleType Marco temporal o tipo de vela personalizado usado para construir las Bandas de Bollinger. Marco temporal de 1 minuto

Diferencias del experto MQL5

  • El experto original opera en el modo de hedging de MetaTrader y puede mantener posiciones largas y cortas simultáneas. Las estrategias de StockSharp están en modo netting, por lo que este port aplana la exposición contraria antes de entrar en una nueva operación. El parámetro MaxPositions por tanto limita el tamaño absoluto de la posición neta en lugar del número de tickets independientes.
  • Los stops de órdenes se simulan dentro de la estrategia en lugar de enviarse como órdenes stop adjuntas. Esto coincide con la lógica de trailing de la implementación MQL pero significa que las salidas ocurren en la siguiente vela finalizada.
  • La configuración del trailing es validada al inicio. Habilitar un trailing stop con un paso de trailing cero lanza una excepción para imitar la verificación de inicialización original.

Notas de uso

  1. Configure Volume, MaxPositions y los parámetros de riesgo para que coincidan con el tamaño del contrato del instrumento y el valor del tick.
  2. Asegúrese de que la seguridad exponga un PriceStep válido. Si el paso es cero o falta, la estrategia usa 1 como respaldo, que puede no encajar en todos los mercados.
  3. Inicie la estrategia solo después de que el período de precalentamiento del indicador (período de Bollinger) se haya completado para evitar actuar sobre datos incompletos.
  4. Monitoree los registros en busca de errores de validación del paso de trailing cuando personalice los ajustes de riesgo.
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;
	}
}