Ver en GitHub

Fractals Distancia Mínima

Descripción general

Fractals Distancia Mínima replica el asesor experto de MetaTrader "Fractals minimum distance" utilizando la API de estrategia de alto nivel de StockSharp. El sistema escanea la serie de velas configurada en busca de patrones fractales de cinco barras estilo Bill Williams. Cada vez que aparece un nuevo fractal confirmado en el desplazamiento de barra de señal especificado, la estrategia mide la brecha entre los fractales de subida y bajada más recientes. Solo se permite una orden de mercado cuando esta distancia supera el umbral requerido expresado en pips.

La conversión mantiene el comportamiento original de cerrar cualquier exposición opuesta inmediatamente antes de revertir. A diferencia de la versión MQL, el tamaño de la posición se toma de la propiedad Volume de la estrategia en lugar de realizar cálculos de riesgo basados en la cuenta. No se envían órdenes de stop-loss ni take-profit, coincidiendo con el experto fuente.

Lógica de señales

  1. Suscribirse al tipo de vela definido por CandleType y construir buffers deslizantes de máximos y mínimos que siempre contengan la barra ubicada SignalBar velas en el pasado junto con dos vecinas en cada lado.
  2. Detectar un fractal superior cuando el máximo de la barra central es estrictamente mayor que los máximos de las dos velas precedentes y las dos siguientes. Detectar un fractal inferior de forma análoga para los mínimos.
  3. Convertir el parámetro DistancePips a una distancia de precio usando el PriceStep del símbolo. Los símbolos con tres o cinco dígitos decimales se ajustan automáticamente para tratar las cotizaciones 0.001/0.00001 como un pip.
  4. Cuando se confirma un fractal superior:
    • Almacenar el nuevo nivel superior y cerrar las posiciones largas existentes.
    • Si tanto el último fractal superior como el inferior son conocidos y su diferencia absoluta es al menos el umbral de distancia, enviar una orden de venta de mercado usando Volume.
  5. Cuando se confirma un fractal inferior:
    • Almacenar el nuevo nivel inferior y cerrar las posiciones cortas existentes.
    • Si se cumple la condición de distancia, enviar una orden de compra de mercado usando Volume.

Las operaciones se realizan solo después de que se cierra la vela que finaliza el fractal, asegurando que las barras inacabadas nunca desencadenen entradas. La estrategia se basa en IsFormedAndOnlineAndAllowTrading() para evitar colocar órdenes antes de que el entorno esté listo.

Parámetros

Nombre Descripción Notas
DistancePips Espaciado mínimo entre los últimos fractales de subida y bajada medido en pips. Convertido internamente a unidades de precio usando el tamaño del tick del instrumento.
SignalBar Número de barras completamente cerradas que deben pasar después de la barra que aloja el fractal. El valor efectivo mínimo es 2, coincidiendo con la confirmación de dos barras usada por los fractales de Bill Williams.
CandleType Serie de datos que alimenta los cálculos. El valor predeterminado es el marco temporal de un minuto; cambiar para trabajar con otras resoluciones.
Volume Propiedad estándar de la estrategia StockSharp que define el tamaño de la operación. Reemplaza el dimensionamiento basado en riesgo original del experto MetaTrader.

Gestión de posiciones y diferencias con MQL

  • Las posiciones siempre se aplanan antes de revertir la dirección, exactamente como lo hacía el helper ClosePositions fuente.
  • El experto original llamaba a RefreshRates() y realizaba configuraciones explícitas de deslizamiento. Esos aspectos se delegan a la infraestructura de StockSharp en este port.
  • Las órdenes de stop-loss y take-profit no eran parte de la lógica MQL y permanecen ausentes aquí.
  • DistancePips usa precisión entera como la entrada ushort, mientras que SignalBar refleja la entrada uchar de MQL.
  • Dado que StockSharp trabaja con posiciones netas, abrir una orden en dirección opuesta automáticamente voltea la exposición, coincidiendo con el comportamiento de netting de MetaTrader.

Consejos de uso

  • Comience con el mismo desplazamiento de barra de señal (SignalBar = 3) del código original y calibre el umbral de distancia según la volatilidad del instrumento.
  • Aumente SignalBar para esperar más velas después de que aparezca un fractal, lo que puede filtrar oscilaciones rápidas.
  • Combine con gestión de riesgo externa como el helper integrado StartProtection() si se requiere un stop de protección.
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>
/// Fractals minimum distance breakout strategy converted from MetaTrader.
/// </summary>
public class FractalsMinimumDistanceStrategy : Strategy
{
	private readonly StrategyParam<int> _distancePips;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _prevUpperFractal;
	private decimal? _prevLowerFractal;
	private decimal[] _highs = Array.Empty<decimal>();
	private decimal[] _lows = Array.Empty<decimal>();
	private int _bufferCount;
	private int _windowSize;
	private int _signalOffset;
	private decimal _pipSize;

	public int DistancePips
	{
		get => _distancePips.Value;
		set => _distancePips.Value = value;
	}

	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public FractalsMinimumDistanceStrategy()
	{
		_distancePips = Param(nameof(DistancePips), 15)
			.SetDisplay("Distance (pips)", "Minimum allowed gap between the last two fractals", "Risk")
			;

		_signalBar = Param(nameof(SignalBar), 3)
			.SetDisplay("Signal bar offset", "How many closed bars ago the fractal must appear", "General")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle type", "Primary candle series used for signals", "Data")
			;
	}

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

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

		_prevUpperFractal = null;
		_prevLowerFractal = null;
		_highs = Array.Empty<decimal>();
		_lows = Array.Empty<decimal>();
		_bufferCount = 0;
		_windowSize = 0;
		_signalOffset = 0;
		_pipSize = 0m;
	}

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

		InitializeBuffers();
		_pipSize = CalculatePipSize();

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

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

	private void InitializeBuffers()
	{
		_signalOffset = Math.Max(2, SignalBar);
		_windowSize = Math.Max(_signalOffset + 3, 5);
		_highs = new decimal[_windowSize];
		_lows = new decimal[_windowSize];
		_bufferCount = 0;
	}

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

		// Shift the rolling buffers to keep the configured number of historical bars.
		for (var i = 0; i < _windowSize - 1; i++)
		{
			_highs[i] = _highs[i + 1];
			_lows[i] = _lows[i + 1];
		}

		// Append the latest candle extremes.
		_highs[_windowSize - 1] = candle.HighPrice;
		_lows[_windowSize - 1] = candle.LowPrice;

		if (_bufferCount < _windowSize)
			_bufferCount++;

		if (_bufferCount < _windowSize)
			return;

		var centerIndex = _windowSize - 1 - _signalOffset;
		if (centerIndex < 2 || centerIndex > _windowSize - 3)
			return;

		var high = _highs[centerIndex];
		var low = _lows[centerIndex];

		var isUpperFractal =
			high > _highs[centerIndex - 1] &&
			high > _highs[centerIndex - 2] &&
			high > _highs[centerIndex + 1] &&
			high > _highs[centerIndex + 2];

		var isLowerFractal =
			low < _lows[centerIndex - 1] &&
			low < _lows[centerIndex - 2] &&
			low < _lows[centerIndex + 1] &&
			low < _lows[centerIndex + 2];

		var distanceThreshold = DistancePips * _pipSize;

		if (isUpperFractal)
		{
			_prevUpperFractal = high;

			// Close existing long exposure before reversing.
			if (Position > 0)
				SellMarket();

			// Enter a short position if the fractals are far enough apart.
			if (ShouldOpenTrade(distanceThreshold))
				SellMarket();
		}

		if (isLowerFractal)
		{
			_prevLowerFractal = low;

			// Close existing short exposure before reversing.
			if (Position < 0)
				BuyMarket();

			// Enter a long position if the fractals are far enough apart.
			if (ShouldOpenTrade(distanceThreshold))
				BuyMarket();
		}
	}

	private bool ShouldOpenTrade(decimal distanceThreshold)
	{
		if (Volume <= 0)
			return false;

		if (_prevUpperFractal is not decimal upper || _prevLowerFractal is not decimal lower)
			return false;

		var threshold = Math.Abs(distanceThreshold);
		return Math.Abs(upper - lower) >= threshold;
	}

	private decimal CalculatePipSize()
	{
		var priceStep = Security?.PriceStep;

		if (priceStep is not decimal step || step <= 0m)
			return 1m;

		var decimals = GetDecimalPlaces(step);

		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}
}