Ver en GitHub

Estrategia SendClose

Descripción general

SendClose es una estrategia de ruptura basada en fractales que recrea el comportamiento del asesor experto de MT5 original. El algoritmo construye continuamente líneas dinámicas de soporte y resistencia vinculando pivotes fractales alternantes y reacciona en el momento en que el precio vuelve a esos niveles proyectados. El port de StockSharp mantiene intactas las mecánicas centrales: las líneas de tendencia se generan a partir de secuencias de fractales alternantes arriba/abajo, las rupturas desencadenan entradas de mercado, y se usan líneas de offset separadas para forzar la liquidación de posiciones.

Flujo de detección de fractales

  1. Ventana de cinco velas – la estrategia mantiene un buffer rodante de las últimas cinco velas completadas. Tan pronto como la ventana esté llena, evalúa la vela central frente a los dos vecinos más antiguos y los dos más nuevos.
  2. Regla de fractal ascendente – la vela central forma un fractal ascendente cuando su máximo es mayor que los máximos de las dos velas más nuevas y estrictamente mayor que los máximos de las dos velas más antiguas. Esto coincide con la lógica iFractals de MT5 (>= en el lado más nuevo, > en el lado más antiguo).
  3. Regla de fractal descendente – de manera similar, la vela central es un fractal descendente si su mínimo es menor o igual comparado con las velas más nuevas y estrictamente menor que las dos velas más antiguas.
  4. Cola de fractales – cada fractal recién confirmado se introduce en una cola FIFO de seis elementos ordenada de más reciente a más antiguo. Esta cola se escanea posteriormente para encontrar los patrones alternantes requeridos.

Construcción de líneas de tendencia

  • Línea de venta – el algoritmo busca la secuencia más reciente fractal ascendente → fractal descendente → fractal ascendente. La línea se traza a través del primer y último fractal ascendente, conectando efectivamente dos máximos de oscilación separados por un mínimo de oscilación.
  • Línea de compra – simétricamente, busca una cadena fractal descendente → fractal ascendente → fractal descendente y conecta los mínimos de oscilación circundantes.
  • Proyección – los puntos finales almacenados (tiempo y precio) se usan para interpolar o extrapolar el valor de la línea para cualquier marca de tiempo posterior. Cuando el mercado alcanza la proyección al cierre de la vela actual, se toma una decisión de trading.
  • Líneas de cierre – se calculan dos niveles auxiliares desplazando la línea de venta hacia arriba y la línea de compra hacia abajo por LineOffsetSteps * PriceStep. Actúan como disparadores de salida forzada igual que las líneas Close1/Close2 originales.

Lógica de trading

  • Condiciones de entrada
    • Vender cuando el precio toca la línea de venta y no hay exposición larga en conflicto. La exposición corta existente puede aumentarse hasta alcanzar el límite MaxPositions.
    • Comprar cuando el precio toca la línea de compra y no hay exposición corta en conflicto. La exposición larga existente puede aumentarse hasta el mismo límite.
  • Condiciones de salida
    • El precio que toca cualquier línea de cierre cierra inmediatamente la posición abierta, emulando el comportamiento de MT5 donde tocar Close1/Close2 emite una salida completa.
    • Las señales de entrada intentan aplanar posiciones opuestas antes de colocar la nueva orden, reflejando la adaptación de cobertura a neteo dentro de StockSharp.
  • Detección de contacto – la precisión de tick de MT5 se aproxima con datos de velas. Un nivel se considera "tocado" cuando se encuentra entre el máximo y el mínimo de la vela.

Parámetros

Nombre Descripción
EnableSellLine Habilita o deshabilita las órdenes basadas en la línea fractal superior (de venta).
EnableBuyLine Habilita o deshabilita las órdenes basadas en la línea fractal inferior (de compra).
EnableCloseSellLine Activa el nivel Close1 que cierra posiciones cuando el precio sube por encima de la línea de venta más el offset.
EnableCloseBuyLine Activa el nivel Close2 que cierra posiciones cuando el precio cae por debajo de la línea de compra menos el offset.
MaxPositions Número máximo de lotes que pueden permanecer abiertos en una dirección. Las entradas adicionales que superen este límite se ignoran.
OrderVolume Volumen de cada orden de mercado. El valor debe coincidir con el tamaño del contrato del instrumento.
LineOffsetSteps Offset, medido en pasos de precio, usado al calcular los niveles Close1/Close2. El valor predeterminado de 15 replica el desplazamiento 15*Point() de MT5.
CandleType Serie de velas usada para el análisis. Elija un marco temporal que coincida con el gráfico que planea operar (ej., M15, H1).

Notas de implementación

  • La estrategia se ejecuta en velas completadas para respetar el EA original, que dependía de barras MT5 confirmadas antes de evaluar los fractales.
  • La igualdad a nivel de tick con bid/ask se aproxima con rangos de velas. Si se requiere mayor precisión, alimentar datos de tick en lugar de velas.
  • El parámetro MaxPositions opera sobre la posición neta de StockSharp. Por lo tanto es adecuado para cuentas de neteo; las cuentas de cobertura aún pueden simular el escalado aumentando MaxPositions.
  • Las líneas de cierre se evalúan antes que las entradas. Si tanto una salida como una entrada se activan en la misma vela, la salida tiene prioridad, evitando órdenes conflictivas.

Directrices de uso

  1. Configure el símbolo y el marco temporal deseados en su terminal StockSharp y asegúrese de que el instrumento proporcione información de PriceStep. La lógica de offset depende de ello.
  2. Ajuste CandleType para que coincida con el marco temporal que desea analizar. El valor predeterminado es 30 minutos, que ofrece un equilibrio entre ruido y capacidad de respuesta.
  3. Establezca OrderVolume en el tamaño de posición que desea enviar por operación. Para futuros, use recuentos de contratos; para CFDs de FX, use tamaños de lote.
  4. Ajuste LineOffsetSteps para alinearse con la volatilidad del instrumento. Los offsets más grandes requieren un movimiento más fuerte para activar las salidas Close1/Close2.
  5. Monitoree el número de lotes abiertos cuando aumente MaxPositions. La estrategia no superará este límite pero puede seguir piramidando posiciones en mercados con tendencia.

Diferencias con la versión MT5

  • StockSharp opera con posiciones netas, por lo que el código aplana la exposición opuesta antes de abrir una nueva operación en lugar de mantener tickets de compra/venta simultáneos.
  • Los objetos de gráfico no se dibujan automáticamente. Si necesita visualización en el gráfico, conecte un módulo de gráfico y trace los valores de línea generados manualmente.
  • La detección de contacto basada en velas puede activarse ligeramente más tarde que las verificaciones de tick de MT5, especialmente en mercados rápidos con velas amplias.

Gestión del riesgo

La estrategia coloca órdenes de mercado sin stop-losses integrados. Siempre complémenlela con controles de riesgo externos como stops de capital, filtros de horario de trading o supervisión manual. Haga backtesting extensivo en el instrumento y marco temporal objetivo antes de desplegarla en vivo.

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>
/// SendClose strategy replicates fractal breakout lines with close-based exits.
/// This class recreates the MT5 SendClose expert using StockSharp high level API.
/// </summary>
public class SendCloseStrategy : Strategy
{
	private enum FractalTypes
	{
		Up,
		Down
	}

	private readonly struct FractalPoint
	{
		public FractalPoint(FractalTypes type, DateTimeOffset time, decimal price)
		{
			Type = type;
			Time = time;
			Price = price;
		}

		public FractalTypes Type { get; }
		public DateTimeOffset Time { get; }
		public decimal Price { get; }
	}

	private readonly struct FractalLine
	{
		public FractalLine(FractalPoint recent, FractalPoint older)
		{
			if (recent.Time < older.Time)
			{
				Recent = older;
				Older = recent;
			}
			else
			{
				Recent = recent;
				Older = older;
			}
		}

		public FractalPoint Recent { get; }
		public FractalPoint Older { get; }

		public decimal GetPrice(DateTimeOffset time)
		{
			var totalSeconds = (decimal)(Recent.Time - Older.Time).TotalSeconds;
			if (totalSeconds == 0m)
				return Recent.Price;

			var offsetSeconds = (decimal)(time - Older.Time).TotalSeconds;
			return Older.Price + (Recent.Price - Older.Price) * (offsetSeconds / totalSeconds);
		}
	}

	private readonly StrategyParam<bool> _enableSellLine;
	private readonly StrategyParam<bool> _enableBuyLine;
	private readonly StrategyParam<bool> _enableCloseSellLine;
	private readonly StrategyParam<bool> _enableCloseBuyLine;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _lineOffsetSteps;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _h0;
	private decimal _h1;
	private decimal _h2;
	private decimal _h3;
	private decimal _h4;

	private decimal _l0;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal _l4;

	private DateTimeOffset _t0;
	private DateTimeOffset _t1;
	private DateTimeOffset _t2;
	private DateTimeOffset _t3;
	private DateTimeOffset _t4;

	private int _bufferCount;

	private FractalPoint? _fractal0;
	private FractalPoint? _fractal1;
	private FractalPoint? _fractal2;
	private FractalPoint? _fractal3;
	private FractalPoint? _fractal4;
	private FractalPoint? _fractal5;

	private FractalLine? _sellLine;
	private FractalLine? _buyLine;

	/// <summary>
	/// Enable sell breakout line.
	/// </summary>
	public bool EnableSellLine
	{
		get => _enableSellLine.Value;
		set => _enableSellLine.Value = value;
	}

	/// <summary>
	/// Enable buy breakout line.
	/// </summary>
	public bool EnableBuyLine
	{
		get => _enableBuyLine.Value;
		set => _enableBuyLine.Value = value;
	}

	/// <summary>
	/// Enable upper close line (based on sell trend line).
	/// </summary>
	public bool EnableCloseSellLine
	{
		get => _enableCloseSellLine.Value;
		set => _enableCloseSellLine.Value = value;
	}

	/// <summary>
	/// Enable lower close line (based on buy trend line).
	/// </summary>
	public bool EnableCloseBuyLine
	{
		get => _enableCloseBuyLine.Value;
		set => _enableCloseBuyLine.Value = value;
	}

	/// <summary>
	/// Maximum number of lots that can remain open.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Order volume per entry.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Offset in price steps for close lines.
	/// </summary>
	public int LineOffsetSteps
	{
		get => _lineOffsetSteps.Value;
		set => _lineOffsetSteps.Value = value;
	}

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

	/// <summary>
	/// Initialize <see cref="SendCloseStrategy"/>.
	/// </summary>
	public SendCloseStrategy()
	{
		_enableSellLine = Param(nameof(EnableSellLine), true)
			.SetDisplay("Sell Line", "Enable sell fractal breakout line", "General");

		_enableBuyLine = Param(nameof(EnableBuyLine), true)
			.SetDisplay("Buy Line", "Enable buy fractal breakout line", "General");

		_enableCloseSellLine = Param(nameof(EnableCloseSellLine), true)
			.SetDisplay("Close Line 1", "Enable closing line above sell trend", "General");

		_enableCloseBuyLine = Param(nameof(EnableCloseBuyLine), true)
			.SetDisplay("Close Line 2", "Enable closing line below buy trend", "General");

		_maxPositions = Param(nameof(MaxPositions), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum number of simultaneous lots", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 0.10m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume per signal", "Risk");

		_lineOffsetSteps = Param(nameof(LineOffsetSteps), 60)
			.SetGreaterThanZero()
			.SetDisplay("Offset Steps", "Offset in price steps for close levels", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

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

		// Clear buffers that hold recent highs, lows, and times.
		_h0 = _h1 = _h2 = _h3 = _h4 = 0m;
		_l0 = _l1 = _l2 = _l3 = _l4 = 0m;
		_t0 = _t1 = _t2 = _t3 = _t4 = default;
		_bufferCount = 0;

		// Reset stored fractal points and active lines.
		_fractal0 = _fractal1 = _fractal2 = _fractal3 = _fractal4 = _fractal5 = null;
		_sellLine = null;
		_buyLine = null;
	}

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

		// Subscribe to candle data and process each completed candle.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with completed candles to match the MT5 expert behaviour.
		if (candle.State != CandleStates.Finished)
			return;

		// Update internal buffers and detect new fractal points.
		UpdateBuffers(candle);
		UpdateFractalLines();

		// Ensure trading is allowed before evaluating signals.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var offset = GetOffset();
		var shouldClose = false;

		// Check closing logic derived from the upper fractal trend line.
		if (EnableCloseSellLine && _sellLine is { } sellLine)
		{
			var closePrice = GetLinePrice(sellLine, candle.CloseTime) + offset;
			if (IsTouched(closePrice, candle))
				shouldClose = true;
		}

		// Check closing logic derived from the lower fractal trend line.
		if (EnableCloseBuyLine && _buyLine is { } buyLine)
		{
			var closePrice = GetLinePrice(buyLine, candle.CloseTime) - offset;
			if (IsTouched(closePrice, candle))
				shouldClose = true;
		}

		// Close any open position if price reached one of the close lines.
		if (shouldClose && Position != 0m)
		{
			if (Position > 0) SellMarket(); else BuyMarket();
			return;
		}

		// Entry logic for sell breakout.
		if (EnableSellLine && _sellLine is { } sellEntryLine)
		{
			var sellPrice = GetLinePrice(sellEntryLine, candle.CloseTime);
			if (IsTouched(sellPrice, candle))
			{
				if (Position > 0m)
				{
					// Flatten long positions before attempting to go short.
					SellMarket();
				}
				else if (CanIncreaseShort())
				{
					SellMarket(OrderVolume);
				}
			}
		}

		// Entry logic for buy breakout.
		if (EnableBuyLine && _buyLine is { } buyEntryLine)
		{
			var buyPrice = GetLinePrice(buyEntryLine, candle.CloseTime);
			if (IsTouched(buyPrice, candle))
			{
				if (Position < 0m)
				{
					// Flatten short positions before attempting to go long.
					BuyMarket();
				}
				else if (CanIncreaseLong())
				{
					BuyMarket(OrderVolume);
				}
			}
		}
	}

	private void UpdateBuffers(ICandleMessage candle)
	{
		// Shift buffers to keep the latest five candles for fractal detection.
		_h4 = _h3;
		_h3 = _h2;
		_h2 = _h1;
		_h1 = _h0;
		_h0 = candle.HighPrice;

		_l4 = _l3;
		_l3 = _l2;
		_l2 = _l1;
		_l1 = _l0;
		_l0 = candle.LowPrice;

		_t4 = _t3;
		_t3 = _t2;
		_t2 = _t1;
		_t1 = _t0;
		_t0 = candle.OpenTime;

		if (_bufferCount < 5)
		{
			_bufferCount++;
			return;
		}

		// Identify new fractal points once enough candles are available.
		if (IsUpFractal())
			RegisterFractal(new FractalPoint(FractalTypes.Up, _t2, _h2));

		if (IsDownFractal())
			RegisterFractal(new FractalPoint(FractalTypes.Down, _t2, _l2));
	}

	private void UpdateFractalLines()
	{
		// Build the sell line using the most recent up-down-up pattern.
		if (TryBuildLine(FractalTypes.Up, out var sellLine))
			_sellLine = sellLine;

		// Build the buy line using the most recent down-up-down pattern.
		if (TryBuildLine(FractalTypes.Down, out var buyLine))
			_buyLine = buyLine;
	}

	private bool IsUpFractal()
	{
		return _h2 >= _h3 && _h2 > _h4 && _h2 >= _h1 && _h2 > _h0;
	}

	private bool IsDownFractal()
	{
		return _l2 <= _l3 && _l2 < _l4 && _l2 <= _l1 && _l2 < _l0;
	}

	private void RegisterFractal(FractalPoint point)
	{
		// Skip duplicates that can appear on flat sequences.
		if (_fractal0 is { } latest && latest.Time == point.Time && latest.Type == point.Type)
			return;

		_fractal5 = _fractal4;
		_fractal4 = _fractal3;
		_fractal3 = _fractal2;
		_fractal2 = _fractal1;
		_fractal1 = _fractal0;
		_fractal0 = point;
	}

	private bool TryBuildLine(FractalTypes target, out FractalLine line)
	{
		line = default;
		FractalPoint? latest = null;
		FractalPoint? middle = null;
		FractalPoint? oldest = null;

		foreach (var item in EnumerateFractals())
		{
			if (item is not { } point)
				continue;

			if (latest is null)
			{
				if (point.Type == target)
					latest = point;
				continue;
			}

			if (middle is null)
			{
				if (point.Type != target)
					middle = point;
				continue;
			}

			if (point.Type == target)
			{
				oldest = point;
				break;
			}
		}

		if (latest is not { } latestPoint || middle is null || oldest is not { } oldestPoint)
			return false;

		if (latestPoint.Time == oldestPoint.Time)
			return false;

		line = new FractalLine(latestPoint, oldestPoint);
		return true;
	}

	private IEnumerable<FractalPoint?> EnumerateFractals()
	{
		yield return _fractal0;
		yield return _fractal1;
		yield return _fractal2;
		yield return _fractal3;
		yield return _fractal4;
		yield return _fractal5;
	}

	private bool CanIncreaseShort()
	{
		if (OrderVolume <= 0m || MaxPositions <= 0)
			return false;

		var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
		return lots < MaxPositions;
	}

	private bool CanIncreaseLong()
	{
		if (OrderVolume <= 0m || MaxPositions <= 0)
			return false;

		var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
		return lots < MaxPositions;
	}

	private decimal GetOffset()
	{
		var step = Security?.PriceStep ?? 1m;
		return step * LineOffsetSteps;
	}

	private static bool IsTouched(decimal price, ICandleMessage candle)
	{
		return price <= candle.HighPrice && price >= candle.LowPrice;
	}

	private static decimal GetLinePrice(FractalLine line, DateTimeOffset time)
	{
		return line.GetPrice(time);
	}
}