Ver en GitHub

Estrategia TDS Global

La estrategia replica el experto original de MetaTrader "TDSGlobal" basado en el concepto Triple Screen de Alexander Elder. Evalúa velas diarias y combina la pendiente del MACD (12, 23, 9) con un filtro Williams %R de 24 períodos. El sistema busca comprar cuando la tendencia está girando hacia arriba mientras que %R muestra condiciones de sobreventa, y vender cuando la tendencia gira hacia abajo y %R señala sobrecompra.

Siempre que se detecta una configuración válida, la estrategia coloca órdenes stop más allá del máximo o mínimo de la sesión anterior. Las entradas se alejan del mercado actual por un búfer configurable para evitar entrar demasiado cerca del precio, imitando la lógica de offset original de "16 puntos". Una vez en posición, la estrategia gestiona un stop de protección, toma de ganancias opcional y un trailing stop en pasos de precio.

Lógica de Trading

  • Datos: Trabaja con velas diarias por defecto (configurable).
  • Filtro de tendencia: Compara los dos valores más recientes de la línea principal del MACD. MACD ascendente implica sesgo largo, MACD descendente implica sesgo corto.
  • Filtro oscilador: Usa el valor anterior de Williams %R. Por debajo de WilliamsBuyLevel (predeterminado -75) permite configuraciones largas, por encima de WilliamsSellLevel (predeterminado -25) permite configuraciones cortas.
  • Entrada:
    • Largo: colocar un buy-stop por encima del máximo anterior más un paso de precio. La entrada se eleva a al menos EntryBufferSteps pasos de precio por encima del último cierre para mantener una distancia mínima del mercado.
    • Corto: colocar un sell-stop por debajo del mínimo anterior menos un paso de precio. La orden se baja a como máximo el último cierre menos los pasos de EntryBufferSteps.
  • Gestión de riesgos:
    • El stop inicial está anclado al extremo opuesto de la vela anterior (máximo para cortos, mínimo para largos).
    • La distancia de toma de ganancias equivale a TakeProfitSteps pasos de precio. El valor predeterminado (999) mantiene el comportamiento cercano a la versión MQL que usaba un objetivo muy amplio.
    • El trailing stop se habilita cuando TrailingStopSteps > 0. Sigue el cierre por esa cantidad de pasos y solo se ajusta en la dirección de la operación.
  • Manejo de órdenes:
    • Las órdenes stop existentes se cancelan y se actualizan cuando el precio de entrada o los niveles de protección necesitan ser actualizados.
    • Las señales de tendencia opuesta eliminan las órdenes pendientes que ya no se alinean con la dirección del MACD.
    • Cuando se abre una posición, los niveles pendientes almacenados se reutilizan para inicializar los precios de stop/toma de ganancias en vivo.
  • Escalonamiento opcional: El EA original escalonaba la colocación de órdenes en pares de divisas para evitar órdenes pendientes simultáneas. Establecer UseSymbolStagger en true impone las mismas ventanas de minutos para EURUSD, GBPUSD, USDCHF y USDJPY.

Parámetros

  • MacdFastLength, MacdSlowLength, MacdSignalLength – Períodos MACD usados para la verificación de la pendiente de tendencia.
  • WilliamsLength – Períodos de lookback para Williams %R.
  • WilliamsBuyLevel, WilliamsSellLevel – Umbrales de sobreventa/sobrecompra (valores negativos, más cercanos a -100/-0 respectivamente).
  • EntryBufferSteps – Offset mínimo desde el mercado actual al colocar entradas stop (número de pasos de precio).
  • TakeProfitSteps – Distancia objetivo en pasos de precio (establecer un número pequeño para activar un objetivo rígido).
  • TrailingStopSteps – Distancia del trailing stop en pasos; establecer en cero para deshabilitar el trailing.
  • UseSymbolStagger – Habilita las ventanas de minutos específicas por símbolo.
  • CandleType – Marco temporal para las velas (diario por defecto).

Notas

  • Usar el volumen de estrategia para controlar el tamaño del lote; por defecto es 1 si no se especifica volumen.
  • Las órdenes pendientes y las salidas trailing operan en velas completadas, por lo que los llenados entre cierres de velas se aproximan con el precio de entrada almacenado.
  • El valor predeterminado del take profit es grande para coincidir con el comportamiento del EA original; ajustarlo cuando se necesita un objetivo finito.
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;
		}
	}
}