Auf GitHub ansehen

Bollinger Bands N Positionen-Strategie

Überblick

Diese Strategie ist ein StockSharp-Port des MetaTrader Expert Advisors Bollinger Bands N positions. Sie überwacht Schlusskurse relativ zu einem Bollinger-Bänder-Envelope und tritt in eine Position ein, wann immer der Markt eine Kerze außerhalb des Kanals schließt. Das Positionsmanagement repliziert den ursprünglichen Experten, indem es eine Obergrenze für das Gesamtengagement durchsetzt, feste Stop-Loss- und Take-Profit-Abstände platziert und einen Trailing-Stop aktiviert, sobald der Trade ausreichend im Gewinn ist.

Handelslogik

  1. Den konfigurierten Kerzentyp abonnieren und Bollinger Bänder mit dem ausgewählten Zeitraum und der Breite berechnen.
  2. Bei jeder abgeschlossenen Kerze prüft die Strategie zunächst, ob eine bestehende Position geschlossen werden muss:
    • Long-Positionen steigen aus, wenn der Preis den festen Stop-Loss, den festen Take-Profit oder den Trailing-Stop-Level berührt.
    • Short-Positionen wenden die symmetrische Logik an.
  3. Wenn Trading erlaubt ist und auf der aktuellen Kerze kein Ausstieg erfolgte, werden Einstiegssignale bewertet:
    • Wenn der Schlusskurs über dem oberen Band liegt, schließt die Strategie Short-Engagements und öffnet, wenn innerhalb der Positionsobergrenze, eine neue Long-Position mit dem angeforderten Volumen.
    • Wenn der Schlusskurs unter dem unteren Band liegt, schließt sie Long-Engagements und öffnet auf die gleiche Weise eine Short-Position.
  4. Trailing-Stops bewegen sich in Schritten, die durch den Trailing-Schritt-Parameter definiert sind, sobald der Trade um die Trailing-Distanz plus den Trailing-Schritt voraus liegt. Das Trailing-Level bleibt um die Trailing-Distanz hinter dem Preis und rückt nur vor, wenn der Gewinn um mindestens einen Trailing-Schritt zunimmt.

Positionsmanagement

  • Max Positions definiert das maximale Nettoengagement, gemessen als MaxPositions × Volume. Da StockSharp im Netting-Modus arbeitet, kann die Strategie nur eine Nettoposition gleichzeitig halten. Der Parameter wirkt daher als Sicherheitsobergrenze, die verhindert, dass die Strategie erneut einsteigt, wenn die aktuelle absolute Position das konfigurierte Limit bereits erreicht.
  • Stop-Loss- und Take-Profit-Abstände werden in Pips angegeben. Die Strategie konvertiert sie mithilfe des PriceStep der Sicherheit in Preise. Wenn das Instrument Bruchteil-Pip-Preise verwendet, müssen die Werte möglicherweise entsprechend angepasst werden.
  • Trailing-Stops erfordern, dass sowohl der Abstand als auch der Schritt positiv sind. Wenn der Trailing-Stop-Abstand auf null gesetzt wird, wird das Trailing-Modul deaktiviert.

Parameter

Parameter Beschreibung Standard
Volume Ordergröße in Lots für jeden Einstieg. 0.1
MaxPositions Nettopositionsobergrenze in Vielfachen von Volume. 9
BollingerPeriod Rückblicklänge für den Bollinger-Gleitenden-Durchschnitt. 20
BollingerWidth Standardabweichungsmultiplikator für die Bollinger Bänder. 2
StopLossPips Stop-Loss-Abstand in Pips. 50
TakeProfitPips Take-Profit-Abstand in Pips. 50
TrailingStopPips Trailing-Stop-Abstand in Pips. Auf 0 setzen zum Deaktivieren. 5
TrailingStepPips Mindestgewinninkrement, das benötigt wird, bevor der Trailing-Stop vorrückt. 5
CandleType Zeitrahmen oder benutzerdefinierter Kerzentyp für die Bollinger-Bänder. 1-Minuten-Zeitrahmen

Unterschiede zum MQL5-Experten

  • Der ursprüngliche Experte arbeitet im Hedging-Modus von MetaTrader und kann gleichzeitig Long- und Short-Positionen halten. StockSharp-Strategien sind genettete, daher schließt dieser Port entgegengesetzte Engagements, bevor er in einen neuen Trade eintritt. Der Parameter MaxPositions begrenzt daher die absolute Größe der Nettoposition statt der Anzahl unabhängiger Tickets.
  • Orderstops werden innerhalb der Strategie simuliert, anstatt als angehängte Stop-Orders gesendet zu werden. Dies stimmt mit der Trailing-Logik der MQL-Implementierung überein, bedeutet aber, dass Ausstiege auf der nächsten abgeschlossenen Kerze erfolgen.
  • Die Trailing-Konfiguration wird beim Start validiert. Das Aktivieren eines Trailing-Stops mit einem null Trailing-Schritt wirft eine Ausnahme, um die ursprüngliche Initialisierungsprüfung nachzuahmen.

Verwendungshinweise

  1. Volume, MaxPositions und die Risikoparameter so konfigurieren, dass sie zur Kontraktgröße und dem Tick-Wert des Instruments passen.
  2. Sicherstellen, dass die Sicherheit einen gültigen PriceStep exponiert. Wenn der Schritt null oder fehlt, fällt die Strategie auf 1 zurück, was möglicherweise nicht für alle Märkte passt.
  3. Die Strategie erst starten, nachdem die Indikator-Aufwärmperiode (Bollinger-Periode) abgeschlossen ist, um nicht auf unvollständigen Daten zu agieren.
  4. Protokolle auf Trailing-Schritt-Validierungsfehler überwachen, wenn die Risikoeinstellungen angepasst werden.
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;
	}
}