Auf GitHub ansehen

Up3x1 Strategie mit dynamischer Positionsgrößenbestimmung

Überblick

  • Konvertierung des MetaTrader 5 Expert Advisors up3x1.mq5 in die StockSharp High-Level-API.
  • Handelt einen Triple-EMA-Crossover (exponentieller gleitender Durchschnitt) mit Stop-Loss-, Take-Profit- und Trailing-Stop-Management.
  • Verarbeitet nur abgeschlossene Kerzen, um den ursprünglichen iTickVolume(0) > 1-Schutz zu emulieren, der eine Entscheidung pro Bar erzwang.
  • Standard-Kerzenserie ist 1 Stunde, aber der Zeitrahmen ist über den CandleType-Parameter konfigurierbar.

Handelslogik

  1. Indikatoren
    • Schnelle EMA (FastPeriod, Standard 24).
    • Mittlere EMA (MediumPeriod, Standard 60).
    • Langsame EMA (SlowPeriod, Standard 120).
  2. Long-Einstieg
    • Vorherige Bar: schnelle EMA unterhalb der mittleren EMA und mittlere unterhalb der langsamen (EMAfast₍t-1₎ < EMAmedium₍t-1₎ < EMAslow₍t-1₎).
    • Aktuelle Bar: mittlere EMA unterhalb der schnellen EMA, während die schnelle unterhalb der langsamen bleibt (EMAmedium₍t₎ < EMAfast₍t₎ < EMAslow₍t₎).
  3. Short-Einstieg
    • Vorherige Bar: schnelle EMA oberhalb der mittleren EMA und mittlere oberhalb der langsamen (EMAfast₍t-1₎ > EMAmedium₍t-1₎ > EMAslow₍t-1₎).
    • Aktuelle Bar: mittlere EMA kreuzt über die schnelle EMA, während beide über der langsamen EMA bleiben (EMAmedium₍t₎ > EMAfast₍t₎ > EMAslow₍t₎).
  4. Ausstiegslogik für beide Richtungen
    • Take Profit, wenn der Preis TakeProfitOffset vom Einstieg voranzieht (Kerzenhoch für Longs, -tief für Shorts).
    • Stop Loss, wenn der Preis StopLossOffset vom Einstieg zurückzieht (Kerzentief für Longs, -hoch für Shorts).
    • Trailing Stop aktiviert sich, sobald sich die Position um mehr als TrailingStopOffset günstig bewegt, und folgt dem Preis in diesem festen Abstand, bewertet an Kerzenextremen.
    • Fallback-Ausstieg, wenn die schnelle EMA wieder unter die mittlere EMA kreuzt, während beide über der langsamen EMA bleiben (spiegelt die ma_one_1 > ma_two_1 > ma_three_1-Prüfung der MQL-Version wider).

Positionsgrößenbestimmung und Risikomanagement

  • RiskFraction (Standard 0.02) multipliziert den aktuellen Portfoliowert, um die ursprüngliche FreeMargin * 0.02 / 1000-Lot-Größenbestimmung zu approximieren.
  • BaseVolume (Standard 0.1) dient als Fallback, wenn Portfolio-Daten nicht verfügbar sind oder die berechnete Größe nicht positiv wird.
  • Nach mehr als einem verlierenden Ausstieg wird das Volumen um volume * losses / 3 reduziert, was den kumulativen losses-Zähler des Skripts imitiert (der Zähler wird nach profitablen Trades nicht zurückgesetzt, wie im Originalcode).
  • Volumen wird auf Security.VolumeStep abgerundet, durch Security.MinVolume / Security.MaxVolume begrenzt und auf null gesetzt, wenn das Instrumentminimum nicht erfüllt werden kann.

Parameter

Parameter Standard Beschreibung
FastPeriod 24 Länge der schnellsten EMA.
MediumPeriod 60 Länge der mittleren EMA.
SlowPeriod 120 Länge der langsamen EMA als Langzeittrend-Filter.
TakeProfitOffset 0.015 Absoluter Preisabstand für die Take-Profit-Order (an Instrument-Quotierung anpassen).
StopLossOffset 0.01 Absoluter Preisabstand für die Stop-Loss-Order.
TrailingStopOffset 0.004 Trailing-Distanz, die Gewinne sichert, sobald der Preis ausreichend voranzieht; auf 0 setzen zum Deaktivieren.
BaseVolume 0.1 Fallback-Handelsgröße, wenn dynamische Größenbestimmung nicht berechnet werden kann.
RiskFraction 0.02 Anteil des Portfoliowerts, der auf die dynamische Größenformel angewendet wird.
CandleType 1-Stunden-Zeitrahmen Kerzenserie für Indikatorberechnungen und Entscheidungsfindung.

Konvertierungshinweise

  • Trailing Stop und Schutzausstiege verwenden Kerzenhochs/-tiefs statt roher Ticks, da die High-Level-API abgeschlossene Kerzen verarbeitet; dies hält das Verhalten über Backtests und Live-Runs deterministisch.
  • Stop Loss und Take Profit werden über Markt-Flattening-Befehle am bewerteten Schwellenwert ausgeführt statt durch separate Schutzorders, was Kompatibilität mit dem High-Level-Strategiefluss sicherstellt.
  • Dynamische Positionsgrößenbestimmung hängt von Portfolio.CurrentValue ab. Wenn nicht verfügbar, fällt die Strategie auf BaseVolume zurück, ähnlich dem ursprünglichen LotCheck-Fallback auf den manuellen Lots-Eingang.
  • Der losses-Zähler ist absichtlich kumulativ (wird nie bei gewinnenden Trades zurückgesetzt), um der MQL-Implementierung zu folgen.
  • Alle Kommentare sind auf Englisch gemäß Projektrichtlinien.

Verwendungstipps

  1. Hängen Sie die Strategie an ein Instrument und Portfolio, dann konfigurieren Sie CandleType entsprechend der Chart-Auflösung, die Sie von MT5 emulieren möchten.
  2. Überprüfen Sie die Preisoffsets, damit sie die Tick-Größe Ihres Instruments widerspiegeln (z.B. entspricht für ein 5-stelliges Forex-Paar 0.015 150 Punkten wie im Quell-Expert).
  3. Stimmen Sie RiskFraction / BaseVolume ab, um realistische Positionsgrößen relativ zu Ihrem Konto zu erreichen.
  4. Optional: Trailing deaktivieren durch Setzen von TrailingStopOffset auf null.
  5. Überwachen Sie Protokolle auf Nachrichten wie "Enter long" oder "Exit short", die die MetaTrader Print-Diagnosen spiegeln.

Repository-Struktur

API/2512_Up3x1/
├── CS/Up3x1DynamicSizingStrategy.cs      # Konvertierte C#-Strategie
├── README.md                # Englische Dokumentation (diese Datei)
├── README_zh.md             # Chinesische Übersetzung
└── README_ru.md             # Russische Übersetzung

Haftungsausschluss

Der Handel birgt erhebliche Risiken. Dieses Beispiel dient nur zu Bildungszwecken und sollte an historischen und simulierten Daten validiert werden, bevor es live eingesetzt wird.

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>
/// EMA triple crossover strategy converted from the MetaTrader 5 "up3x1" expert.
/// Uses three exponential moving averages with optional stop loss, take profit and trailing logic.
/// Position size is reduced after losing trades similar to the original lot optimization routine.
/// </summary>
public class Up3x1DynamicSizingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _mediumPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _takeProfitOffset;
	private readonly StrategyParam<decimal> _stopLossOffset;
	private readonly StrategyParam<decimal> _trailingStopOffset;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _riskFraction;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _mediumEma;
	private ExponentialMovingAverage _slowEma;

	private bool _hasPrevValues;
	private decimal _prevFast;
	private decimal _prevMedium;
	private decimal _prevSlow;

	private decimal _entryPrice;
	private decimal _highestPrice;
	private decimal _lowestPrice;

	private int _losses;

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Medium EMA period.
	/// </summary>
	public int MediumPeriod
	{
		get => _mediumPeriod.Value;
		set => _mediumPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Absolute price distance for take profit.
	/// </summary>
	public decimal TakeProfitOffset
	{
		get => _takeProfitOffset.Value;
		set => _takeProfitOffset.Value = value;
	}

	/// <summary>
	/// Absolute price distance for stop loss.
	/// </summary>
	public decimal StopLossOffset
	{
		get => _stopLossOffset.Value;
		set => _stopLossOffset.Value = value;
	}

	/// <summary>
	/// Absolute trailing stop distance.
	/// </summary>
	public decimal TrailingStopOffset
	{
		get => _trailingStopOffset.Value;
		set => _trailingStopOffset.Value = value;
	}

	/// <summary>
	/// Base volume used when dynamic sizing cannot be calculated.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of portfolio value used for dynamic position sizing.
	/// </summary>
	public decimal RiskFraction
	{
		get => _riskFraction.Value;
		set => _riskFraction.Value = value;
	}

	/// <summary>
	/// Candle type to subscribe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public Up3x1DynamicSizingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 24)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fastest EMA", "Indicators");

		_mediumPeriod = Param(nameof(MediumPeriod), 60)
			.SetGreaterThanZero()
			.SetDisplay("Medium EMA", "Period of the middle EMA", "Indicators");

		_slowPeriod = Param(nameof(SlowPeriod), 120)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slowest EMA", "Indicators");

		_takeProfitOffset = Param(nameof(TakeProfitOffset), 0.015m)
			.SetDisplay("Take Profit", "Absolute take profit distance in price units", "Risk");

		_stopLossOffset = Param(nameof(StopLossOffset), 0.01m)
			.SetDisplay("Stop Loss", "Absolute stop loss distance in price units", "Risk");

		_trailingStopOffset = Param(nameof(TrailingStopOffset), 0.004m)
			.SetDisplay("Trailing", "Trailing stop distance that follows price", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetDisplay("Base Volume", "Fallback trade volume if dynamic sizing fails", "Money Management");

		_riskFraction = Param(nameof(RiskFraction), 0.02m)
			.SetDisplay("Risk Fraction", "Fraction of portfolio value used for sizing", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for calculations", "General");
	}

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

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

		Volume = BaseVolume;
		ResetState();
		_losses = 0;
		_hasPrevValues = false;
		_prevFast = 0m;
		_prevMedium = 0m;
		_prevSlow = 0m;
	}

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

		Volume = BaseVolume;

		_fastEma = new EMA
		{
			Length = FastPeriod
		};

		_mediumEma = new EMA
		{
			Length = MediumPeriod
		};

		_slowEma = new EMA
		{
			Length = SlowPeriod
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(_fastEma, _mediumEma, _slowEma, ProcessCandle)
			.Start();

		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_fastEma.IsFormed || !_mediumEma.IsFormed || !_slowEma.IsFormed)
			return;

		if (!_hasPrevValues)
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			_hasPrevValues = true;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			return;
		}

		if (Position > 0)
		{
			if (TryHandleLongExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else if (Position < 0)
		{
			if (TryHandleShortExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else
		{
			var bullishSetup = _prevFast < _prevMedium && _prevMedium < _prevSlow && mediumValue < fastValue && fastValue < slowValue;
			var bearishSetup = _prevFast > _prevMedium && _prevMedium > _prevSlow && mediumValue > fastValue && fastValue > slowValue;

			if (bullishSetup)
			{
				TryEnterLong(candle);
			}
			else if (bearishSetup)
			{
				TryEnterShort(candle);
			}
		}

		_prevFast = fastValue;
		_prevMedium = mediumValue;
		_prevSlow = slowValue;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped long entry because calculated volume is below minimum.");
			return;
		}

		BuyMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter long at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped short entry because calculated volume is below minimum.");
			return;
		}

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter short at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private bool TryHandleLongExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice + TakeProfitOffset;
			if (candle.HighPrice >= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice - StopLossOffset;
			if (candle.LowPrice <= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_highestPrice = candle.HighPrice > _highestPrice ? candle.HighPrice : _highestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _highestPrice - _entryPrice > TrailingStopOffset)
		{
			var trail = _highestPrice - TrailingStopOffset;
			if (candle.LowPrice <= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private bool TryHandleShortExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice - TakeProfitOffset;
			if (candle.LowPrice <= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice + StopLossOffset;
			if (candle.HighPrice >= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_lowestPrice = _lowestPrice == 0m || candle.LowPrice < _lowestPrice ? candle.LowPrice : _lowestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _entryPrice - _lowestPrice > TrailingStopOffset)
		{
			var trail = _lowestPrice + TrailingStopOffset;
			if (candle.HighPrice >= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private void ExitPosition(decimal exitPrice, string reason)
	{
		var isLong = Position > 0;
		var volume = Math.Abs(Position);

		if (volume <= 0m)
			return;

		var pnl = isLong
			? (exitPrice - _entryPrice) * volume
			: (_entryPrice - exitPrice) * volume;

		if (isLong)
		{
			SellMarket();
		}
		else
		{
			BuyMarket();
		}

		LogInfo($"Exit {(isLong ? "long" : "short")} at {exitPrice} because {reason}. Approx PnL: {pnl}.");

		if (pnl < 0m)
			_losses++;

		ResetState();
	}

	private decimal CalculateOrderVolume()
	{
		var volume = 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;

		if (portfolioValue > 0m && RiskFraction > 0m)
			volume = portfolioValue * RiskFraction / 1000m;

		if (volume <= 0m)
			volume = BaseVolume;

		if (_losses > 1)
		{
			var reduction = volume * _losses / 3m;
			volume -= reduction;

			if (volume <= 0m)
				volume = BaseVolume;
		}

		volume = AdjustVolumeToInstrument(volume);

		return volume;
	}

	private decimal AdjustVolumeToInstrument(decimal volume)
	{
		var security = Security;

		if (security == null)
			return volume;

		var step = security.VolumeStep ?? 0m;

		if (step > 0m)
			volume = Math.Floor(volume / step) * step;

		var minVolume = security.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			return 0m;

		var maxVolume = security.MaxVolume ?? 0m;
		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private void ResetState()
	{
		_entryPrice = 0m;
		_highestPrice = 0m;
		_lowestPrice = 0m;
	}
}