Auf GitHub ansehen

Einfache Kloss-Strategie

Die Kloss Simple Strategy ist eine direkte Umsetzung des MetaTrader 4 Expertenberaters Kloss_.mq4. Es rekonstruiert die ursprüngliche Handelsidee unter Verwendung des High-Level-API von StockSharp und hält den Indikatorsatz identisch: einen exponentiellen gleitenden Durchschnitt (EMA), der auf gewichteten Schlusskursen berechnet wird, den Commodity Channel Index (CCI) und den Stochastic-Oszillator. Signale werden aus der zuvor abgeschlossenen Kerze generiert und spiegeln die Ein-Balken-Verschiebungslogik in der MQL-Version wider. Die Positionsgröße kann entweder auf einem festen Auftragsvolumen oder auf einem Risikoprozentsatz des Portfoliowerts basieren, genau wie die ursprünglichen Lot-Berechnungsregeln.

Kernidee

  1. Überwachen Sie den Momentumkontext mit den Schwellenwerten CCI und Stochastic um ihre neutralen Niveaus.
  2. Bestätigen Sie Momentumsignale mit einem kurzfristigen EMA des gewichteten Schlusskurses.
  3. Geben Sie Positionen nur dann ein, wenn die vorherige Kerze alle Signalbedingungen erfüllt, um vorzeitige Geschäfte aufgrund unvollständiger Marktdaten zu verhindern.
  4. Erlauben Sie mehrere Einträge in die gleiche Richtung bis zu einem konfigurierbaren Limit und emulieren Sie den Parameter „MaxOrders“ aus dem MT4-Skript.

Anzeigekonfiguration

  • EMA (MaPeriod): Verwendet den gewichteten Abschluss (Close * 2 + High + Low) / 4, um PRICE_WEIGHTED von MetaTrader abzugleichen. Fungiert als kurzfristiger Trendfilter.
  • CCI (CciPeriod): Bewertet Momentumabweichungen vom Durchschnittspreis. Der Schwellenwert ±CciLevel definiert aggressive gegenüber konservativen Einträgen.
  • Stochastic (StochasticKPeriod / DPeriod / Smooth): Verwendet die Hauptlinie %K, um überkaufte oder überverkaufte Bedingungen relativ zum neutralen 50-Niveau zu erkennen. Die Abweichung von 50 wird durch StochasticLevel gesteuert.

Alle Indikatoren arbeiten mit der durch CandleType definierten primären Kerzenserie. Die Strategie aktualisiert die Indikatorwerte nur für fertige Kerzen und gewährleistet so ein stabiles Backtesting und Live-Verhalten.

Handelslogik

Lange Einrichtung

  1. Der vorherige Kerzenschluss liegt über dem vorherigen EMA-Wert.
  2. Der vorherige CCI-Wert liegt unter -CciLevel, was auf eine überverkaufte Dynamik hinweist.
  3. Der vorherige %K-Wert von Stochastic liegt unter 50 - StochasticLevel, was eine überverkaufte Schwankung bestätigt.
  4. Wenn die Bedingungen erfüllt sind, wird jedes Short-Engagement geschlossen und eine neue Long-Position eröffnet, sofern die Anzahl der bestehenden Long-Orders unter MaxOrders liegt.

Kurze Einrichtung

  1. Der vorherige Kerzenschluss liegt unter dem vorherigen EMA-Wert.
  2. Der vorherige CCI-Wert liegt über +CciLevel, was auf eine überkaufte Dynamik hinweist.
  3. Der vorherige Stochastic %K-Wert liegt über 50 + StochasticLevel, was eine überkaufte Schwankung bestätigt.
  4. Wenn die Bedingungen erfüllt sind, wird jede Long-Position geschlossen und eine neue Short-Position eröffnet, vorbehaltlich des MaxOrders-Limits.

Exit-Management

  • Stop Loss / Take Profit: Optionale absolute Abstände in Instrumentenpunkten. Wenn einer der Werte größer als Null ist, wird der integrierte Positionsschutz von StockSharp aktiviert.
  • Gegensignal: Vor dem Öffnen in die entgegengesetzte Richtung wird die aktuelle Position geschlossen, um den ursprünglichen Expert Advisor nachzuahmen.

Positionsgrößen

  • OrderVolume: Feste Standardgröße, die den Parameter Lots von MT4 repliziert.
  • Risikoprozentsatz: Wenn der Wert größer als Null ist, berechnet die Strategie die Handelsgröße als Prozentsatz des Portfoliowerts. Sofern verfügbar, werden die Margin-Anforderungen von Instrumenten verwendet, andernfalls wird auf die preisbasierte Größenbestimmung zurückgegriffen, wodurch das Lots == 0-Verhalten des MQL-Codes reproduziert wird.
  • MaxOrders: Begrenzt das kumulative Volumen pro Richtung, indem eine Belichtung von bis zu MaxOrders * OrderVolume zugelassen wird.

Parameter

Parameter Beschreibung
OrderVolume Basisauftragsgröße, die verwendet wird, wenn RiskPercentage Null ist.
MaPeriod Länge des EMA basierend auf gewichteten Schlusskursen.
CciPeriod Anzahl der Balken, die in der CCI-Berechnung verwendet werden.
CciLevel Absoluter CCI-Schwellenwert für die Signalgenerierung.
StochasticKPeriod Lookback für die Zeile Stochastic %K.
StochasticDPeriod Gleitender Durchschnittszeitraum für die %D-Linie.
StochasticSmooth Zusätzliche Glättung auf %K angewendet.
StochasticLevel Abweichung von 50 wird zur Erkennung von Überkauft/Überverkauft verwendet.
MaxOrders Maximal zulässige Anzahl an Einträgen pro Richtung.
StopLossPoints Optionale Stop-Loss-Distanz in Preispunkten.
TakeProfitPoints Optionale Take-Profit-Distanz in Preispunkten.
RiskPercentage Portfolio-Prozentsatz für dynamische Positionsgrößenbestimmung.
CandleType Für alle Berechnungen verwendete Kerzenreihe.

Praktische Hinweise

  • Funktioniert am besten bei Intraday-Daten, bei denen kurzfristige Oszillatoren schnell auf Preisschwankungen reagieren.
  • Der gewichtete Schlusskurs sorgt dafür, dass der EMA reaktionsfähig bleibt und gleichzeitig den Hoch-/Tief-Bereich der Kerze berücksichtigt.
  • Da jede Entscheidung auf der vorherigen Kerze basiert, vermeidet die Strategie ein Neuzeichnen innerhalb eines Balkens und bleibt bei historischen Tests deterministisch.
  • Das Risikomanagement sollte an den Vertragsspezifikationen des Brokers ausgerichtet sein, sodass OrderVolume und MaxOrders den ausführbaren Handelsgrößen entsprechen.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy based on EMA, CCI, and Stochastic oscillator signals.
/// Converts the original Kloss expert advisor from MetaTrader 4 to StockSharp.
/// </summary>
public class KlossSimpleStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<decimal> _cciLevel;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSmooth;
	private readonly StrategyParam<decimal> _stochasticLevel;
	private readonly StrategyParam<int> _maxOrders;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _riskPercentage;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema;
	private CommodityChannelIndex _cci;
	private StochasticOscillator _stochastic;

	private decimal? _previousCci;

	/// <summary>
	/// Initializes a new instance of the <see cref="KlossSimpleStrategy"/> class.
	/// </summary>
	public KlossSimpleStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Base order volume", "Trading");

		_maPeriod = Param(nameof(MaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "Length of the exponential moving average", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_cciPeriod = Param(nameof(CciPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("CCI Period", "Length of the commodity channel index", "Indicators")
			
			.SetOptimize(5, 30, 5);

		_cciLevel = Param(nameof(CciLevel), 200m)
			.SetGreaterThanZero()
			.SetDisplay("CCI Level", "Distance from zero to trigger signals", "Indicators")

			.SetOptimize(50m, 200m, 10m);

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "Period of the %K line", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %D", "Period of the %D line", "Indicators")

			.SetOptimize(1, 10, 1);

		_stochasticSmooth = Param(nameof(StochasticSmooth), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Smooth", "Smoothing factor for %K", "Indicators")
			
			.SetOptimize(1, 10, 1);

		_stochasticLevel = Param(nameof(StochasticLevel), 30m)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Level", "Distance from 50 to trigger signals", "Indicators")

			.SetOptimize(10m, 40m, 5m);

		_maxOrders = Param(nameof(MaxOrders), 1)
			.SetNotNegative()
			.SetDisplay("Max Orders", "Maximum number of positions per direction", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pts)", "Stop loss distance in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pts)", "Take profit distance in points", "Risk");

		_riskPercentage = Param(nameof(RiskPercentage), 10m)
			.SetNotNegative()
			.SetDisplay("Risk %", "Portfolio percentage for dynamic position sizing", "Risk");

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

	/// <summary>Base order volume for new entries.</summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>EMA length used to filter price action.</summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>CCI period for momentum detection.</summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>Absolute level that CCI must exceed to signal an entry.</summary>
	public decimal CciLevel
	{
		get => _cciLevel.Value;
		set => _cciLevel.Value = value;
	}

	/// <summary>Stochastic %K period.</summary>
	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	/// <summary>Stochastic %D period.</summary>
	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	/// <summary>Smoothing applied to the %K line.</summary>
	public int StochasticSmooth
	{
		get => _stochasticSmooth.Value;
		set => _stochasticSmooth.Value = value;
	}

	/// <summary>Offset around 50 used for stochastic thresholds.</summary>
	public decimal StochasticLevel
	{
		get => _stochasticLevel.Value;
		set => _stochasticLevel.Value = value;
	}

	/// <summary>Maximum number of simultaneous entries per direction.</summary>
	public int MaxOrders
	{
		get => _maxOrders.Value;
		set => _maxOrders.Value = value;
	}

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

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

	/// <summary>Portfolio percentage used to size positions dynamically.</summary>
	public decimal RiskPercentage
	{
		get => _riskPercentage.Value;
		set => _riskPercentage.Value = value;
	}

	/// <summary>Candle type used for indicator calculations.</summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_previousCci = null;
	}

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

		_ema = new EMA { Length = MaPeriod };
		_cci = new CommodityChannelIndex { Length = CciPeriod };
		_stochastic = new StochasticOscillator();
		_stochastic.K.Length = StochasticKPeriod;
		_stochastic.D.Length = StochasticDPeriod;

		Volume = OrderVolume;

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

		Unit stopLossUnit = null;
		Unit takeProfitUnit = null;
		var priceStep = Security?.PriceStep ?? 0m;

		if (StopLossPoints > 0m && priceStep > 0m)
		{
			stopLossUnit = new Unit(StopLossPoints * priceStep, UnitTypes.Absolute);
		}

		if (TakeProfitPoints > 0m && priceStep > 0m)
		{
			takeProfitUnit = new Unit(TakeProfitPoints * priceStep, UnitTypes.Absolute);
		}

		if (stopLossUnit != null || takeProfitUnit != null)
		{
			StartProtection(stopLoss: stopLossUnit, takeProfit: takeProfitUnit);
		}

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

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var maResult = _ema.Process(candle).ToNullableDecimal();
		var cciResult = _cci.Process(candle).ToNullableDecimal();
		var stochasticValue = (StochasticOscillatorValue)_stochastic.Process(candle);

		if (maResult == null || cciResult == null)
			return;

		if (stochasticValue.K is not decimal stochasticK)
			return;

		if (!_ema.IsFormed || !_cci.IsFormed || !_stochastic.IsFormed)
			return;

		var maValue = maResult.Value;
		var cciValue = cciResult.Value;

		var lowerStochastic = 50m - StochasticLevel;
		var upperStochastic = 50m + StochasticLevel;

		// Buy signal: CCI crosses up through -level from oversold territory
		var cciBuyXover = _previousCci != null && _previousCci.Value < -CciLevel && cciValue >= -CciLevel;
		// Sell signal: CCI crosses down through +level from overbought territory
		var cciSellXover = _previousCci != null && _previousCci.Value > CciLevel && cciValue <= CciLevel;

		if (cciBuyXover && stochasticK < lowerStochastic)
		{
			CloseShortPositions();
			TryEnterLong(candle);
		}
		else if (cciSellXover && stochasticK > upperStochastic)
		{
			CloseLongPositions();
			TryEnterShort(candle);
		}

		_previousCci = cciValue;
	}

	private void CloseLongPositions()
	{
		var longVolume = Position > 0m ? Position : 0m;
		if (longVolume <= 0m)
			return;

		// Close existing long volume before reversing into short trades.
		SellMarket(longVolume);
	}

	private void CloseShortPositions()
	{
		var shortVolume = Position < 0m ? Position.Abs() : 0m;
		if (shortVolume <= 0m)
			return;

		// Close existing short volume before opening new long trades.
		BuyMarket(shortVolume);
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
			return;

		var currentLongVolume = Position > 0m ? Position : 0m;

		if (MaxOrders > 0)
		{
			var maxVolume = volume * MaxOrders;
			if (currentLongVolume >= maxVolume)
				return;

			var additionalVolume = volume.Min(maxVolume - currentLongVolume);
			if (additionalVolume <= 0m)
				return;

			// Add new long exposure without exceeding MaxOrders limit.
			BuyMarket(additionalVolume);
		}
		else
		{
			BuyMarket(volume);
		}
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
			return;

		var currentShortVolume = Position < 0m ? Position.Abs() : 0m;

		if (MaxOrders > 0)
		{
			var maxVolume = volume * MaxOrders;
			if (currentShortVolume >= maxVolume)
				return;

			var additionalVolume = volume.Min(maxVolume - currentShortVolume);
			if (additionalVolume <= 0m)
				return;

			// Add new short exposure without exceeding MaxOrders limit.
			SellMarket(additionalVolume);
		}
		else
		{
			SellMarket(volume);
		}
	}

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

		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);

		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)
			return volume;

		var steps = Math.Floor(volume / step);
		var rounded = steps * step;

		if (rounded <= 0m)
			rounded = step;

		return rounded;
	}
}