Открыть на GitHub

Стратегия SilverTrend V3 (C#)

Общее описание

Стратегия SilverTrend V3 перенесена из MetaTrader 5 советника "SilverTrend v3" и реализована на высокоуровневом API StockSharp. Алгоритм определяет направление тренда с помощью канальной логики SilverTrend, подтверждает сигнал осциллятором J_TPO и сопровождает позиции стопами, трейлингом и фильтром для вечерних часов пятницы.

Формирование сигналов

  1. Направление SilverTrend

    • Используется окно из 350 баров и сглаживающий параметр 9 баров для вычисления динамических уровней поддержки (smin) и сопротивления (smax).
    • Закрытие ниже smin переводит стратегию в медвежий режим, закрытие выше smax — в бычий.
    • Расчёт выполняется по всей истории окна от старых баров к новым, чтобы сохранить рекурсивную природу исходного MQL-кода.
  2. Фильтр J_TPO

    • Реализован оригинальный 14-периодный J_TPO, отражающий распределение цены внутри диапазона.
    • Открытие длинной позиции разрешается только при положительном значении осциллятора, короткой — при отрицательном, что отсекает слабые движения.
  3. Отслеживание смены режима

    • Сделки открываются только при смене знака SilverTrend относительно предыдущего значения, что уменьшает количество ложных входов.

Управление позициями

  • Рыночные входы — используется значение Volume. При наличии встречной позиции она закрывается и разворачивается одним ордером.
  • Первичный стоп-лосс — необязательный параметр в шагах цены, пересчитываемый через PriceStep инструмента относительно цены входа.
  • Тейк-профит — также задаётся в шагах цены и проверяется по экстремумам свечи, имитируя модификации ордеров оригинального советника.
  • Трейлинг-стоп — активируется после движения цены в прибыль на заданное расстояние; для лонга подтягивается вверх, для шорта — вниз.
  • Закрытие по противоположному сигналу — если предыдущий режим указывает обратное направление, позиция закрывается на следующей закрытой свече.
  • Пятничный фильтр — после указанного часа в пятницу новые сделки не открываются во избежание гэпов.

Параметры

Параметр Значение по умолчанию Описание
TrailingStopPoints 50 Дистанция трейлинг-стопа в шагах цены. Ноль отключает трейлинг.
TakeProfitPoints 50 Дистанция тейк-профита в шагах цены. Ноль отключает тейк.
InitialStopLossPoints 0 Первичный стоп-лосс в шагах цены. Ноль — без стопа.
FridayCutoffHour 16 Час биржевого времени, после которого в пятницу не открываются новые позиции. Ноль снимает ограничение.
CandleType Свечи 1H Тип свечей для расчётов, можно изменить под нужный таймфрейм.
Volume 1 Объём сделки (свойство Volume базового класса).

Все расстояния пересчитываются через PriceStep, поэтому стратегия автоматически адаптируется к инструментам с разным минимальным шагом (включая 3/5-знаковые валютные пары).

Требования к данным и окружению

  • Для запуска логики необходимо минимум 360 завершённых свечей, чтобы сформировать буферы SilverTrend и J_TPO.
  • Стратегия работает с одним инструментом, подписываясь на свечи через SubscribeCandles; метод GetWorkingSecurities возвращает именно эту пару инструмент/тип данных.
  • В OnStarted вызывается StartProtection() для подключения стандартной защиты позиций StockSharp.

Практические рекомендации

  • Лучше всего стратегия работает на трендовых и ликвидных инструментах (основные форекс-пары, фьючерсы и т.п.); подбирайте таймфрейм под текущую волатильность.
  • Из-за рекурсивного расчёта SilverTrend при нехватке истории после перезапуска придётся дождаться накопления достаточного числа свечей.
  • Стоп-лосс, тейк-профит и трейлинг проверяются по экстремумам свечи. В реальной торговле при необходимости можно дополнительно выставлять биржевые стоп/лимит ордера.
  • Внутренние переменные (_previousSignal, _entryPrice, параметры трейлинга) обновляются один раз на закрытии свечи, что повторяет поведение оригинальной версии "один бар — одно решение".

Особенности переноса

  • Полностью воспроизведены математические процедуры из SilverTrend v3.mq5, включая многомерный расчёт индикатора J_TPO.
  • Соблюдены правила репозитория: параметры оформлены через StrategyParam<T>, все комментарии на английском языке, отступы сделаны табуляцией.
  • По условию задачи создана только C#-версия; папка и реализация на Python не добавлялись.
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;
	}
}