Ver en GitHub

Estrategia inteligente de seguimiento de tendencias

Descripción general

La Estrategia Smart Trend Follower es una StockSharp versión del MetaTrader 5 asesores expertos Smart Trend Follower. el El sistema original alterna entre un cruce de media móvil contraria y una configuración de seguimiento de tendencias que utiliza estocástico. confirmación. Escala posiciones con un multiplicador de volumen similar a una martingala y mantiene una toma de ganancias/stop-loss compartida. para cada cesta direccional. La versión StockSharp mantiene el mismo comportamiento mientras usa el nivel alto API (vela suscripciones, vinculaciones de indicadores y órdenes de mercado).

Lógica de señal

Hay dos motores de señal independientes disponibles y se pueden cambiar con el parámetro SignalMode:

  1. CrossMa – replica el crossover contrario original. Cuando el rápido SMA cruza debajo del lento SMA (rápido < lento pero antes rápido > lento) la estrategia abre o promedia posiciones largas. Cuando el rápido SMA cruza por encima del lento SMA (rápido > lento pero antes rápido < lento) abre o promedia cortos.
  2. Tendencia: sigue el modo de tendencia original que requiere confirmación del oscilador estocástico. Una señal alcista aparece cuando el SMA rápido se mantiene por encima del SMA lento, la vela cierra más alto de lo que abrió y el valor estocástico %K está en 30 o menos. Una señal bajista requiere rápido < lento, un cuerpo de vela bajista y %K estocástico en 70 o más.

Las señales se evalúan únicamente en velas terminadas. Cada vez que llega una nueva señal mientras las posiciones opuestas aún están abiertas, el La estrategia primero liquida la canasta opuesta y solo luego procesa nuevas entradas para mantenerse alineada con la dirección de la canasta. señal actual.

Escala de posición

La estrategia reproduce la lógica de la martingala MQL:

  • El primer pedido utiliza InitialVolume lotes.
  • Cada pedido promedio adicional multiplica el volumen anterior por Multiplier (los valores ≤ 1 desactivan el crecimiento del volumen).
  • Se permite una nueva orden promedio para la dirección activa solo después de que el mercado se haya movido LayerDistancePips pips de distancia del mejor precio de entrada de la cesta actual (lleno largo más bajo o relleno corto más alto).
  • Los volúmenes se normalizan utilizando los límites del instrumento VolumeStep, VolumeMin y VolumeMax cuando estén disponibles.

Gestión del riesgo

Para cada cesta direccional, la estrategia rastrea un precio de equilibrio compartido (promedio ponderado por volumen de todos los rellenos):

  • TakeProfitPips define la distancia entre el precio medio de entrada y la obtención de beneficios de la cesta. Las cestas largas salen cuando el el máximo de la vela toca ese nivel, cestas cortas cuando el mínimo de la vela lo alcanza. Establezca en 0 para deshabilitar los objetivos de obtención de beneficios.
  • StopLossPips refleja el comportamiento de las salidas de protección. Las cestas largas se cierran cuando el mínimo de la vela cae por debajo del stop, cestas cortas cuando la vela alta cruza por encima de ella. Establezca en 0 para desactivar la parada de protección.

Las órdenes de salida se ejecutan mediante órdenes de mercado cuando la siguiente vela terminada confirma que se ha alcanzado el nivel. el La estrategia mantiene los indicadores _longExitRequested y _shortExitRequested para evitar envíos de salida duplicados mientras se completan. aún pendiente.

Parámetros

Parámetro Tipo Predeterminado Descripción
SignalMode enumeración (CrossMa, Trend) CrossMa Selecciona el motor de señal (cruce contrario o tendencia con filtro estocástico).
CandleType DataType plazo de 30 minutos Serie de velas primarias utilizadas para cálculos y generación de señales.
InitialVolume decimales 0.01 Tamaño base del pedido en lotes para la primera entrada de cualquier cesta.
Multiplier decimales 2 Multiplicador de volumen aplicado a cada orden promedio adicional.
LayerDistancePips decimales 200 Distancia mínima de pips desde la mejor entrada antes de agregar otra orden en la misma dirección.
FastPeriod entero 14 Período de la media móvil simple rápida.
SlowPeriod entero 28 Periodo de la media móvil simple lenta (debe ser mayor que FastPeriod).
StochasticKPeriod entero 10 Longitud retrospectiva de la línea %K del oscilador estocástico.
StochasticDPeriod entero 3 Longitud de suavizado para la línea estocástica %D.
StochasticSlowing entero 3 Suavizado adicional aplicado a %K antes del cálculo de %D.
TakeProfitPips decimales 500 Distancia en pips desde la entrada promedio donde se coloca la toma de ganancias de la canasta. Establezca 0 para desactivar.
StopLossPips decimales 0 Distancia de parada de protección en pips. Establezca 0 para desactivar la parada brusca.

Notas de implementación

  • El tamaño del pip se deriva del instrumento PriceStep y Decimals, coincidiendo con la noción MetaTrader de "punto" (p. ej. 0,0001 para cotizaciones FX de 5 dígitos).
  • El seguimiento de posición utiliza dos listas de objetos PositionEntry para reflejar la contabilidad por ticket de MetaTrader. Las entradas son estilo FIFO reducido cuando las operaciones opuestas cierran parte de una canasta.
  • Todos los cálculos de indicadores se basan en el enlace de alto nivel API (SubscribeCandles().BindEx(...)) de StockSharp. Sin llamadas manuales a GetValue son obligatorios y los indicadores nunca se inyectan en Strategy.Indicators.
  • La estrategia llama a StartProtection() al inicio, lo que permite a StockSharp gestionar módulos globales de control de riesgos (punto de equilibrio, controles de margen, etc.).
  • Debido a que StockSharp consolida posiciones netas por dirección, las posiciones opuestas se cierran por completo antes de que se realicen nuevas entradas. evaluado. Esto mantiene la implementación determinista y estrechamente alineada con el comportamiento original de EA.

Archivos

  • CS/SmartTrendFollowerStrategy.cs – Implementación en C# de la estrategia utilizando la API de alto nivel de StockSharp.
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>
/// Port of the "Smart Trend Follower" MetaTrader 5 expert advisor that combines moving average signals
/// with stochastic confirmation and a martingale-style layering engine.
/// </summary>
public class SmartTrendFollowerStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<SignalModes> _signalMode;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<decimal> _layerDistancePips;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSlowing;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;

	private SimpleMovingAverage _fastSma;
	private SimpleMovingAverage _slowSma;
	private StochasticOscillator _stochastic;

	private readonly List<PositionEntry> _longEntries = new();
	private readonly List<PositionEntry> _shortEntries = new();

	private decimal? _prevFast;
	private decimal? _prevSlow;
	private decimal _pipSize;
	private bool _longExitRequested;
	private bool _shortExitRequested;

	/// <summary>
	/// Trading signal mode.
	/// </summary>
	public SignalModes SignalMode
	{
		get => _signalMode.Value;
		set => _signalMode.Value = value;
	}

	/// <summary>
	/// Base candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initial order volume expressed in lots.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the volume of every additional averaging order.
	/// </summary>
	public decimal Multiplier
	{
		get => _multiplier.Value;
		set => _multiplier.Value = value;
	}

	/// <summary>
	/// Distance in pips required before stacking another order in the same direction.
	/// </summary>
	public decimal LayerDistancePips
	{
		get => _layerDistancePips.Value;
		set => _layerDistancePips.Value = value;
	}

	/// <summary>
	/// Fast simple moving average period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow simple moving average period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic oscillator %K length.
	/// </summary>
	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic oscillator %D smoothing length.
	/// </summary>
	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Additional smoothing applied to the %K line.
	/// </summary>
	public int StochasticSlowing
	{
		get => _stochasticSlowing.Value;
		set => _stochasticSlowing.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips relative to the average entry price.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips relative to the average entry price.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="SmartTrendFollowerStrategy"/>.
	/// </summary>
	public SmartTrendFollowerStrategy()
	{
		_signalMode = Param(nameof(SignalMode), SignalModes.CrossMa)
		.SetDisplay("Signal Mode", "Trading logic selection", "Signals");

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

		_initialVolume = Param(nameof(InitialVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Volume", "Starting order volume in lots", "Money Management");

		_multiplier = Param(nameof(Multiplier), 2m)
		.SetNotNegative()
		.SetDisplay("Volume Multiplier", "Martingale multiplier applied to additional entries", "Money Management");

		_layerDistancePips = Param(nameof(LayerDistancePips), 200m)
		.SetNotNegative()
		.SetDisplay("Layer Distance", "Pip distance before adding another order", "Money Management");

		_fastPeriod = Param(nameof(FastPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
		;

		_slowPeriod = Param(nameof(SlowPeriod), 28)
		.SetGreaterThanZero()
		.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
		;

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %K", "%K lookback length", "Indicators");

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators");

		_stochasticSlowing = Param(nameof(StochasticSlowing), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic Slowing", "Extra smoothing for %K", "Indicators");

		_takeProfitPips = Param(nameof(TakeProfitPips), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Target distance in pips", "Risk Management");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Protective distance in pips", "Risk Management");
	}

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

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

		_fastSma = null;
		_slowSma = null;
		_stochastic = null;

		_longEntries.Clear();
		_shortEntries.Clear();

		_prevFast = null;
		_prevSlow = null;
		_pipSize = 0m;
		_longExitRequested = false;
		_shortExitRequested = false;
	}

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

		_fastSma = new SimpleMovingAverage { Length = Math.Max(1, FastPeriod) };
		_slowSma = new SimpleMovingAverage { Length = Math.Max(1, SlowPeriod) };
		_stochastic = new StochasticOscillator();
		_stochastic.K.Length = Math.Max(1, StochasticKPeriod);
		_stochastic.D.Length = Math.Max(1, StochasticDPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_fastSma, _slowSma, _stochastic, ProcessCandle)
		.Start();

		_pipSize = CalculatePipSize();

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

		// protection managed manually via ManageExits
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;

		if (trade.Order.Side == Sides.Buy)
		{
			ReduceEntries(_shortEntries, ref volume);

			if (volume > 0m)
			{
				_longEntries.Add(new PositionEntry(price, volume));
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			ReduceEntries(_longEntries, ref volume);

			if (volume > 0m)
			{
				_shortEntries.Add(new PositionEntry(price, volume));
			}
		}

		if (GetTotalVolume(_longEntries) <= 0m)
		{
			_longEntries.Clear();
			_longExitRequested = false;
		}

		if (GetTotalVolume(_shortEntries) <= 0m)
		{
			_shortEntries.Clear();
			_shortExitRequested = false;
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue fastValue, IIndicatorValue slowValue, IIndicatorValue stochasticValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var fast = fastValue.ToDecimal();
		var slow = slowValue.ToDecimal();

		ManageExits(candle);

		var signal = SignalDirections.None;

		if (SignalMode == SignalModes.CrossMa)
		{
			if (_prevFast.HasValue && _prevSlow.HasValue)
			{
				var crossBuy = fast < slow && _prevSlow.Value < _prevFast.Value;
				var crossSell = fast > slow && _prevSlow.Value > _prevFast.Value;

				if (crossBuy)
					signal = SignalDirections.Buy;
				else if (crossSell)
					signal = SignalDirections.Sell;
			}
		}
		else if (_stochastic?.IsFormed == true)
		{
			var kValue = stochasticValue.ToDecimal();
			var bullish = candle.ClosePrice > candle.OpenPrice;
			var bearish = candle.ClosePrice < candle.OpenPrice;

			if (fast > slow && bullish && kValue <= 30m)
				signal = SignalDirections.Buy;
			else if (fast < slow && bearish && kValue >= 70m)
				signal = SignalDirections.Sell;
		}

		if (signal != SignalDirections.None)
		{
			ProcessSignal(signal, candle.ClosePrice);
		}

		_prevFast = fast;
		_prevSlow = slow;
	}

	private void ProcessSignal(SignalDirections signal, decimal referencePrice)
	{
		switch (signal)
		{
			case SignalDirections.Buy:
			{
				var shortVolume = GetTotalVolume(_shortEntries);
				if (shortVolume > 0m)
				{
					if (!_shortExitRequested)
					{
						_shortExitRequested = true;
						BuyMarket(shortVolume);
					}
					return;
				}

				var longCount = _longEntries.Count;
				var requested = CalculateRequestedVolume(longCount);
				var volume = PrepareNextVolume(requested);
				if (volume <= 0m)
					return;

				if (longCount == 0)
				{
					BuyMarket(volume);
					return;
				}

				var lowest = GetExtremePrice(_longEntries, true);
				var threshold = lowest - LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);

				if (referencePrice <= threshold)
				{
					BuyMarket(volume);
				}

				break;
			}
			case SignalDirections.Sell:
			{
				var longVolume = GetTotalVolume(_longEntries);
				if (longVolume > 0m)
				{
					if (!_longExitRequested)
					{
						_longExitRequested = true;
						SellMarket(longVolume);
					}
					return;
				}

				var shortCount = _shortEntries.Count;
				var requested = CalculateRequestedVolume(shortCount);
				var volume = PrepareNextVolume(requested);
				if (volume <= 0m)
					return;

				if (shortCount == 0)
				{
					SellMarket(volume);
					return;
				}

				var highest = GetExtremePrice(_shortEntries, false);
				var threshold = highest + LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);

				if (referencePrice >= threshold)
				{
					SellMarket(volume);
				}

				break;
			}
		}
	}

	private void ManageExits(ICandleMessage candle)
	{
		var longVolume = GetTotalVolume(_longEntries);
		if (longVolume > 0m && !_longExitRequested)
		{
			var average = GetAveragePrice(_longEntries);
			var takeProfit = TakeProfitPips > 0m ? average + TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
			var stopLoss = StopLossPips > 0m ? average - StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;

			if (takeProfit.HasValue && candle.HighPrice >= takeProfit.Value)
			{
				_longExitRequested = true;
				SellMarket(longVolume);
				return;
			}

			if (stopLoss.HasValue && candle.LowPrice <= stopLoss.Value)
			{
				_longExitRequested = true;
				SellMarket(longVolume);
				return;
			}
		}

		var shortVolume = GetTotalVolume(_shortEntries);
		if (shortVolume > 0m && !_shortExitRequested)
		{
			var average = GetAveragePrice(_shortEntries);
			var takeProfit = TakeProfitPips > 0m ? average - TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
			var stopLoss = StopLossPips > 0m ? average + StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;

			if (takeProfit.HasValue && candle.LowPrice <= takeProfit.Value)
			{
				_shortExitRequested = true;
				BuyMarket(shortVolume);
				return;
			}

			if (stopLoss.HasValue && candle.HighPrice >= stopLoss.Value)
			{
				_shortExitRequested = true;
				BuyMarket(shortVolume);
			}
		}
	}

	private decimal CalculateRequestedVolume(int existingCount)
	{
		if (InitialVolume <= 0m)
			return 0m;

		var result = InitialVolume;

		if (existingCount > 0 && Multiplier > 0m)
		{
			result *= (decimal)Math.Pow((double)Math.Max(Multiplier, 1m), existingCount);
		}

		return result;
	}

	private decimal PrepareNextVolume(decimal requested)
	{
		if (requested <= 0m)
			return 0m;

		var security = Security;
		if (security == null)
			return requested;

		var step = security.VolumeStep ?? 0m;
		if (step > 0m)
		{
			requested = step * Math.Round(requested / step, MidpointRounding.AwayFromZero);
		}

		var min = security.MinVolume ?? 0m;
		if (min > 0m && requested < min)
			return 0m;

		var max = security.MaxVolume ?? decimal.MaxValue;
		if (requested > max)
		{
			requested = max;
		}

		return requested;
	}

	private void ReduceEntries(List<PositionEntry> entries, ref decimal volume)
	{
		var index = 0;
		while (volume > 0m && index < entries.Count)
		{
			var entry = entries[index];
			if (volume >= entry.Volume)
			{
				volume -= entry.Volume;
				entries.RemoveAt(index);
			}
			else
			{
				entry.Volume -= volume;
				volume = 0m;
				entries[index] = entry;
			}
		}
	}

	private static decimal GetTotalVolume(List<PositionEntry> entries)
	{
		var total = 0m;
		for (var i = 0; i < entries.Count; i++)
			total += entries[i].Volume;
		return total;
	}

	private static decimal GetAveragePrice(List<PositionEntry> entries)
	{
		var totalVolume = GetTotalVolume(entries);
		if (totalVolume <= 0m)
			return 0m;

		var weighted = 0m;
		for (var i = 0; i < entries.Count; i++)
			weighted += entries[i].Price * entries[i].Volume;

		return weighted / totalVolume;
	}

	private static decimal GetExtremePrice(List<PositionEntry> entries, bool forLong)
	{
		if (entries.Count == 0)
			return 0m;

		var extreme = entries[0].Price;
		for (var i = 1; i < entries.Count; i++)
		{
			var price = entries[i].Price;
			if (forLong)
			{
				if (price < extreme)
					extreme = price;
			}
			else if (price > extreme)
			{
				extreme = price;
			}
		}

		return extreme;
	}

	private decimal CalculatePipSize()
	{
		var security = Security;
		if (security == null)
			return 0m;

		var step = security.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;

		var decimals = security.Decimals;
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private enum SignalDirections
	{
		None,
		Buy,
		Sell
	}

	/// <summary>
	/// Signal selector for the strategy.
	/// </summary>
	public enum SignalModes
	{
		/// <summary>
		/// Use moving average crossovers in a contrarian fashion.
		/// </summary>
		CrossMa,

		/// <summary>
		/// Follow trend direction using moving averages with stochastic confirmation.
		/// </summary>
		Trend
	}

	private sealed class PositionEntry
	{
		public PositionEntry(decimal price, decimal volume)
		{
			Price = price;
			Volume = volume;
		}

		public decimal Price { get; }

		public decimal Volume { get; set; }
	}
}