Auf GitHub ansehen

MA + RSI Wizard-Strategie

Überblick

Diese Strategie ist der StockSharp-Port des MetaTrader 5-Experten "MQL5 Wizard MA RSI" aus dem Ordner MQL/17489. Der ursprüngliche Roboter kombiniert einen gleitenden Durchschnittsfilter mit einem RSI-Filter und öffnet Trades, wenn die gewichtete Summe der Filter konfigurierbare Schwellenwerte kreuzt. Die C#-Version behält dieselbe Struktur bei und drückt die Logik mit der High-Level-API von StockSharp und modernen Risikomanagement-Helfern aus.

Der Bot arbeitet auf jedem Instrument, das OHLCV-Kerzen bereitstellt. Er wertet einen gleitenden Durchschnitt aus, der um eine benutzerdefinierte Anzahl von Bars verschoben werden kann, und einen RSI, der mit verschiedenen Preisquellen gespeist werden kann. Beide Indikatoren tragen zu einer zusammengesetzten Punktzahl bei. Eine Position wird geöffnet, sobald die Punktzahl den Öffnungsschwellenwert überschreitet, und geschlossen, wenn die entgegengesetzte Punktzahl den Schließschwellenwert erreicht. Optionale Abstands-, Stop-Loss- und Take-Profit-Einstellungen replizieren die Geldmanagement-Parameter des ursprünglichen Expert Advisors.

Indikatoren und Scoring

  • Gleitender Durchschnitt – konfigurierbarer Zeitraum, Methode (einfach, exponentiell, geglättet, linear gewichtet), Preisquelle und Vorwärtsverschiebung. Wenn der Schlusskurs über dem verschobenen Durchschnitt liegt, entspricht der MA-Score 100, andernfalls 0.
  • Relative Strength Index (RSI) – konfigurierbarer Zeitraum und Preisquelle. Der RSI-Beitrag wächst linear von 0 bei RSI = 50 auf 100 bei RSI = 100 für Long-Signale und spiegelt dasselbe Verhalten für Short-Signale wider.
  • Zusammengesetzte Punktzahl – die MA- und RSI-Scores werden durch MaWeight und RsiWeight gewichtet. Der Endwert ist der gewichtete Durchschnitt score = (maScore * MaWeight + rsiScore * RsiWeight) / (MaWeight + RsiWeight), der im Intervall [0;100] bleibt, genau wie in der MetaTrader-Version.
  • PreisabstandsfilterPriceLevelPoints definiert den Mindestabstand zwischen dem Kerzenschlusskurs und dem verschobenen gleitenden Durchschnitt (in Preis umgerechnet über den Instrument-Schritt). Signale, die näher als der Schwellenwert liegen, werden ignoriert.

Handelsregeln

  1. Jede abgeschlossene Kerze aktualisiert die Indikatoren und Scores.
  2. Wenn der entgegengesetzte Score ThresholdClose verletzt, wird die aktuelle Position zum Markt geschlossen.
  3. Long-Einstieg – erlaubt, wenn kein Long-Exposure besteht, der Long-Score mindestens ThresholdOpen ist, die Abklingzeit (ExpirationBars) vergangen ist und der Preisabstandsfilter erfüllt ist. Die Ordergröße entspricht Volume + |Position|, was automatisch eine Short-Position dreht, wenn nötig.
  4. Short-Einstieg – symmetrisch zur Long-Logik.
  5. Optionale StartProtection wendet Stop-Loss und Take-Profit unter Verwendung absoluter Preispunkte an.

Risikomanagement

Die Strategie aktiviert StartProtection einmal beim Start. Abstände werden in Preispunkten definiert (StopLevelPoints, TakeLevelPoints) und mit dem aktuellen Security.PriceStep übersetzt. Beide Werte können auf null gesetzt werden, um den entsprechenden Schutz zu deaktivieren. Der Abklingzeitparameter verhindert sofortige Wiedereinstiege in dieselbe Richtung und emuliert die Einstellung für das Auslaufen von Pending Orders des ursprünglichen EA.

Parameter

Parameter Beschreibung Standardwerte
CandleType Für die Analyse verwendete Datenserie. 15-Minuten-Zeitrahmen
ThresholdOpen Minimale gewichtete Punktzahl zum Öffnen einer Position. 55
ThresholdClose Minimale entgegengesetzte Punktzahl zum Schließen einer Position. 100
PriceLevelPoints Erforderlicher Abstand zwischen Preis und verschobenem MA (in Punkten). 0
StopLevelPoints Stop-Loss-Abstand (Punkte). 50
TakeLevelPoints Take-Profit-Abstand (Punkte). 50
ExpirationBars Abklingzeit in Bars vor dem Wiedereinstieg in dieselbe Richtung. 4
MaPeriod Gleitender Durchschnittszeitraum. 20
MaShift Auf den MA-Ausgang angewendete Vorwärtsverschiebung (Bars). 3
MaMethods Gleitende Durchschnittsmethode (Simple, Exponential, Smoothed, LinearWeighted). Simple
MaAppliedPrice Preisquelle für den MA. Close
MaWeight Dem MA-Score zugewiesenes Gewicht. 0.8
RsiPeriod RSI-Zeitraum. 3
RsiAppliedPrice Preisquelle für den RSI. Close
RsiWeight Dem RSI-Score zugewiesenes Gewicht. 0.5

Hinweise

  • Die Strategie läuft strikt auf abgeschlossenen Kerzen und ignoriert Teilaktualisierungen.
  • Das Setzen beider Indikatorgewichte auf null deaktiviert den Handel, da die kombinierte Punktzahl die Schwellenwerte nicht mehr erreichen kann.
  • Abklingzeit (ExpirationBars) gleich null erlaubt mehrere Einträge in dieselbe Richtung ohne Wartezeit.
  • Da StockSharp standardmäßig Marktorders ausführt, wird das Auslaufen von Pending Orders des ursprünglichen EA durch den Abklingzeitmechanismus anstelle der tatsächlichen Orderstornierung dargestellt.
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 plus RSI strategy converted from the MQL5 Wizard template.
/// The strategy computes weighted scores from a shifted moving average and RSI momentum.
/// </summary>
public class MaRsiWizardStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation methods supported by the strategy.
	/// </summary>
	public enum MaMethods
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted
	}

	/// <summary>
	/// Price sources compatible with the indicators used in the strategy.
	/// </summary>
	public enum AppliedPrices
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted
	}
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _thresholdOpen;
	private readonly StrategyParam<int> _thresholdClose;
	private readonly StrategyParam<decimal> _priceLevelPoints;
	private readonly StrategyParam<int> _stopLevelPoints;
	private readonly StrategyParam<int> _takeLevelPoints;
	private readonly StrategyParam<int> _expirationBars;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<MaMethods> _maMethod;
	private readonly StrategyParam<AppliedPrices> _maAppliedPrice;
	private readonly StrategyParam<decimal> _maWeight;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<AppliedPrices> _rsiAppliedPrice;
	private readonly StrategyParam<decimal> _rsiWeight;

	private DecimalLengthIndicator _ma = null!;
	private RelativeStrengthIndex _rsi = null!;
	private readonly Queue<decimal> _maShiftBuffer = new();

	private int _barIndex;
	private int? _lastLongEntryBar;
	private int? _lastShortEntryBar;

	/// <summary>
	/// Initializes a new instance of the <see cref="MaRsiWizardStrategy"/>.
	/// </summary>
	public MaRsiWizardStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for incoming candles", "General");

		_thresholdOpen = Param(nameof(ThresholdOpen), 75)
			.SetRange(0, 100)
			.SetDisplay("Open Threshold", "Weighted score required to open a position", "Signals")
			;

		_thresholdClose = Param(nameof(ThresholdClose), 100)
			.SetRange(0, 100)
			.SetDisplay("Close Threshold", "Weighted score required to exit an existing position", "Signals")
			;

		_priceLevelPoints = Param(nameof(PriceLevelPoints), 0m)
			.SetDisplay("Price Level (points)", "Minimum distance between price and moving average", "Signals")
			;

		_stopLevelPoints = Param(nameof(StopLevelPoints), 50)
			.SetDisplay("Stop Loss (points)", "Protective stop distance expressed in price points", "Risk")
			;

		_takeLevelPoints = Param(nameof(TakeLevelPoints), 50)
			.SetDisplay("Take Profit (points)", "Profit target distance expressed in price points", "Risk")
			;

		_expirationBars = Param(nameof(ExpirationBars), 24)
			.SetDisplay("Signal Cooldown (bars)", "Bars to wait before allowing a new trade in the same direction", "Signals")
			;

		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average period", "Moving Average")
			;

		_maShift = Param(nameof(MaShift), 3)
			.SetRange(0, 100)
			.SetDisplay("MA Shift", "Lag applied to the moving average output", "Moving Average")
			;

		_maMethod = Param(nameof(MaMethods), MaMethods.Simple)
			.SetDisplay("MA Method", "Moving average calculation method", "Moving Average");

		_maAppliedPrice = Param(nameof(MaAppliedPrice), AppliedPrices.Close)
			.SetDisplay("MA Source", "Price type used for the moving average", "Moving Average");

		_maWeight = Param(nameof(MaWeight), 0.8m)
			.SetDisplay("MA Weight", "Contribution of the moving average score", "Signals")
			.SetRange(0m, 1m)
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "RSI calculation length", "RSI")
			;

		_rsiAppliedPrice = Param(nameof(RsiAppliedPrice), AppliedPrices.Close)
			.SetDisplay("RSI Source", "Price type used for RSI", "RSI");

		_rsiWeight = Param(nameof(RsiWeight), 0.5m)
			.SetDisplay("RSI Weight", "Contribution of the RSI score", "Signals")
			.SetRange(0m, 1m)
			;
	}

	/// <summary>
	/// Type of candles used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Weighted score required to open a new position.
	/// </summary>
	public int ThresholdOpen
	{
		get => _thresholdOpen.Value;
		set => _thresholdOpen.Value = value;
	}

	/// <summary>
	/// Weighted score required to close the current position.
	/// </summary>
	public int ThresholdClose
	{
		get => _thresholdClose.Value;
		set => _thresholdClose.Value = value;
	}

	/// <summary>
	/// Minimum price distance from the moving average expressed in points.
	/// </summary>
	public decimal PriceLevelPoints
	{
		get => _priceLevelPoints.Value;
		set => _priceLevelPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in points.
	/// </summary>
	public int StopLevelPoints
	{
		get => _stopLevelPoints.Value;
		set => _stopLevelPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in points.
	/// </summary>
	public int TakeLevelPoints
	{
		get => _takeLevelPoints.Value;
		set => _takeLevelPoints.Value = value;
	}

	/// <summary>
	/// Cooldown measured in bars before a new trade in the same direction is allowed.
	/// </summary>
	public int ExpirationBars
	{
		get => _expirationBars.Value;
		set => _expirationBars.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Number of bars used to lag the moving average output.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

	/// <summary>
	/// Moving average calculation method.
	/// </summary>
	public MaMethods MaMethod
	{
		get => _maMethod.Value;
		set => _maMethod.Value = value;
	}

	/// <summary>
	/// Price source used for the moving average.
	/// </summary>
	public AppliedPrices MaAppliedPrice
	{
		get => _maAppliedPrice.Value;
		set => _maAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the moving average score in the weighted decision.
	/// </summary>
	public decimal MaWeight
	{
		get => _maWeight.Value;
		set => _maWeight.Value = value;
	}

	/// <summary>
	/// RSI calculation length.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Price source used for the RSI indicator.
	/// </summary>
	public AppliedPrices RsiAppliedPrice
	{
		get => _rsiAppliedPrice.Value;
		set => _rsiAppliedPrice.Value = value;
	}

	/// <summary>
	/// Contribution of the RSI score in the weighted decision.
	/// </summary>
	public decimal RsiWeight
	{
		get => _rsiWeight.Value;
		set => _rsiWeight.Value = value;
	}

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

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

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;
	}

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

		_maShiftBuffer.Clear();
		_barIndex = 0;
		_lastLongEntryBar = null;
		_lastShortEntryBar = null;

		_ma = CreateMovingAverage(MaMethod, MaPeriod);
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

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

		var step = Security.PriceStep ?? 1m;

		Unit takeProfit = TakeLevelPoints > 0
			? new Unit(TakeLevelPoints * step, UnitTypes.Absolute)
			: null;

		Unit stopLoss = StopLevelPoints > 0
			? new Unit(StopLevelPoints * step, UnitTypes.Absolute)
			: null;

		if (stopLoss != null || takeProfit != null)
			StartProtection(stopLoss ?? new Unit(), takeProfit ?? new Unit());

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

		var rsiArea = CreateChartArea();
		if (rsiArea != null)
		{
			rsiArea.Title = "RSI";
			DrawIndicator(rsiArea, _rsi);
		}
	}

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

		// removed IsFormedAndOnlineAndAllowTrading for backtesting

		_barIndex++;

		var maInput = SelectAppliedPrice(candle, MaAppliedPrice);
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, maInput, candle.OpenTime) { IsFinal = true });
		if (!maValue.IsFinal || maValue is not DecimalIndicatorValue maResult)
			return;

		var rsiInput = SelectAppliedPrice(candle, RsiAppliedPrice);
		var rsiValue = _rsi.Process(new DecimalIndicatorValue(_rsi, rsiInput, candle.OpenTime) { IsFinal = true });
		if (!rsiValue.IsFinal || rsiValue is not DecimalIndicatorValue rsiResult)
			return;

		var referenceMa = UpdateAndGetShiftedMa(maResult.Value);
		if (referenceMa == null)
			return;

		var currentPrice = candle.ClosePrice;
		var step = Security.PriceStep ?? 1m;
		var priceOffset = PriceLevelPoints * step;

		if (PriceLevelPoints > 0 && Math.Abs(currentPrice - referenceMa.Value) < priceOffset)
			return;

		var maLongSignal = currentPrice > referenceMa.Value ? 100m : 0m;
		var maShortSignal = currentPrice < referenceMa.Value ? 100m : 0m;

		var rsi = rsiResult.Value;
		var rsiLongSignal = rsi > 50m ? Math.Min(100m, (rsi - 50m) * 2m) : 0m;
		var rsiShortSignal = rsi < 50m ? Math.Min(100m, (50m - rsi) * 2m) : 0m;

		var weightSum = MaWeight + RsiWeight;
		if (weightSum <= 0m)
			return;

		var longScore = (MaWeight * maLongSignal + RsiWeight * rsiLongSignal) / weightSum;
		var shortScore = (MaWeight * maShortSignal + RsiWeight * rsiShortSignal) / weightSum;

		if (Position > 0 && shortScore >= ThresholdClose)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0 && longScore >= ThresholdClose)
		{
			BuyMarket(Math.Abs(Position));
		}

		var allowLong = ExpirationBars <= 0 || _lastLongEntryBar == null || _barIndex - _lastLongEntryBar >= ExpirationBars;
		var allowShort = ExpirationBars <= 0 || _lastShortEntryBar == null || _barIndex - _lastShortEntryBar >= ExpirationBars;

		if (Position <= 0 && longScore >= ThresholdOpen && allowLong)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				BuyMarket(volume);
				_lastLongEntryBar = _barIndex;
			}
			return;
		}

		if (Position >= 0 && shortScore >= ThresholdOpen && allowShort)
		{
			var volume = Volume + Math.Abs(Position);
			if (volume > 0)
			{
				SellMarket(volume);
				_lastShortEntryBar = _barIndex;
			}
		}
	}

	private decimal? UpdateAndGetShiftedMa(decimal maValue)
	{
		var shift = Math.Max(0, MaShift);
		if (shift == 0)
		{
			return maValue;
		}

		_maShiftBuffer.Enqueue(maValue);

		if (_maShiftBuffer.Count <= shift)
			return null;

		if (_maShiftBuffer.Count > shift + 1)
			_maShiftBuffer.Dequeue();

		return _maShiftBuffer.Count == shift + 1 ? _maShiftBuffer.Peek() : (decimal?)null;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaMethods method, int period)
	{
		return method switch
		{
			MaMethods.Simple => new SMA { Length = period },
			MaMethods.Exponential => new EMA { Length = period },
			MaMethods.Smoothed => new SmoothedMovingAverage { Length = period },
			MaMethods.LinearWeighted => new WeightedMovingAverage { Length = period },
			_ => new SMA { Length = period }
		};
	}

	private static decimal SelectAppliedPrice(ICandleMessage candle, AppliedPrices price)
	{
		return price switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}