Auf GitHub ansehen

TDS Global Strategie

Die Strategie repliziert den originalen MetaTrader "TDSGlobal"-Experten basierend auf Alexander Elders Triple Screen-Konzept. Sie wertet Tageskerzen aus und kombiniert die Steigung des MACD (12, 23, 9) mit einem 24-Perioden Williams %R-Filter. Das System versucht zu kaufen, wenn der Trend nach oben dreht und %R überverkaufte Bedingungen zeigt, und zu verkaufen, wenn der Trend nach unten dreht und %R Überkauf signalisiert.

Immer wenn ein gültiges Setup erkannt wird, platziert die Strategie Stop-Orders jenseits des Hochs oder Tiefs der vorherigen Session. Einstiege werden durch einen konfigurierbaren Puffer vom aktuellen Markt weggeschoben, um ein Einsteigen zu nah am Preis zu vermeiden, was die ursprüngliche "16-Punkte"-Offset-Logik widerspiegelt. Einmal in einer Position verwaltet die Strategie einen Schutzstop, optionalen Take-Profit und einen Trailing Stop in Preisschritten.

Handelslogik

  • Daten: Arbeitet standardmäßig mit Tageskerzen (konfigurierbar).
  • Trendfilter: Vergleicht die zwei aktuellsten MACD-Hauptlinienwerte. Steigender MACD impliziert Long-Bias, fallender MACD impliziert Short-Bias.
  • Oszillatorfilter: Verwendet den vorherigen Williams %R-Wert. Unter WilliamsBuyLevel (Standard -75) erlaubt Long-Setups, über WilliamsSellLevel (Standard -25) erlaubt Short-Setups.
  • Einstieg:
    • Long: Buy-Stop über dem vorherigen Hoch plus einen Preisschritt platzieren. Der Einstieg wird auf mindestens EntryBufferSteps Preisschritte über dem letzten Schlusskurs angehoben, um eine Mindestdistanz vom Markt zu wahren.
    • Short: Sell-Stop unter dem vorherigen Tief minus einen Preisschritt platzieren. Die Order wird auf höchstens den letzten Schlusskurs minus EntryBufferSteps Schritte gesenkt.
  • Risikomanagement:
    • Der anfängliche Stop ist am entgegengesetzten Extrem der vorherigen Kerze verankert (Hoch für Shorts, Tief für Longs).
    • Die Take-Profit-Distanz entspricht TakeProfitSteps Preisschritten. Der Standardwert (999) hält das Verhalten nahe an der MQL-Version, die ein sehr weites Ziel verwendete.
    • Der Trailing Stop wird aktiviert, wenn TrailingStopSteps > 0 ist. Er folgt dem Schlusskurs um diese Anzahl von Schritten und zieht sich nur in Richtung des Trades an.
  • Order-Handling:
    • Bestehende Stop-Orders werden storniert und erneuert, wenn der Einstiegspreis oder die Schutzniveaus aktualisiert werden müssen.
    • Entgegengesetzte Trendsignale entfernen ausstehende Orders, die nicht mehr mit der MACD-Richtung übereinstimmen.
    • Wenn eine Position eröffnet wird, werden die gespeicherten ausstehenden Niveaus wiederverwendet, um die Live-Stop/Take-Preise zu initialisieren.
  • Optionale Staffelung: Der ursprüngliche EA staffelte die Order-Platzierung über FX-Paare hinweg, um simultane ausstehende Orders zu vermeiden. Das Setzen von UseSymbolStagger auf true erzwingt dieselben Minute-Fenster für EURUSD, GBPUSD, USDCHF und USDJPY.

Parameter

  • MacdFastLength, MacdSlowLength, MacdSignalLength – MACD-Perioden für die Trendsteigungsprüfung.
  • WilliamsLength – Lookback für Williams %R.
  • WilliamsBuyLevel, WilliamsSellLevel – Überverkauft/Überkauft-Schwellenwerte (negative Werte, näher an -100/-0 entsprechend).
  • EntryBufferSteps – Mindestabstand vom aktuellen Markt beim Platzieren von Stop-Einstiegen (Anzahl der Preisschritte).
  • TakeProfitSteps – Zieldistanz in Preisschritten (kleinen Wert setzen, um ein hartes Ziel zu aktivieren).
  • TrailingStopSteps – Trailing-Stop-Distanz in Schritten; auf null setzen zum Deaktivieren des Trailings.
  • UseSymbolStagger – Aktiviert die symbolspezifischen Minute-Fenster.
  • CandleType – Zeitrahmen für Kerzen (standardmäßig täglich).

Hinweise

  • Das Strategie-Volumen verwenden, um die Lot-Größe zu steuern; standardmäßig 1, wenn kein Volumen angegeben.
  • Ausstehende Orders und Trailing-Ausstiege operieren auf abgeschlossenen Kerzen, daher werden Füllungen zwischen Kerzenabschlüssen durch den gespeicherten Einstiegspreis angenähert.
  • Der Standard-Take-Profit-Wert ist groß, um dem ursprünglichen EA-Verhalten zu entsprechen; anpassen, wenn ein endliches Ziel benötigt 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>
/// Triple screen style daily breakout strategy using MACD slope and Williams %R.
/// Places stop orders beyond the prior day's extremes and manages trailing exits.
/// </summary>
public class TdsGlobalStrategy : Strategy
{
	private readonly StrategyParam<int> _macdFastLength;
	private readonly StrategyParam<int> _macdSlowLength;
	private readonly StrategyParam<int> _macdSignalLength;
	private readonly StrategyParam<int> _williamsLength;
	private readonly StrategyParam<decimal> _williamsBuyLevel;
	private readonly StrategyParam<decimal> _williamsSellLevel;
	private readonly StrategyParam<int> _entryBufferSteps;
	private readonly StrategyParam<decimal> _takeProfitSteps;
	private readonly StrategyParam<decimal> _trailingStopSteps;
	private readonly StrategyParam<bool> _useSymbolStagger;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _priceStep;

	private decimal? _macdPrev1;
	private decimal? _macdPrev2;
	private decimal? _williamsPrev1;
	private decimal? _prevHigh;
	private decimal? _prevLow;
	private decimal? _prevClose;

	private bool _hasPendingOrder;
	private Sides? _pendingSide;
	private decimal? _pendingEntryPrice;
	private decimal? _pendingStopPrice;
	private decimal? _pendingTakePrice;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private DateTimeOffset? _lastEntryTime;

	/// <summary>
	/// Fast EMA period used by MACD.
	/// </summary>
	public int MacdFastLength
	{
		get => _macdFastLength.Value;
		set => _macdFastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA period used by MACD.
	/// </summary>
	public int MacdSlowLength
	{
		get => _macdSlowLength.Value;
		set => _macdSlowLength.Value = value;
	}

	/// <summary>
	/// Signal period for MACD.
	/// </summary>
	public int MacdSignalLength
	{
		get => _macdSignalLength.Value;
		set => _macdSignalLength.Value = value;
	}

	/// <summary>
	/// Williams %R lookback length.
	/// </summary>
	public int WilliamsLength
	{
		get => _williamsLength.Value;
		set => _williamsLength.Value = value;
	}

	/// <summary>
	/// Oversold threshold for Williams %R.
	/// </summary>
	public decimal WilliamsBuyLevel
	{
		get => _williamsBuyLevel.Value;
		set => _williamsBuyLevel.Value = value;
	}

	/// <summary>
	/// Overbought threshold for Williams %R.
	/// </summary>
	public decimal WilliamsSellLevel
	{
		get => _williamsSellLevel.Value;
		set => _williamsSellLevel.Value = value;
	}

	/// <summary>
	/// Minimum distance from market price when placing stop entries (in steps).
	/// </summary>
	public int EntryBufferSteps
	{
		get => _entryBufferSteps.Value;
		set => _entryBufferSteps.Value = value;
	}

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

	/// <summary>
	/// Trailing stop distance expressed in price steps.
	/// </summary>
	public decimal TrailingStopSteps
	{
		get => _trailingStopSteps.Value;
		set => _trailingStopSteps.Value = value;
	}

	/// <summary>
	/// Enable symbol specific minute windows for order placement.
	/// </summary>
	public bool UseSymbolStagger
	{
		get => _useSymbolStagger.Value;
		set => _useSymbolStagger.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations (defaults to daily candles).
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="TdsGlobalStrategy"/> class.
	/// </summary>
	public TdsGlobalStrategy()
	{
		_macdFastLength = Param(nameof(MacdFastLength), 12)
			.SetGreaterThanZero()
			.SetDisplay("MACD Fast", "Fast EMA length", "MACD");

		_macdSlowLength = Param(nameof(MacdSlowLength), 23)
			.SetGreaterThanZero()
			.SetDisplay("MACD Slow", "Slow EMA length", "MACD");

		_macdSignalLength = Param(nameof(MacdSignalLength), 9)
			.SetGreaterThanZero()
			.SetDisplay("MACD Signal", "Signal line length", "MACD");

		_williamsLength = Param(nameof(WilliamsLength), 24)
			.SetGreaterThanZero()
			.SetDisplay("Williams Length", "%R lookback", "Filters");

		_williamsBuyLevel = Param(nameof(WilliamsBuyLevel), 30m)
			.SetDisplay("Williams Buy", "Oversold threshold", "Filters");

		_williamsSellLevel = Param(nameof(WilliamsSellLevel), 70m)
			.SetDisplay("Williams Sell", "Overbought threshold", "Filters");

		_entryBufferSteps = Param(nameof(EntryBufferSteps), 16)
			.SetGreaterThanZero()
			.SetDisplay("Entry Buffer", "Minimum distance from market in steps", "Risk");

		_takeProfitSteps = Param(nameof(TakeProfitSteps), 999m)
			.SetDisplay("Take Profit Steps", "Target distance in price steps", "Risk");

		_trailingStopSteps = Param(nameof(TrailingStopSteps), 10m)
			.SetDisplay("Trailing Stop Steps", "Trailing stop distance in steps", "Risk");

		_useSymbolStagger = Param(nameof(UseSymbolStagger), false)
			.SetDisplay("Use Time Windows", "Apply symbol specific minute windows", "General");

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

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

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

		_priceStep = 0m;

		_macdPrev1 = null;
		_macdPrev2 = null;
		_williamsPrev1 = null;
		_prevHigh = null;
		_prevLow = null;
		_prevClose = null;

		ResetPendingOrder();
		ResetPositionState();
		_lastEntryTime = null;
	}

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

		_priceStep = Security?.PriceStep ?? 1m;
		if (_priceStep <= 0)
			_priceStep = 1m;

		var macd = new MACD
		{
			ShortMa = { Length = MacdFastLength },
			LongMa = { Length = MacdSlowLength },
		};

		var williams = new RelativeStrengthIndex { Length = WilliamsLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(macd, williams, ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
		{
			UpdateHistory(macdLine, williams, candle);
			return;
		}

		ManageOpenPosition(candle);

		if (Math.Abs(Position) > 0)
		{
			UpdateHistory(macdLine, williams, candle);
			return;
		}

		if (_macdPrev1 is null || _macdPrev2 is null || _williamsPrev1 is null ||
			_prevHigh is null || _prevLow is null || _prevClose is null)
		{
			UpdateHistory(macdLine, williams, candle);
			return;
		}

		if (UseSymbolStagger && !IsWithinAllowedWindow(candle.CloseTime))
		{
			UpdateHistory(macdLine, williams, candle);
			return;
		}

		var direction = _macdPrev1 > _macdPrev2 ? 1 : _macdPrev1 < _macdPrev2 ? -1 : 0;
		var oversold = _williamsPrev1 <= WilliamsBuyLevel;
		var overbought = _williamsPrev1 >= WilliamsSellLevel;

		var volume = Volume;
		if (volume <= 0)
			volume = 1m;

		var cooldownPassed = _lastEntryTime is null || candle.CloseTime - _lastEntryTime >= TimeSpan.FromHours(24);

		if (direction > 0 && oversold && cooldownPassed)
		{
			PlaceBuyStop(volume, candle.CloseTime);
		}
		else if (direction < 0 && overbought && cooldownPassed)
		{
			PlaceSellStop(volume, candle.CloseTime);
		}
		else if (_hasPendingOrder)
		{
			if ((_pendingSide == Sides.Buy && direction < 0) ||
				(_pendingSide == Sides.Sell && direction > 0))
			{
				ResetPendingOrder();
			}
		}

		UpdateHistory(macdLine, williams, candle);
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		var position = Position;

		if (position > 0)
		{
			if (_entryPrice is null)
			{
				_entryPrice = _pendingEntryPrice ?? candle.ClosePrice;
				_stopPrice = _pendingStopPrice;
				_takePrice = _pendingTakePrice;
				ResetPendingOrder();
			}

			if (TrailingStopSteps > 0)
			{
				var trailing = candle.ClosePrice - TrailingStopSteps * _priceStep;
				if (_stopPrice is null || trailing > _stopPrice)
					_stopPrice = trailing;
			}

			if (_stopPrice is not null && candle.LowPrice <= _stopPrice)
			{
				if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetPositionState();
				ResetPendingOrder();
				return;
			}

			if (_takePrice is not null && candle.HighPrice >= _takePrice)
			{
				if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetPositionState();
				ResetPendingOrder();
			}
		}
		else if (position < 0)
		{
			if (_entryPrice is null)
			{
				_entryPrice = _pendingEntryPrice ?? candle.ClosePrice;
				_stopPrice = _pendingStopPrice;
				_takePrice = _pendingTakePrice;
				ResetPendingOrder();
			}

			if (TrailingStopSteps > 0)
			{
				var trailing = candle.ClosePrice + TrailingStopSteps * _priceStep;
				if (_stopPrice is null || trailing < _stopPrice)
					_stopPrice = trailing;
			}

			if (_stopPrice is not null && candle.HighPrice >= _stopPrice)
			{
				if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetPositionState();
				ResetPendingOrder();
				return;
			}

			if (_takePrice is not null && candle.LowPrice <= _takePrice)
			{
				if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));
				ResetPositionState();
				ResetPendingOrder();
			}
		}
		else
		{
			ResetPositionState();
		}
	}

	private void PlaceBuyStop(decimal volume, DateTimeOffset time)
	{
		if (_prevHigh is null || _prevLow is null || _prevClose is null)
			return;

		var entryPrice = Math.Max(_prevHigh.Value + _priceStep, _prevClose.Value + EntryBufferSteps * _priceStep);
		var stopPrice = _prevLow.Value - _priceStep;
		var takePrice = TakeProfitSteps > 0 ? entryPrice + TakeProfitSteps * _priceStep : (decimal?)null;

		if (_hasPendingOrder && _pendingSide == Sides.Buy &&
			_pendingEntryPrice == entryPrice &&
			_pendingStopPrice == stopPrice &&
			_pendingTakePrice == takePrice)
		{
			return;
		}

		BuyMarket(volume);
		_lastEntryTime = time;

		_pendingSide = Sides.Buy;
		_pendingEntryPrice = entryPrice;
		_pendingStopPrice = stopPrice;
		_pendingTakePrice = takePrice;
		_hasPendingOrder = true;
	}

	private void PlaceSellStop(decimal volume, DateTimeOffset time)
	{
		if (_prevHigh is null || _prevLow is null || _prevClose is null)
			return;

		var entryPrice = Math.Min(_prevLow.Value - _priceStep, _prevClose.Value - EntryBufferSteps * _priceStep);
		var stopPrice = _prevHigh.Value + _priceStep;
		var takePrice = TakeProfitSteps > 0 ? entryPrice - TakeProfitSteps * _priceStep : (decimal?)null;

		if (_hasPendingOrder && _pendingSide == Sides.Sell &&
			_pendingEntryPrice == entryPrice &&
			_pendingStopPrice == stopPrice &&
			_pendingTakePrice == takePrice)
		{
			return;
		}

		SellMarket(volume);
		_lastEntryTime = time;

		_pendingSide = Sides.Sell;
		_pendingEntryPrice = entryPrice;
		_pendingStopPrice = stopPrice;
		_pendingTakePrice = takePrice;
		_hasPendingOrder = true;
	}

	private void ResetPendingOrder()
	{
		_hasPendingOrder = false;
		_pendingSide = null;
		_pendingEntryPrice = null;
		_pendingStopPrice = null;
		_pendingTakePrice = null;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

	private void UpdateHistory(decimal macdLine, decimal williams, ICandleMessage candle)
	{
		if (_macdPrev1 is not null)
			_macdPrev2 = _macdPrev1;

		_macdPrev1 = macdLine;
		_williamsPrev1 = williams;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_prevClose = candle.ClosePrice;
	}

	private bool IsWithinAllowedWindow(DateTimeOffset time)
	{
		if (!UseSymbolStagger)
			return true;

		var minute = time.Minute;
		var code = Security?.Code?.ToUpperInvariant();

		switch (code)
		{
			case "USDCHF":
				return (minute >= 0 && minute <= 1) || (minute >= 8 && minute <= 9) ||
					(minute >= 16 && minute <= 17) || (minute >= 24 && minute <= 25) ||
					(minute >= 32 && minute <= 33) || (minute >= 40 && minute <= 41) ||
					(minute >= 48 && minute <= 49);

			case "GBPUSD":
				return (minute >= 2 && minute <= 3) || (minute >= 10 && minute <= 11) ||
					(minute >= 18 && minute <= 19) || (minute >= 26 && minute <= 27) ||
					(minute >= 34 && minute <= 35) || (minute >= 42 && minute <= 43) ||
					(minute >= 50 && minute <= 51);

			case "USDJPY":
				return (minute >= 4 && minute <= 5) || (minute >= 12 && minute <= 13) ||
					(minute >= 20 && minute <= 21) || (minute >= 28 && minute <= 29) ||
					(minute >= 36 && minute <= 37) || (minute >= 44 && minute <= 45) ||
					(minute >= 52 && minute <= 53);

			case "EURUSD":
				return (minute >= 6 && minute <= 7) || (minute >= 14 && minute <= 15) ||
					(minute >= 22 && minute <= 23) || (minute >= 30 && minute <= 31) ||
					(minute >= 38 && minute <= 39) || (minute >= 46 && minute <= 47) ||
					(minute >= 54 && minute <= 59);

			default:
				return true;
		}
	}
}