Ver en GitHub

Estrategia SilverTrend V3 (C#)

Descripción general

La estrategia SilverTrend V3 es un sistema de seguimiento de momentum que se origina en el asesor experto de MetaTrader 5 "SilverTrend v3". El port a StockSharp reproduce la lógica original adaptándola a la API de estrategias de alto nivel. La idea central es detectar momentum alcista o bajista mediante el cálculo del canal SilverTrend, confirmarlo con el oscilador de perfil de mercado J_TPO y gestionar las posiciones resultantes con stops protectores, lógica de trailing y un filtro de sesión del viernes.

Motor de señales

  1. Dirección SilverTrend

    • Utiliza una ventana deslizante de 350 barras con un parámetro de suavizado de 9 barras para calcular soporte dinámico (smin) y resistencia (smax).
    • Cuando el cierre actual cae por debajo de smin, el sistema marca un régimen bajista; un cierre por encima de smax cambia el régimen a alcista.
    • El cálculo itera desde la barra más antigua a la más reciente para replicar la naturaleza recursiva del código MQL original.
  2. Confirmación J_TPO

    • Implementa el oscilador J_TPO original de 14 periodos que mide cómo se agrupan los precios dentro de una distribución de corto plazo.
    • Solo permite entradas largas cuando el oscilador es positivo y entradas cortas cuando es negativo, filtrando los cambios de momentum débiles.
  3. Detección de cambio de señal

    • Una operación se inicia solo cuando la dirección SilverTrend recién calculada difiere del valor anterior, asegurando que la estrategia reaccione a cambios de régimen genuinos en lugar de ruido.

Gestión de operaciones

  • Entradas de mercado – La estrategia opera con el Volume configurado. Si hay una posición contraria abierta, se cierra y revierte en una sola orden de mercado.
  • Stop loss inicial – Opcional. Definido en pasos de precio relativos al precio de entrada (convertidos con el PriceStep del instrumento).
  • Take profit – Opcional. También definido en pasos de precio y evaluado contra los extremos de la vela para simular el comportamiento original de modificación de órdenes.
  • Trailing stop – Se activa una vez que el precio se mueve a favor la distancia de trailing configurada. Para posiciones largas el stop sube gradualmente, para cortas baja, coincidiendo con la lógica de MetaTrader.
  • Salida por señal opuesta – Cuando el régimen anterior apunta en dirección contraria, cualquier posición existente se liquida al cierre de la siguiente vela.
  • Bloqueo de operaciones del viernes – Las nuevas posiciones se omiten después de la hora especificada los viernes para evitar gaps de fin de semana, exactamente como en el EA fuente.

Parámetros

Nombre Valor predeterminado Descripción
TrailingStopPoints 50 Distancia del trailing stop medida en pasos de precio. Poner en cero para deshabilitar el trailing.
TakeProfitPoints 50 Distancia del take profit en pasos de precio. Cero deshabilita el objetivo.
InitialStopLossPoints 0 Stop protector inicial en pasos de precio. Cero deja la posición sin stop inicial.
FridayCutoffHour 16 Hora de bolsa a partir de la cual no se permiten nuevas entradas el viernes. Usar 0 para permitir operar todo el día.
CandleType Velas de 1 hora Serie de datos que alimenta los indicadores. Se puede usar cualquier temporalidad soportada.
Volume 1 lote Tamaño de operación para cada posición (propiedad Volume de StockSharp).

Todas las distancias se multiplican por PriceStep en tiempo de ejecución, lo que adapta automáticamente la estrategia al tamaño del tick del instrumento (incluyendo símbolos forex de 3/5 dígitos).

Requisitos de datos y entorno

  • Requiere al menos 360 velas completadas antes de producir señales en vivo para que los buffers de SilverTrend y J_TPO estén completamente formados.
  • Diseñado para operar con un único instrumento a través de SubscribeCandles. El override GetWorkingSecurities asegura que la estrategia se suscriba solo al instrumento y temporalidad configurados.
  • Usa StartProtection() para activar el servicio estándar de protección de posiciones de StockSharp una vez al inicio.

Notas de uso

  • El algoritmo espera instrumentos con tendencia como los pares forex principales o futuros líquidos; adaptar el marco temporal a la volatilidad del mercado.
  • Debido a que el cálculo de SilverTrend es recursivo, reiniciar la estrategia con velas históricas insuficientes retrasará la formación de señales hasta que se recopilen suficientes datos.
  • La implementación de la API de alto nivel utiliza los extremos de las velas para simular la gestión de órdenes (stop loss, take profit, trailing). En operativa en vivo considerar emparejar la lógica con órdenes stop/límite reales si la infraestructura lo requiere.
  • El port almacena el estado interno (_previousSignal, _entryPrice, stops de trailing) exactamente una vez por vela terminada, coincidiendo con el comportamiento "una operación por barra" del EA original.

Detalles de conversión

  • Reproduce fielmente las rutinas matemáticas de SilverTrend v3.mq5, incluyendo el algoritmo J_TPO de arrays anidados.
  • Aplica buenas prácticas de C#: los parámetros se exponen vía StrategyParam<T>, todos los comentarios están en inglés, y la indentación usa tabulaciones según las directrices del repositorio.
  • No se incluye versión en Python en esta versión según los requisitos de la tarea.
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>
/// SilverTrend v3 momentum strategy ported from MetaTrader 5.
/// </summary>
public class SilverTrendV3Strategy : Strategy
{
	private readonly StrategyParam<int> _countBars;
	private readonly StrategyParam<int> _ssp;
	private readonly StrategyParam<int> _jtpoLength;
	private readonly StrategyParam<int> _historyCapacity;
	private readonly StrategyParam<int> _risk;

	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _initialStopLossPoints;
	private readonly StrategyParam<int> _fridayCutoffHour;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();
	private readonly List<decimal> _highHistory = new();
	private readonly List<decimal> _lowHistory = new();

	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal _entryPrice;
	private int _previousSignal;
	private decimal _pointValue;

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

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

	/// <summary>
	/// Initial stop loss distance expressed in price steps.
	/// </summary>
	public decimal InitialStopLossPoints
	{
		get => _initialStopLossPoints.Value;
		set => _initialStopLossPoints.Value = value;
	}

	/// <summary>
	/// Hour after which no new trades are allowed on Friday (exchange time).
	/// </summary>
	public int FridayCutoffHour
	{
		get => _fridayCutoffHour.Value;
		set => _fridayCutoffHour.Value = value;
	}

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

	/// <summary>
	/// Number of bars used in the indicator history.
	/// </summary>
	public int CountBars
	{
		get => _countBars.Value;
		set => _countBars.Value = value;
	}

	/// <summary>
	/// Sliding window length for the signal filter.
	/// </summary>
	public int Ssp
	{
		get => _ssp.Value;
		set => _ssp.Value = value;
	}

	/// <summary>
	/// Length used when smoothing JTPO indicator.
	/// </summary>
	public int JtpoLength
	{
		get => _jtpoLength.Value;
		set => _jtpoLength.Value = value;
	}

	/// <summary>
	/// Maximum number of candles stored in history.
	/// </summary>
	public int HistoryCapacity
	{
		get => _historyCapacity.Value;
		set => _historyCapacity.Value = value;
	}

	/// <summary>
	/// Risk coefficient used in signal calculations.
	/// </summary>
	public int Risk
	{
		get => _risk.Value;
		set => _risk.Value = value;
	}

	/// <summary>
	/// Initialize default parameters.
	/// </summary>
	public SilverTrendV3Strategy()
	{
		_countBars = Param(nameof(CountBars), 150)
			.SetGreaterThanZero()
			.SetDisplay("Count Bars", "Number of candles required before trading", "Indicator");

		_ssp = Param(nameof(Ssp), 9)
			.SetGreaterThanZero()
			.SetDisplay("SSP", "Sliding window length", "Indicator");

		_jtpoLength = Param(nameof(JtpoLength), 14)
			.SetGreaterThanZero()
			.SetDisplay("JTPO Length", "JTPO smoothing length", "Indicator");

		_historyCapacity = Param(nameof(HistoryCapacity), 220)
			.SetGreaterThanZero()
			.SetDisplay("History Capacity", "Maximum stored candles", "Indicator");

		_risk = Param(nameof(Risk), 3)
			.SetGreaterThanZero()
			.SetDisplay("Risk", "Risk coefficient", "Trading");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 50m)
			.SetDisplay("Trailing Stop", "Trailing distance in price steps", "Risk")
			.SetNotNegative();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
			.SetDisplay("Take Profit", "Take profit distance in price steps", "Risk")
			.SetNotNegative();

		_initialStopLossPoints = Param(nameof(InitialStopLossPoints), 0m)
			.SetDisplay("Initial Stop Loss", "Initial stop loss in price steps", "Risk")
			.SetNotNegative();

		_fridayCutoffHour = Param(nameof(FridayCutoffHour), 16)
			.SetDisplay("Friday Cutoff Hour", "Disable new entries after this hour on Friday", "Sessions")
			.SetNotNegative();

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

		Volume = 1m;
	}

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

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

		_closeHistory.Clear();
		_highHistory.Clear();
		_lowHistory.Clear();
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_entryPrice = 0m;
		_previousSignal = 0;
		_pointValue = 0m;
	}

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

		_pointValue = Security?.PriceStep ?? 1m;
		if (_pointValue <= 0m)
		{
			_pointValue = 1m;
		}

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

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

		// protection handled manually via trailing/TP/SL
	}

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

		UpdateHistory(candle);

		// indicators are processed manually

		if (_closeHistory.Count < CountBars + Ssp + 1)
		{
			return;
		}

		var jtpo = CalculateJtpo(JtpoLength);
		var signal = CalculateSilverTrendSignal();

		var longSignal = _previousSignal != signal && signal > 0 && jtpo > 0m;
		var shortSignal = _previousSignal != signal && signal < 0 && jtpo < 0m;

		var exitLong = _previousSignal < 0;
		var exitShort = _previousSignal > 0;

		ManageOpenPosition(candle, exitLong, exitShort);

		if (Position <= 0 && longSignal && !IsFridayBlocked(candle))
		{
			EnterLong(candle);
		}
		else if (Position >= 0 && shortSignal && !IsFridayBlocked(candle))
		{
			EnterShort(candle);
		}

		_previousSignal = signal;
	}

	private void UpdateHistory(ICandleMessage candle)
	{
		_closeHistory.Add(candle.ClosePrice);
		_highHistory.Add(candle.HighPrice);
		_lowHistory.Add(candle.LowPrice);

		if (_closeHistory.Count > HistoryCapacity)
		{
			_closeHistory.RemoveAt(0);
			_highHistory.RemoveAt(0);
			_lowHistory.RemoveAt(0);
		}
	}

	private void ManageOpenPosition(ICandleMessage candle, bool exitLongSignal, bool exitShortSignal)
	{
		if (Position > 0)
		{
			UpdateLongTrailing(candle);

			var initialStop = InitialStopLossPoints > 0m ? _entryPrice - GetDistance(InitialStopLossPoints) : (decimal?)null;
			var trailingStop = _longTrailingStop;
			var stop = CombineLongStops(initialStop, trailingStop);
			var takeProfit = TakeProfitPoints > 0m ? _entryPrice + GetDistance(TakeProfitPoints) : (decimal?)null;

			if (exitLongSignal ||
				(takeProfit.HasValue && candle.HighPrice >= takeProfit.Value) ||
				(stop.HasValue && candle.LowPrice <= stop.Value))
			{
				SellMarket();
				ResetStops();
			}
		}
		else if (Position < 0)
		{
			UpdateShortTrailing(candle);

			var initialStop = InitialStopLossPoints > 0m ? _entryPrice + GetDistance(InitialStopLossPoints) : (decimal?)null;
			var trailingStop = _shortTrailingStop;
			var stop = CombineShortStops(initialStop, trailingStop);
			var takeProfit = TakeProfitPoints > 0m ? _entryPrice - GetDistance(TakeProfitPoints) : (decimal?)null;

			if (exitShortSignal ||
				(takeProfit.HasValue && candle.LowPrice <= takeProfit.Value) ||
				(stop.HasValue && candle.HighPrice >= stop.Value))
			{
				BuyMarket();
				ResetStops();
			}
		}
		else
		{
			ResetStops();
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var volume = Volume;
		if (Position < 0)
		{
			volume += Math.Abs(Position);
		}

		BuyMarket(volume);

		_entryPrice = candle.ClosePrice;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var volume = Volume;
		if (Position > 0)
		{
			volume += Position;
		}

		SellMarket(volume);

		_entryPrice = candle.ClosePrice;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private void UpdateLongTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m)
		{
			return;
		}

		var distance = GetDistance(TrailingStopPoints);
		var trigger = _entryPrice + distance;

		if (candle.ClosePrice > trigger)
		{
			var newStop = candle.ClosePrice - distance;
			if (!_longTrailingStop.HasValue || newStop > _longTrailingStop.Value)
			{
				_longTrailingStop = newStop;
			}
		}
	}

	private void UpdateShortTrailing(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m)
		{
			return;
		}

		var distance = GetDistance(TrailingStopPoints);
		var trigger = _entryPrice - distance;

		if (candle.ClosePrice < trigger)
		{
			var newStop = candle.ClosePrice + distance;
			if (!_shortTrailingStop.HasValue || newStop < _shortTrailingStop.Value)
			{
				_shortTrailingStop = newStop;
			}
		}
	}

	private decimal? CombineLongStops(decimal? initialStop, decimal? trailingStop)
	{
		if (initialStop == null && trailingStop == null)
		{
			return null;
		}

		if (initialStop == null)
		{
			return trailingStop;
		}

		if (trailingStop == null)
		{
			return initialStop;
		}

		return Math.Max(initialStop.Value, trailingStop.Value);
	}

	private decimal? CombineShortStops(decimal? initialStop, decimal? trailingStop)
	{
		if (initialStop == null && trailingStop == null)
		{
			return null;
		}

		if (initialStop == null)
		{
			return trailingStop;
		}

		if (trailingStop == null)
		{
			return initialStop;
		}

		return Math.Min(initialStop.Value, trailingStop.Value);
	}

	private void ResetStops()
	{
		if (Position == 0)
		{
			_entryPrice = 0m;
		}

		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private bool IsFridayBlocked(ICandleMessage candle)
	{
		if (FridayCutoffHour <= 0)
		{
			return false;
		}

		var time = candle.OpenTime;
		return time.DayOfWeek == DayOfWeek.Friday && time.Hour > FridayCutoffHour;
	}

	private int CalculateSilverTrendSignal()
	{
		var k = 33 - Risk;
		var uptrend = false;
		var val = 0;

		for (var i = CountBars - Ssp; i >= 0; i--)
		{
			var ssMax = GetHigh(i);
			var ssMin = GetLow(i);

			for (var i2 = i; i2 <= i + Ssp - 1; i2++)
			{
				var priceHigh = GetHigh(i2);
				if (ssMax < priceHigh)
				{
					ssMax = priceHigh;
				}

				var priceLow = GetLow(i2);
				if (ssMin >= priceLow)
				{
					ssMin = priceLow;
				}
			}

			var smin = ssMin + (ssMax - ssMin) * k / 100m;
			var smax = ssMax - (ssMax - ssMin) * k / 100m;

			if (GetClose(i) < smin)
			{
				uptrend = false;
			}

			if (GetClose(i) > smax)
			{
				uptrend = true;
			}

			val = uptrend ? 1 : -1;
		}

		return val;
	}

	private decimal CalculateJtpo(int len)
	{
		if (_closeHistory.Count < 200)
		{
			return 0m;
		}

		decimal f8 = 0m;
		decimal f10 = 0m;
		decimal f18 = 0m;
		decimal f20 = 0m;
		decimal f30 = 0m;
		decimal f40 = 0m;
		decimal k = 0m;
		decimal var14 = 0m;
		decimal var18 = 0m;
		decimal var1C = 0m;
		decimal var20 = 0m;
		decimal var24 = 0m;
		decimal value = 0m;
		var f38 = 0;
		var f48 = 0;
		var arr0 = new decimal[400];
		var arr1 = new decimal[400];
		var arr2 = new decimal[400];
		var arr3 = new decimal[400];

		for (var i = 200 - len - 100; i >= 0; i--)
		{
			var14 = 0m;
			var1C = 0m;

			if (f38 == 0)
			{
				f38 = 1;
				f40 = 0m;
				f30 = len - 1 >= 2 ? len - 1 : 2;
				f48 = (int)f30 + 1;
				f10 = GetClose(i);
				arr0[f38] = f10;
				k = f48;
				f18 = 12m / (k * (k - 1) * (k + 1));
				f20 = (f48 + 1) * 0.5m;
			}
			else
			{
				if (f38 <= f48)
				{
					f38 += 1;
				}
				else
				{
					f38 = f48 + 1;
				}

				f8 = f10;
				f10 = GetClose(i);

				if (f38 > f48)
				{
					for (var var6 = 2; var6 <= f48; var6++)
					{
						arr0[var6 - 1] = arr0[var6];
					}

					arr0[f48] = f10;
				}
				else
				{
					arr0[f38] = f10;
				}

				if (f30 >= f38 && f8 != f10)
				{
					f40 = 1m;
				}

				if (f30 == f38 && f40 == 0m)
				{
					f38 = 0;
				}
			}

			if (f38 >= f48)
			{
				for (var varA = 1; varA <= f48; varA++)
				{
					arr2[varA] = varA;
					arr3[varA] = varA;
					arr1[varA] = arr0[varA];
				}

				for (var varA = 1; varA <= f48 - 1; varA++)
				{
					var24 = arr1[varA];
					var var12 = varA;

					for (var var6 = varA + 1; var6 <= f48; var6++)
					{
						if (arr1[var6] < var24)
						{
							var24 = arr1[var6];
							var12 = var6;
						}
					}

					var20 = arr1[varA];
					arr1[varA] = arr1[var12];
					arr1[var12] = var20;

					var20 = arr2[varA];
					arr2[varA] = arr2[var12];
					arr2[var12] = var20;
				}

				var varIndex = 1;
				while (f48 > varIndex)
				{
					var var6 = varIndex + 1;
					var14 = 1m;
					var1C = arr3[varIndex];

					while (var14 != 0m && var6 < arr3.Length)
					{
						if (arr1[varIndex] != arr1[var6])
						{
							if ((var6 - varIndex) > 1)
							{
								var1C /= (var6 - varIndex);

								for (var varE = varIndex; varE <= var6 - 1; varE++)
								{
									arr3[varE] = var1C;
								}
							}

							var14 = 0m;
						}
						else
						{
							var1C += arr3[var6];
							var6 += 1;

							if (var6 > f48 + 1)
							{
								break;
							}
						}
					}

					varIndex = var6;
				}

				var1C = 0m;
				for (var varA = 1; varA <= f48; varA++)
				{
					var1C += (arr3[varA] - f20) * (arr2[varA] - f20);
				}

				var18 = f18 * var1C;
			}
			else
			{
				var18 = 0m;
			}

			value = var18;

			if (value == 0m)
			{
				value = 0.00001m;
			}
		}

		return value;
	}

	private decimal GetClose(int shift)
	{
		var index = _closeHistory.Count - 1 - shift;
		if (index < 0)
		{
			index = 0;
		}

		return _closeHistory[index];
	}

	private decimal GetHigh(int shift)
	{
		var index = _highHistory.Count - 1 - shift;
		if (index < 0)
		{
			index = 0;
		}

		return _highHistory[index];
	}

	private decimal GetLow(int shift)
	{
		var index = _lowHistory.Count - 1 - shift;
		if (index < 0)
		{
			index = 0;
		}

		return _lowHistory[index];
	}

	private decimal GetDistance(decimal points)
	{
		return points * _pointValue;
	}
}