Auf GitHub ansehen

Rsi-Teststrategie

Überblick

RsiTestStrategy wandelt den MetaTrader 4 Expertenberater RSI_Test in den hohen Level API von StockSharp um. Die Strategie kombiniert einen schnellen RSI-Momentumfilter mit einfacher Kerzenbestätigung und risikobewusster Positionsgrößenbestimmung. Es handelt ein einzelnes, durch die Host-Strategie definiertes Instrument und verwendet nur abgeschlossene Kerzen, was die Tick-to-Close-Logik des Originalcodes widerspiegelt.

Handelsregeln

  1. Berechnen Sie den Relative Strength Index mit dem konfigurierbaren RsiPeriod.
  2. Gehen Sie long, wenn der RSI aus einem überverkauften Bereich (BuyLevel) steigt und die aktuelle Kerze über der vorherigen eröffnet.
  3. Gehen Sie short, wenn der RSI aus einem überkauften Bereich (SellLevel) fällt und die aktuelle Kerze unterhalb der vorherigen eröffnet.
  4. Beachten Sie das Limit von MaxOpenPositions. Ein Wert von 0 deaktiviert die Obergrenze; andernfalls darf das Nettorisiko MaxOpenPositions * Volume nicht überschreiten.
  5. Verwalten Sie Ausstiege über einen treppenförmigen Trailing-Stop, der aktiviert wird, sobald der Preis um TrailingDistanceSteps Ticks über den durchschnittlichen Einstiegspreis steigt.
  6. Es wird kein expliziter Take-Profit verwendet. Positionen werden geschlossen, wenn der Trailing Stop ausgelöst wird oder wenn die Handelssitzung die Strategie beendet.

Positionsgröße und Risiko

  • Die Strategie leitet eine vorläufige Ordergröße aus RiskPercentage des aktuellen Portfoliowerts ab. Wenn das Instrument Margendaten (Security.MarginBuy/Security.MarginSell) bereitstellt, wird das erforderliche Kapital pro Lot eingehalten; andernfalls wird der Betrag als konservativer Fallback durch den letzten Schlusskurs dividiert.
  • Die Volumina werden auf Security.VolumeStep gerundet (oder auf zwei Dezimalstellen, wenn der Schritt unbekannt ist) und innerhalb des Bereichs Security.MinVolume/Security.MaxVolume eingegrenzt.
  • Setzen Sie RiskPercentage auf Null, um die dynamische Größenanpassung zu deaktivieren und immer mit dem konfigurierten Volume zu handeln.

Trailing-Stop-Verhalten

  • TrailingDistanceSteps drückt die Distanz in Preisschritten (Security.PriceStep) aus. Wenn das Instrument keinen Schritt aufweist, wird die Distanz als direkter Preisversatz behandelt.
  • Sobald das Schluss- oder Intrabar-Hoch das Aktivierungsniveau überschreitet (entry + distance für Long-Positionen, entry - distance für Short-Positionen), aktiviert die Strategie den Trailing Stop mit demselben Offset über dem Einstiegspreis.
  • Der Schutzanschlag wird nur einmal pro Position angewendet, genau wie der ursprüngliche EA, der den Anschlag von der Gewinnschwelle zur ersten Stufe bewegt und dort hält.

Parameter

Name Beschreibung Standard
RsiPeriod RSI Lookback-Zeitraum. 14
BuyLevel Überverkaufter Schwellenwert, der eine lange Einrichtung vorbereitet. 12
SellLevel Überkaufter Schwellenwert, der ein kurzes Setup vorbereitet. 88
RiskPercentage Portfolioanteil, der für die Positionsgrößenbestimmung verwendet wird. Stellen Sie 0 auf Ignorieren ein. 10
TrailingDistanceSteps Distanz (in Preisschritten), die erforderlich ist, um den Trailing Stop zu aktivieren. 50
MaxOpenPositions Maximale gleichzeitige Positionen; 0 entfernt das Limit. 1
CandleType Primärer Zeitrahmen für Berechnungen. 15 Minuten
Volume Basisvolumen, wenn die Risikogröße nicht gelöst werden kann. 1

Nutzungshinweise

  1. Hängen Sie die Strategie an ein Wertpapier an, das genaue PriceStep-, VolumeStep- und Margin-Metadaten offenlegt, um die beste Übereinstimmung mit dem MQL-Verhalten zu erzielen.
  2. Der Algorithmus prüft nur abgeschlossene Kerzen (CandleStates.Finished), daher sollten Backtests denselben Zeitrahmen wie die Produktion verwenden.
  3. StartProtection() aus der Basisklasse ist in OnStarted aktiviert, sodass die integrierte Risikokontrolle von StockSharp unerwartete Positionsreste verwalten kann.
  4. Da der ursprüngliche Fachberater MetaTrader Optimierungen durch GlobalVariableGet eingeleitet hat, wird dieses Verhalten absichtlich weggelassen. Konfigurieren Sie die Parameter direkt in StockSharp.
  5. Kombinieren Sie die Strategie mit einem Portfolio, das Portfolio.CurrentValue für eine dynamische Risikogrößenbestimmung aktualisiert. Ohne sie greift die Strategie elegant auf das statische Volume zurück.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// RSI-based strategy with volume sizing and stair-like trailing stop.
/// </summary>
public class RsiTestStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _buyLevel;
	private readonly StrategyParam<decimal> _sellLevel;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<int> _trailingDistanceSteps;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private decimal? _previousOpen;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private bool _trailingArmed;
	private decimal _priceStep;

	/// <summary>
	/// Initialize <see cref="RsiTestStrategy"/>.
	/// </summary>
	public RsiTestStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 7)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback period for RSI", "Indicators")

			.SetOptimize(7, 28, 1);

		_buyLevel = Param(nameof(BuyLevel), 40m)
			.SetDisplay("RSI Buy Level", "Oversold threshold for long entries", "Trading");

		_sellLevel = Param(nameof(SellLevel), 60m)
			.SetDisplay("RSI Sell Level", "Overbought threshold for short entries", "Trading");

		_riskPercentage = Param(nameof(RiskPercentage), 10m)
			.SetDisplay("Risk Percentage", "Portfolio percentage used for sizing", "Risk");

		_trailingDistanceSteps = Param(nameof(TrailingDistanceSteps), 50)
			.SetDisplay("Trailing Distance Steps", "Steps before activating trailing stop", "Risk");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions. 0 disables the limit.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "Data");
	}

	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	public decimal BuyLevel
	{
		get => _buyLevel.Value;
		set => _buyLevel.Value = value;
	}

	public decimal SellLevel
	{
		get => _sellLevel.Value;
		set => _sellLevel.Value = value;
	}

	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

	public int TrailingDistanceSteps
	{
		get => _trailingDistanceSteps.Value;
		set => _trailingDistanceSteps.Value = value;
	}

	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousRsi = null;
		_previousOpen = null;
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
		_priceStep = 0m;
	}

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

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_priceStep = Security?.PriceStep ?? 0m;

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

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

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
	{
		// Only react to fully formed candles to match the MQL implementation.
		if (candle.State != CandleStates.Finished)
		return;

		// Manage trailing logic and exits before attempting fresh entries.
		ManagePosition(candle);

		if (!_rsi.IsFormed)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (_previousRsi is null || _previousOpen is null)
		{
			_previousRsi = rsiValue;
			_previousOpen = candle.OpenPrice;
			return;
		}

		if (rsiValue < BuyLevel && Position <= 0)
		{
			TryEnterLong(candle);
		}
		else if (rsiValue > SellLevel && Position >= 0)
		{
			TryEnterShort(candle);
		}

		_previousRsi = rsiValue;
		_previousOpen = candle.OpenPrice;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		// Close short position first if needed
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		// Close long position first if needed
		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
			ResetPositionState();
		}

		if (Position == 0)
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_stopPrice = null;
			_trailingArmed = false;
		}
	}

	private void ManagePosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetPositionState();
			return;
		}

		var avgPrice = _entryPrice;
		if (avgPrice > 0m)
		_entryPrice = avgPrice;

		if (Position > 0)
		{
			UpdateTrailingForLong(candle);
			TryExitLong(candle);
		}
		else if (Position < 0)
		{
			UpdateTrailingForShort(candle);
			TryExitShort(candle);
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value + trailingDistance;
		if (candle.HighPrice < activationPrice)
		return;

		_stopPrice = _entryPrice.Value + trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated long trailing stop at {_stopPrice:0.#####}.");
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingDistanceSteps <= 0 || _entryPrice is null || _trailingArmed)
		return;

		var trailingDistance = GetPriceOffset(TrailingDistanceSteps);
		if (trailingDistance <= 0m)
		return;

		var activationPrice = _entryPrice.Value - trailingDistance;
		if (candle.LowPrice > activationPrice)
		return;

		_stopPrice = _entryPrice.Value - trailingDistance;
		_trailingArmed = true;
		LogInfo($"Activated short trailing stop at {_stopPrice:0.#####}.");
	}

	private void TryExitLong(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.LowPrice > _stopPrice.Value)
		return;

		SellMarket(volume);
		ResetPositionState();
	}

	private void TryExitShort(ICandleMessage candle)
	{
		if (_stopPrice is null)
		return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (candle.HighPrice < _stopPrice.Value)
		return;

		BuyMarket(volume);
		ResetPositionState();
	}

	private decimal CalculateOrderVolume(decimal referencePrice)
	{
		var volume = Volume;

		if (RiskPercentage > 0m)
		{
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			var riskCapital = portfolioValue * RiskPercentage / 100m;

			if (riskCapital > 0m)
			{
				var margin = GetSecurityValue<decimal?>(Level1Fields.MarginBuy) ?? GetSecurityValue<decimal?>(Level1Fields.MarginSell) ?? 0m;

				if (margin > 0m)
				{
					volume = riskCapital / margin;
				}
				else if (referencePrice > 0m)
				{
					volume = riskCapital / referencePrice;
				}
			}
		}

		volume = RoundVolume(volume);

		// Ensure volume is at least the base Volume when calculation produces too small a value
		if (volume <= 0m)
			volume = Volume;

		var minVolume = Security?.MinVolume;
		if (minVolume != null && minVolume.Value > 0m && volume < minVolume.Value)
		{
			volume = minVolume.Value;
		}

		var maxVolume = Security?.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
		{
			volume = maxVolume.Value;
		}

		return volume;
	}

	private decimal RoundVolume(decimal volume)
	{
		if (volume <= 0m)
		{
			return 0m;
		}

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			if (steps <= 0m)
			{
				return step;
			}

			return steps * step;
		}

		return Math.Round(volume, 2, MidpointRounding.ToZero);
	}

	private bool HasCapacityForNewPosition(decimal volume)
	{
		if (MaxOpenPositions <= 0)
		{
			return true;
		}

		if (volume <= 0m)
		{
			return false;
		}

		var exposure = Math.Abs(Position);
		var maxExposure = MaxOpenPositions * volume;

		return exposure + volume <= maxExposure + volume * 0.0001m;
	}

	private decimal GetPriceOffset(int steps)
	{
		if (steps <= 0)
		{
			return 0m;
		}

		if (_priceStep > 0m)
		{
			return steps * _priceStep;
		}

		return steps;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_trailingArmed = false;
	}
}