Auf GitHub ansehen

Currencyprofits Hoch-Tief-Kanal-Strategie

Übersicht

Diese Strategie ist ein StockSharp-Port des MetaTrader-Expertenberaters Currencyprofits_01.1. Sie kombiniert einen schnellen/langsamen gleitenden Durchschnitt als Trendfilter mit einem Ausbruch aus dem jüngsten Kanalextremum. Wenn der schnelle gleitende Durchschnitt über dem langsamen liegt, erwartet die Strategie ein bullisches Umfeld und wartet darauf, dass der Preis das niedrigste Tief des vorherigen Kanalfensters retestet. Short-Trades werden eingegangen, wenn der schnelle Durchschnitt unter dem langsamen liegt und der Preis das höchste Hoch des Kanals retestet.

Die Implementierung funktioniert auf jedem Instrument, das Kerzendaten liefert. Alle Berechnungen werden auf abgeschlossenen Kerzen durchgeführt, um Stabilität sowohl in Backtests als auch im Live-Handel zu gewährleisten.

Handelslogik

  1. Abonniere den konfigurierten Kerzentyp und berechne zwei gleitende Durchschnitte und einen Donchian-Kanal auf Basis der vorherigen ChannelLength Kerzen (Standard: 6 Bars).
  2. Speichere die vorherigen Kerzenwerte der Indikatoren, um die ursprüngliche MQL-Logik mit einem Ein-Bar-Versatz zu imitieren.
  3. Long-Einstieg: wenn der vorherige schnelle MA größer als der vorherige langsame MA ist und das aktuelle Kerzentief den vorherigen Kanal-Tiefstwert berührt oder bricht.
  4. Short-Einstieg: wenn der vorherige schnelle MA kleiner als der vorherige langsame MA ist und das aktuelle Kerzenhoch den vorherigen Kanal-Höchstwert berührt oder bricht.
  5. Ausstiegsregeln:
    • Long-Positionen schließen, wenn die nächste Kerze über dem gespeicherten Kanal-Höchstwert schließt oder der Schutz-Stop erreicht wird.
    • Short-Positionen schließen, wenn die nächste Kerze unter dem gespeicherten Kanal-Tiefstwert schließt oder der Stop-Loss ausgelöst wird.
  6. Es ist immer nur eine Position aktiv; die Strategie ignoriert neue Signale, während ein Trade offen ist.

Positionsgrößenbestimmung

  • RiskPercent definiert den Anteil des Portfoliowertes, der pro Trade riskiert werden kann (Standard 0.14, d. h. 14%).
  • Der Stop-Loss-Abstand ergibt sich aus StopLossPoints multipliziert mit dem PriceStep des Wertpapiers (oder Punkten, wenn keine Metadaten verfügbar sind).
  • Das Bargeldrisiko pro Kontrakt wird mit dem Börsenschrittwert (StepPrice) geschätzt. Wenn das Wertpapier diese Information nicht bereitstellt, wird die rohe Preisdistanz verwendet.
  • Das endgültige Ordervolumen wird an die Handelsbeschränkungen des Instruments angepasst (VolumeStep, MinVolume, MaxVolume). Wenn die risikobasierte Größenberechnung nicht möglich ist, wird das Basis-Volume der Strategie verwendet.

Parameter

  • FastLength – Länge des schnellen gleitenden Durchschnitts zur Trenderkennung (Standard 32).
  • FastMaType – Typ des schnellen gleitenden Durchschnitts (Simple, Exponential, Smoothed, Weighted).
  • SlowLength – Länge des langsamen gleitenden Durchschnitts (Standard 86).
  • SlowMaType – Typ des langsamen gleitenden Durchschnitts.
  • PriceSource – Kerzenpreis für beide gleitende Durchschnitte (Standard Close).
  • ChannelLength – Anzahl der vorherigen Kerzen, die den Hoch-/Tiefkanal bilden (Standard 6).
  • StopLossPoints – Stop-Abstand in Instrumentenpunkten vor der Umrechnung in einen Preis (Standard 170).
  • RiskPercent – Anteil des pro Trade riskierten Kapitals (Standard 0.14 → 14%).
  • CandleType – Zeitrahmen der für alle Berechnungen verwendeten Kerzen (Standard 1 Stunde, kann geändert werden).

Verwendungshinweise

  • Sicherstellen, dass Security.PriceStep, Security.StepPrice und Volume-Metadaten für eine genaue Positionsgrößenbestimmung gefüllt sind.
  • Das Volume der Strategie auf einen sinnvollen Fallback-Wert setzen, wenn die risikobasierte Größenberechnung deaktiviert ist (z. B. RiskPercent = 0).
  • Die Logik handelt auf abgeschlossenen Kerzen; Live-Ausführungen erfolgen beim Kerzenschluss, der das Signal bestätigt.
  • Der Stop-Loss wird intern verwaltet; es gibt keinen separaten Take-Profit, was den Quell-Expertenberater widerspiegelt.

Quelle

Konvertiert aus MQL/17641/Currencyprofits_01.1.mq5 mit Fokus auf Lesbarkeit und Kompatibilität mit der StockSharp High-Level-API.

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>
/// Currencyprofits strategy that trades trend pullbacks into the recent channel extremes.
/// </summary>
public class CurrencyprofitsHighLowChannelStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _channelLength;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<MovingAverageTypes> _fastMaType;
	private readonly StrategyParam<MovingAverageTypes> _slowMaType;
	private readonly StrategyParam<int> _signalCooldownBars;

	private decimal? _previousFast;
	private decimal? _previousSlow;
	private decimal? _previousHighest;
	private decimal? _previousLowest;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private int _processedCandles;
	private int _cooldownRemaining;

	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	public int ChannelLength
	{
		get => _channelLength.Value;
		set => _channelLength.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

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

	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	public MovingAverageTypes FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	public MovingAverageTypes SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	public int SignalCooldownBars
	{
		get => _signalCooldownBars.Value;
		set => _signalCooldownBars.Value = value;
	}

	private int RequiredBars => Math.Max(Math.Max(FastLength, SlowLength), ChannelLength) + 1;

	public CurrencyprofitsHighLowChannelStrategy()
	{
		_fastLength = Param(nameof(FastLength), 32)
			.SetDisplay("Fast MA Length", "Length of the fast moving average", "Indicators")
			
			.SetOptimize(10, 120, 2);

		_slowLength = Param(nameof(SlowLength), 86)
			.SetDisplay("Slow MA Length", "Length of the slow moving average", "Indicators")
			
			.SetOptimize(20, 200, 2);

		_channelLength = Param(nameof(ChannelLength), 12)
			.SetDisplay("Channel Lookback", "Number of previous candles for high/low channel", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_stopLossPoints = Param(nameof(StopLossPoints), 170m)
			.SetDisplay("Stop Loss (points)", "Distance to stop loss expressed in price steps", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 0.14m)
			.SetDisplay("Risk Fraction", "Fraction of portfolio capital risked per trade", "Risk");

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

		_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
			.SetDisplay("MA Price Source", "Price source used by both moving averages", "Indicators");

		_fastMaType = Param(nameof(FastMaType), MovingAverageTypes.Simple)
			.SetDisplay("Fast MA Type", "Moving average algorithm for the fast line", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageTypes.Simple)
			.SetDisplay("Slow MA Type", "Moving average algorithm for the slow line", "Indicators");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
			.SetNotNegative()
			.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before the next entry", "Risk");
	}

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

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

		_previousFast = null;
		_previousSlow = null;
		_previousHighest = null;
		_previousLowest = null;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_processedCandles = 0;
		_cooldownRemaining = 0;
	}

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

		var fastMa = CreateMovingAverage(FastMaType, FastLength, PriceSource);
		var slowMa = CreateMovingAverage(SlowMaType, SlowLength, PriceSource);
		var highest = new Highest { Length = ChannelLength };
		var lowest = new Lowest { Length = ChannelLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(fastMa, slowMa, highest, lowest, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow, decimal channelHigh, decimal channelLow)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		_processedCandles++;

		if (_processedCandles <= RequiredBars)
		{
			// Collect enough history before taking any decisions.
			_previousFast = fast;
			_previousSlow = slow;
			_previousHighest = channelHigh;
			_previousLowest = channelLow;
			return;
		}

		if (_previousFast is null || _previousSlow is null || _previousHighest is null || _previousLowest is null)
		{
			_previousFast = fast;
			_previousSlow = slow;
			_previousHighest = channelHigh;
			_previousLowest = channelLow;
			return;
		}

		if (Position > 0)
		{
			// Exit long trades when price breaks the opposite channel or the protective stop.
			var exitByChannel = candle.ClosePrice >= _previousHighest.Value;
			var exitByStop = _stopPrice > 0m && candle.LowPrice <= _stopPrice;

			if (exitByChannel || exitByStop)
			{
				SellMarket(Position);
				ResetTradeState();
				_cooldownRemaining = SignalCooldownBars;
			}
		}
		else if (Position < 0)
		{
			// Exit short trades when price hits the lower boundary or the stop.
			var exitByChannel = candle.ClosePrice <= _previousLowest.Value;
			var exitByStop = _stopPrice > 0m && candle.HighPrice >= _stopPrice;

			if (exitByChannel || exitByStop)
			{
				BuyMarket(-Position);
				ResetTradeState();
				_cooldownRemaining = SignalCooldownBars;
			}
		}
		else if (_cooldownRemaining == 0)
		{
			var stopDistance = GetStopDistance();

			if (stopDistance > 0m)
			{
				var bullishTrend = _previousFast.Value > _previousSlow.Value && fast > slow;
				var bearishTrend = _previousFast.Value < _previousSlow.Value && fast < slow;
				var bullishReversal = candle.LowPrice <= _previousLowest.Value && candle.ClosePrice > candle.OpenPrice && candle.ClosePrice > fast;
				var bearishReversal = candle.HighPrice >= _previousHighest.Value && candle.ClosePrice < candle.OpenPrice && candle.ClosePrice < fast;

				// Long entries require a bullish trend and a pullback to the recent low channel.
				if (bullishTrend && bullishReversal)
				{
					var volume = CalculatePositionSize(stopDistance);

					if (volume > 0m)
					{
						BuyMarket(volume);
						_entryPrice = candle.ClosePrice;
						_stopPrice = _entryPrice - stopDistance;
						_cooldownRemaining = SignalCooldownBars;
					}
				}
				// Short entries require a bearish trend and a retest of the recent high channel.
				else if (bearishTrend && bearishReversal)
				{
					var volume = CalculatePositionSize(stopDistance);

					if (volume > 0m)
					{
						SellMarket(volume);
						_entryPrice = candle.ClosePrice;
						_stopPrice = _entryPrice + stopDistance;
						_cooldownRemaining = SignalCooldownBars;
					}
				}
			}
		}

		_previousFast = fast;
		_previousSlow = slow;
		_previousHighest = channelHigh;
		_previousLowest = channelLow;
	}

	private decimal GetStopDistance()
	{
		if (StopLossPoints <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep > 0m)
			return StopLossPoints * priceStep;

		return StopLossPoints;
	}

	private decimal CalculatePositionSize(decimal stopDistance)
	{
		var defaultVolume = AdjustVolume(Volume);

		if (stopDistance <= 0m)
			return defaultVolume;

		var portfolioValue = Portfolio?.CurrentValue;

		if (portfolioValue is null || portfolioValue <= 0m || RiskPercent <= 0m)
			return defaultVolume;

		var riskCapital = portfolioValue.Value * RiskPercent;
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;

		decimal riskPerContract;

		if (priceStep > 0m && stepPrice > 0m)
		{
			// Convert the stop distance into cash risk per contract using exchange specifications.
			riskPerContract = stopDistance / priceStep * stepPrice;
		}
		else
		{
			// Fallback when the security does not expose step metadata.
			riskPerContract = stopDistance;
		}

		if (riskPerContract <= 0m)
			return defaultVolume;

		var desiredVolume = riskCapital / riskPerContract;
		return AdjustVolume(desiredVolume);
	}

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

		var step = Security?.VolumeStep ?? 0m;

		if (step > 0m)
		{
			var steps = decimal.Floor(volume / step);
			volume = steps * step;
		}

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

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

		return volume;
	}

	private void ResetTradeState()
	{
		// Clear cached execution details after a position has been closed.
		_entryPrice = 0m;
		_stopPrice = 0m;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length, CandlePrices price)
	{
		return type switch
		{
			MovingAverageTypes.Simple => new SMA { Length = length },
			MovingAverageTypes.Exponential => new EMA { Length = length },
			MovingAverageTypes.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageTypes.Weighted => new WeightedMovingAverage { Length = length },
			_ => new SMA { Length = length },
		};
	}

	public enum MovingAverageTypes
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted,
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}
}