Auf GitHub ansehen

Two MA RSI Strategie

Überblick

Die Two MA RSI Strategie ist eine Konvertierung des ursprünglichen MetaTrader-Expert Advisors "2MA_RSI". Sie verwendet eine Kreuzung eines schnellen und eines langsamen exponentiellen gleitenden Durchschnitts (EMA), bestätigt durch einen Relative Strength Index (RSI)-Filter. Orders werden mit einem Martingal-ähnlichen Geldverwaltungsblock dimensioniert, der das nächste Ordervolumen nach einem Verlust erhöht. Die StockSharp-Version arbeitet vollständig auf abgeschlossenen Kerzen und reproduziert das ursprüngliche Take-Profit- und Stop-Loss-Verhalten in Preispunkten.

Daten und Indikatoren

  • Die Strategie abonniert eine einzige Kerzenserie, definiert durch CandleType (standardmäßig 5-Minuten-Kerzen).
  • Drei Indikatoren werden auf jeder abgeschlossenen Bar berechnet:
    • FastLength EMA (auf den Kerzenschlusskurs angewendet).
    • SlowLength EMA.
    • RSI mit Länge RsiLength.
  • Historische Indikatorwerte werden intern gespeichert, um EMA-Kreuzungen zu erkennen, ohne Daten aus Indikatorpuffern abzurufen.

Einstiegslogik

  1. Die vorherige Kerze muss abgeschlossen sein, um eine Intrabar-Neubewertung zu vermeiden.
  2. Es ist keine aktive Position erlaubt (Position == 0).
  3. Long-Einstieg:
    • Die schnelle EMA kreuzt über die langsame EMA (schnelle EMA auf der aktuellen Bar ist größer als die langsame EMA, während die vorherige Bar schnelle EMA < langsame EMA hatte).
    • Der RSI-Wert liegt unter RsiOversold, was einen überverkauften Markt bestätigt.
  4. Short-Einstieg:
    • Die schnelle EMA kreuzt unter die langsame EMA mit der analogen Bedingung (schnelle EMA jetzt unter langsamer EMA, vorher darüber).
    • RSI liegt über RsiOverbought, was einen überkauften Markt signalisiert.
  5. Wenn alle Bedingungen erfüllt sind, sendet die Strategie eine Marktorder, die gemäß dem Martingal-Modul dimensioniert ist.

Ausstiegslogik

  • Ein Schutz-Stop-Loss und ein Take-Profit werden unmittelbar nach jedem Einstieg berechnet. Abstände werden in "Punkten" definiert und über den PriceStep des Instruments umgerechnet:
    • Long:
      • Stop Loss = Einstiegspreis - StopLossPoints * PriceStep.
      • Take Profit = Einstiegspreis + TakeProfitPoints * PriceStep.
    • Short:
      • Stop Loss = Einstiegspreis + StopLossPoints * PriceStep.
      • Take Profit = Einstiegspreis - TakeProfitPoints * PriceStep.
  • Nur diese Schutzlevels schließen einen Trade. Die Strategie wartet auf die nächste Kerze, um zu bestätigen, ob das Tief/Hoch das Ziel oder den Stop berührt hat, und sendet entsprechend eine ClosePosition()-Marktorder.
  • Die Ausstiegspriorität entspricht dem konservativen Verhalten des ursprünglichen Roboters: Ein Stop-Loss wird vor einem Take-Profit ausgewertet, wenn beide Levels in denselben Kerzenbereich fallen.

Positionsgrößenbestimmung und Martingal

  1. Das Basisvolumen wird bei jedem Einstieg berechnet als floor(balance / BalanceDivider) * VolumeStep. Der Wert bleibt immer bei oder über einem Volumenschritt und verwendet CurrentValue des Portfolios (fällt auf BeginValue zurück, wenn nötig).
  2. Nach jedem verlierenden Ausstieg erhöht sich die Martingal-Stufe um eins bis maximal MaxDoublings. Das nächste Ordervolumen wird mit 2^stage multipliziert.
  3. Jeder gewinnende Trade oder das Erreichen der maximalen Verdoppelungsanzahl setzt die Stufe auf null zurück und kehrt zum Basisvolumen zurück.
  4. Wenn MaxDoublings null oder negativ ist, erhöht sich die Größe nie und entspricht dem Basisvolumen.

Zusätzliches Verhalten

  • Die Strategie verfolgt intern frühere EMA-Werte und fragt keine historischen Indikatorwerte ab.
  • Orders werden nur ausgeführt, wenn die Strategie online ist, Indikatoren gebildet sind und der Handel erlaubt ist.
  • Die Diagrammausgabe zeichnet Preiskerzen, eigene Trades und die drei Indikatoren für die visuelle Analyse.

Parameter

Parameter Beschreibung Standard
FastLength Länge des schnellen EMA. 5
SlowLength Länge des langsamen EMA. 20
RsiLength Anzahl der Bars, die in der RSI-Berechnung verwendet werden. 14
RsiOverbought RSI-Niveau, das neue Longs blockiert und Shorts erlaubt. 70
RsiOversold RSI-Niveau, das Longs erlaubt. 30
StopLossPoints Stop-Loss-Abstand in Preisschritten ausgedrückt. 500
TakeProfitPoints Take-Profit-Abstand in Preisschritten. 1500
BalanceDivider Dividiert den Portfoliowert zur Ermittlung der Basisordergröße. 1000
MaxDoublings Maximale Anzahl von Martingal-Verdoppelungen nach aufeinanderfolgenden Verlusten. 1
CandleType Von der Strategie verwendete Kerzenserie. 5-Minuten-Zeitrahmen

Verwendungshinweise

  • Ein Portfolio und ein Wertpapier mit gültigen PriceStep- und VolumeStep-Metadaten bereitstellen, damit das punktebasierte Risikomanagement und die Positionsgrößenbestimmung konsistent bleiben.
  • Da Marktorders für Ausstiege verwendet werden, sind Slippage und Spreads im Vergleich zu den Limit-Orders der MetaTrader-Version möglich, aber die Stop/Take-Auswertungslogik ist erhalten.
  • Die Strategie erstellt keine Python-Version; nur die C#-Implementierung wird wie angefordert geliefert.
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>
/// Moving average crossover strategy with RSI confirmation and martingale sizing.
/// </summary>
public class TwoMaRsiStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _rsiOverbought;
	private readonly StrategyParam<decimal> _rsiOversold;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _balanceDivider;
	private readonly StrategyParam<int> _maxDoublings;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _slowEma;
	private RelativeStrengthIndex _rsi;

	private decimal? _previousFast;
	private decimal? _previousSlow;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takeProfitPrice;
	private int _martingaleStage;
	private bool _isClosing;

	/// <summary>
	/// Initializes a new instance of the <see cref="TwoMaRsiStrategy"/> class.
	/// </summary>
	public TwoMaRsiStrategy()
	{
		_fastLength = Param(nameof(FastLength), 5)
			.SetDisplay("Fast EMA Length", "Length of the fast exponential moving average", "Indicators")
			
			.SetOptimize(2, 20, 1);

		_slowLength = Param(nameof(SlowLength), 20)
			.SetDisplay("Slow EMA Length", "Length of the slow exponential moving average", "Indicators")
			
			.SetOptimize(10, 60, 5);

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "Number of bars for the RSI calculation", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_rsiOverbought = Param(nameof(RsiOverbought), 50m)
			.SetDisplay("RSI Overbought", "Upper RSI threshold for short entries", "Signals");

		_rsiOversold = Param(nameof(RsiOversold), 50m)
			.SetDisplay("RSI Oversold", "Lower RSI threshold for long entries", "Signals");

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetDisplay("Stop Loss (points)", "Stop loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 1500m)
			.SetDisplay("Take Profit (points)", "Take profit distance in price steps", "Risk");

		_balanceDivider = Param(nameof(BalanceDivider), 1000m)
			.SetDisplay("Balance Divider", "Divides portfolio value to estimate base order volume", "Money Management");

		_maxDoublings = Param(nameof(MaxDoublings), 1)
			.SetDisplay("Max Doublings", "Maximum number of martingale doublings", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series for the strategy", "General");
	}

	/// <summary>
	/// Fast EMA length.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	/// <summary>
	/// RSI period.
	/// </summary>
	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	/// <summary>
	/// Overbought threshold for RSI.
	/// </summary>
	public decimal RsiOverbought
	{
		get => _rsiOverbought.Value;
		set => _rsiOverbought.Value = value;
	}

	/// <summary>
	/// Oversold threshold for RSI.
	/// </summary>
	public decimal RsiOversold
	{
		get => _rsiOversold.Value;
		set => _rsiOversold.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Divider applied to the portfolio value to calculate the base order volume.
	/// </summary>
	public decimal BalanceDivider
	{
		get => _balanceDivider.Value;
		set => _balanceDivider.Value = value;
	}

	/// <summary>
	/// Maximum number of martingale doublings.
	/// </summary>
	public int MaxDoublings
	{
		get => _maxDoublings.Value;
		set => _maxDoublings.Value = value;
	}

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

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

		_fastEma = null;
		_slowEma = null;
		_rsi = null;
		_previousFast = null;
		_previousSlow = null;
		_entryPrice = default;
		_stopPrice = default;
		_takeProfitPrice = default;
		_martingaleStage = 0;
		_isClosing = false;
	}

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

		_fastEma = new ExponentialMovingAverage
		{
			Length = FastLength
		};

		_slowEma = new ExponentialMovingAverage
		{
			Length = SlowLength
		};

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiLength
		};

		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;

		if (Position == 0 && _isClosing)
		{
			_isClosing = false;
			_entryPrice = default;
			_stopPrice = default;
			_takeProfitPrice = default;
		}

		var fastResult = _fastEma.Process(candle);
		var slowResult = _slowEma.Process(candle);
		var rsiResult = _rsi.Process(candle);

		if (fastResult.IsEmpty || slowResult.IsEmpty || rsiResult.IsEmpty)
		{
			return;
		}

		if (!_fastEma.IsFormed || !_slowEma.IsFormed || !_rsi.IsFormed)
		{
			_previousFast = fastResult.GetValue<decimal>();
			_previousSlow = slowResult.GetValue<decimal>();
			return;
		}

		var fast = fastResult.GetValue<decimal>();
		var slow = slowResult.GetValue<decimal>();
		var rsi = rsiResult.GetValue<decimal>();
		var point = GetPoint();

		if (Position > 0)
		{
			var stopHit = candle.LowPrice <= _stopPrice;
			var takeHit = candle.HighPrice >= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (Position < 0)
		{
			var stopHit = candle.HighPrice >= _stopPrice;
			var takeHit = candle.LowPrice <= _takeProfitPrice;

			if (!_isClosing && stopHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterLoss();
			}
			else if (!_isClosing && takeHit)
			{
				_isClosing = true;
				ClosePosition();
				RegisterWin();
			}
		}
		else if (!_isClosing)
		{
			if (_previousFast is null || _previousSlow is null)
			{
				_previousFast = fast;
				_previousSlow = slow;
				return;
			}

			var prevFast = _previousFast.Value;
			var prevSlow = _previousSlow.Value;

			var crossUp = prevFast < prevSlow && fast > slow && rsi < RsiOversold;
			var crossDown = prevFast > prevSlow && fast < slow && rsi > RsiOverbought;

			if (crossUp)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					BuyMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice - StopLossPoints * point;
					_takeProfitPrice = _entryPrice + TakeProfitPoints * point;
				}
			}
			else if (crossDown)
			{
				var volume = CalculateOrderVolume();
				if (volume > 0m)
				{
					SellMarket(volume);
					_entryPrice = candle.ClosePrice;
					_stopPrice = _entryPrice + StopLossPoints * point;
					_takeProfitPrice = _entryPrice - TakeProfitPoints * point;
				}
			}
		}

		_previousFast = fast;
		_previousSlow = slow;
	}

	private decimal GetPoint()
	{
		var step = Security?.PriceStep ?? 1m;
		return step > 0m ? step : 1m;
	}

	private decimal CalculateOrderVolume()
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var baseVolume = step;
		var divider = BalanceDivider;
		var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (divider > 0m && balance > 0m)
		{
			var count = Math.Floor((double)(balance / divider));
			baseVolume = (decimal)count * step;
			if (baseVolume < step)
				baseVolume = step;
		}

		var multiplier = CalculateMartingaleMultiplier();
		var volume = baseVolume * multiplier;

		if (volume < step)
			volume = step;

		var ratio = volume / step;
		volume = Math.Ceiling(ratio) * step;

		return volume;
	}

	private decimal CalculateMartingaleMultiplier()
	{
		if (MaxDoublings <= 0 || _martingaleStage <= 0)
			return 1m;

		var stage = Math.Min(_martingaleStage, MaxDoublings);
		return (decimal)Math.Pow(2d, stage);
	}

	private void RegisterWin()
	{
		_martingaleStage = 0;
	}

	private void ClosePosition()
	{
		if (Position > 0)
			SellMarket(Position);
		else if (Position < 0)
			BuyMarket(-Position);
	}

	private void RegisterLoss()
	{
		if (MaxDoublings <= 0)
		{
			_martingaleStage = 0;
			return;
		}

		if (_martingaleStage < MaxDoublings)
		{
			_martingaleStage++;
		}
		else
		{
			_martingaleStage = 0;
		}
	}
}