Ver en GitHub

Estrategia de Fractals en Precios de Cierre

Descripción general

Esta estrategia es un port a StockSharp del asesor experto de MetaTrader 5 "Fractals at Close prices" de Vladimir Karputov. Analiza cinco precios de cierre consecutivos para detectar fractales al estilo de Bill Williams construidos estrictamente sobre cierres en lugar de máximos o mínimos. Los dos fractales alcistas y bajistas más recientes se comparan para determinar la tendencia activa. Cuando el último fractal alcista aparece por encima del anterior, la estrategia abre una posición larga. Cuando el último fractal bajista se forma por debajo del anterior, abre una posición corta. Las posiciones opuestas siempre se cierran antes de entrar en una nueva operación, por lo que la estrategia permanece en como máximo una dirección a la vez.

Las operaciones solo se permiten entre la hora de inicio y la hora de fin configurables. Si la hora actual cae fuera de esta ventana, todas las posiciones abiertas se cierran inmediatamente, replicando el comportamiento del EA original. El filtro de tiempo admite ventanas intradías (inicio < fin), sesiones nocturnas que cruzan la medianoche (inicio > fin) y trading durante todo el día (inicio == fin).

Lógica del indicador

  • Cada vela finalizada se añade a una cola deslizante de cinco elementos de precios de cierre.
  • Una vez disponibles cinco valores, se evalúa el cierre intermedio (dos velas atrás):
    • Se registra un fractal alcista si el cierre intermedio es estrictamente mayor que los dos cierres más antiguos y mayor o igual a los dos cierres más nuevos.
    • Se registra un fractal bajista si el cierre intermedio es estrictamente menor que los dos cierres más antiguos y menor o igual a los dos cierres más nuevos.
  • Los fractales alcistas y bajistas más recientes y anteriores se almacenan para comparación posterior.
  • Se detecta una tendencia alcista cuando el último fractal alcista es mayor que el anterior. Se detecta una tendencia bajista cuando el último fractal bajista es menor que el anterior.

Reglas de trading

  1. Entradas largas
    • Cerrar cualquier posición corta activa a mercado.
    • Si no hay posición larga abierta, comprar OrderVolume a mercado en el cierre que confirmó la secuencia de fractal alcista.
  2. Entradas cortas
    • Cerrar cualquier posición larga activa a mercado.
    • Si no hay posición corta abierta, vender OrderVolume a mercado cuando se confirma una secuencia de fractal bajista.
  3. Control de sesión
    • Antes de aplicar señales, la estrategia verifica que candle.OpenTime.Hour esté dentro de la ventana de trading. Si no, se llama a CloseAllPositions y se ignora la barra.

Gestión de riesgos

  • Las distancias de stop-loss y take-profit se expresan en pips. La implementación reproduce el enfoque MT5: el punto del símbolo se multiplica por diez cuando el instrumento tiene 3 o 5 decimales. El valor del pip resultante se multiplica entonces por las distancias configuradas.
  • Al entrar en una posición, los niveles iniciales de stop-loss y take-profit se almacenan internamente. Como StockSharp no gestiona automáticamente las órdenes protectoras al estilo MT5, la estrategia monitorea velas terminadas y sale a mercado cuando su rango de precios toca el nivel almacenado.
  • Los stops de seguimiento siguen las reglas originales del EA. Se calcula un nuevo stop como close ± TrailingStop una vez que el beneficio supera TrailingStop + TrailingStep. El trailing stop solo avanza si el movimiento desde el stop anterior es al menos TrailingStep.
  • Cuando termina el horario de trading, todas las posiciones se cierran independientemente del estado del trailing. Esto replica el EA llamando a CloseAllPositions fuera de la sesión permitida.

Parámetros

Nombre Descripción Predeterminado
OrderVolume Volumen utilizado para cada orden a mercado. 0.1
StartHour Hora (0-23) en que el trading se activa. Si es igual a EndHour, la estrategia opera todo el día. 10
EndHour Hora (0-23) en que el trading deja de aceptar nuevas señales. 22
StopLossPips Distancia del stop-loss expresada en pips. 0 desactiva el stop. 30
TakeProfitPips Distancia del take-profit expresada en pips. 0 desactiva el take. 50
TrailingStopPips Distancia base del trailing stop en pips. 0 desactiva el trailing. 15
TrailingStepPips Beneficio adicional (en pips) requerido antes de que se avance el trailing stop. 5
CandleType Tipo de datos de velas suscrito por la estrategia. El predeterminado es velas de marco temporal de 1 hora. 1 hour TimeFrame

Notas de implementación

  • La estrategia usa SubscribeCandles con la API de alto nivel y no registra indicadores manualmente, siguiendo las directrices del proyecto.
  • Las salidas protectoras (stop, take-profit, trailing stop) se ejecutan enviando órdenes a mercado después de que una vela termina, porque StockSharp no gestiona automáticamente las órdenes protectoras de MT5.
  • El filtrado de sesión, la detección de fractales y la lógica de trailing siguen estrictamente la estructura del EA, incluyendo el cierre de todas las posiciones cuando el filtro de hora no se cumple.
  • La lógica de escalado de pips refleja la implementación MT5 multiplicando el punto del símbolo por diez en instrumentos de 3 o 5 decimales, asegurando distancias de precio equivalentes.

Consejos de uso

  1. Adjuntar la estrategia a un símbolo y establecer OrderVolume al tamaño de lote preferido.
  2. Elegir un tipo de vela que coincida con el marco temporal usado en MetaTrader 5 (el EA original funciona en cualquier marco temporal).
  3. Ajustar la ventana de trading a la sesión del bróker o las horas deseadas.
  4. Ajustar las distancias basadas en pips para reflejar la volatilidad del instrumento. Un TrailingStepPips mayor reduce la frecuencia del trailing, mientras que valores menores hacen que el stop siga el precio más de cerca.
  5. Monitorear los registros para entradas y salidas; la estrategia dibuja operaciones en el área de gráfico opcional para validación visual rápida.
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>
/// Strategy converted from the MT5 "Fractals at Close prices" expert advisor.
/// Detects bullish and bearish fractal sequences built on close prices and trades trend reversals.
/// Includes configurable trading hours and manual risk management with trailing stops.
/// </summary>
public class FractalsAtClosePricesStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeWindow = new(6);

	private decimal? _lastUpperFractal;
	private decimal? _previousUpperFractal;
	private decimal? _lastLowerFractal;
	private decimal? _previousLowerFractal;

	private decimal _pipValue;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingStopDistance;
	private decimal _trailingStepDistance;

	private decimal? _entryPrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Trading volume used for every market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Hour when the strategy can start opening positions.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when the strategy stops opening positions.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Stop-loss size expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit size expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum price improvement required before moving the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

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

	/// <summary>
	/// Initializes <see cref="FractalsAtClosePricesStrategy"/> parameters.
	/// </summary>
	public FractalsAtClosePricesStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used for entries", "General")
		;

		_startHour = Param(nameof(StartHour), 0)
		.SetRange(0, 23)
		.SetDisplay("Start Hour", "Hour when trading can start (0-23)", "Trading Hours");

		_endHour = Param(nameof(EndHour), 0)
		.SetRange(0, 23)
		.SetDisplay("End Hour", "Hour when trading stops (0-23)", "Trading Hours");

		_stopLossPips = Param(nameof(StopLossPips), 200)
		.SetRange(0, 1000)
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk Management")
		;

		_takeProfitPips = Param(nameof(TakeProfitPips), 400)
		.SetRange(0, 1000)
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk Management")
		;

		_trailingStopPips = Param(nameof(TrailingStopPips), 15)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Stop (pips)", "Base distance for the trailing stop", "Risk Management")
		;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Step (pips)", "Additional move required before trailing", "Risk Management")
		;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles processed by the strategy", "General");
	}

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

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

		_closeWindow.Clear();
		_lastUpperFractal = null;
		_previousUpperFractal = null;
		_lastLowerFractal = null;
		_previousLowerFractal = null;

		_pipValue = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingStopDistance = 0m;
		_trailingStepDistance = 0m;

		_entryPrice = null;
		ResetRiskLevels();
	}

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

		var priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;

		_pipValue = priceStep;
		if (decimals == 3 || decimals == 5)
		{
			// MT5 version multiplies point value by 10 when the symbol uses 3 or 5 decimals.
			_pipValue *= 10m;
		}

		_stopLossDistance = StopLossPips == 0 ? 0m : StopLossPips * _pipValue;
		_takeProfitDistance = TakeProfitPips == 0 ? 0m : TakeProfitPips * _pipValue;
		_trailingStopDistance = TrailingStopPips == 0 ? 0m : TrailingStopPips * _pipValue;
		_trailingStepDistance = TrailingStepPips == 0 ? 0m : TrailingStepPips * _pipValue;

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

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

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

		UpdateFractals(candle);

		if (!IsWithinTradingHours(candle.OpenTime))
		{
			CloseAllPositions();
			return;
		}

		ApplyRiskManagement(candle);

		// no bound indicators, skip IsFormedAndOnlineAndAllowTrading()

		ExecuteEntries(candle);
	}

	private void UpdateFractals(ICandleMessage candle)
	{
		// Maintain a rolling window of the five most recent closes.
		_closeWindow.Add(candle.ClosePrice);
		while (_closeWindow.Count > 5)
			_closeWindow.RemoveAt(0);

		if (_closeWindow.Count < 5)
		{
			return;
		}

		var window = _closeWindow;
		var center = window[2];

		var isUpper = center > window[0]
		&& center > window[1]
		&& center >= window[3]
		&& center >= window[4];

		if (isUpper)
		{
			_previousUpperFractal = _lastUpperFractal;
			_lastUpperFractal = center;
		}

		var isLower = center < window[0]
		&& center < window[1]
		&& center <= window[3]
		&& center <= window[4];

		if (isLower)
		{
			_previousLowerFractal = _lastLowerFractal;
			_lastLowerFractal = center;
		}
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.Hour;

		if (StartHour == EndHour)
		{
			// Trade the entire day when start and end hours are equal.
			return true;
		}

		if (StartHour < EndHour)
		{
			return hour >= StartHour && hour < EndHour;
		}

		return hour >= StartHour || hour < EndHour;
	}

	private void ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				// Close the long position if the stop-loss level is breached.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			if (_longTake is decimal take && candle.HighPrice >= take)
			{
				// Close the long position when the take-profit level is hit.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			UpdateLongTrailingStop(candle);
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				// Cover the short position if the stop-loss level is breached.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				// Cover the short position when the take-profit level is hit.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			UpdateShortTrailingStop(candle);
		}
	}

	private void UpdateLongTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = candle.ClosePrice - entry;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice - _trailingStopDistance;
		if (_longStop is decimal currentStop && currentStop >= candle.ClosePrice - (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_longStop = targetStop;
	}

	private void UpdateShortTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = entry - candle.ClosePrice;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice + _trailingStopDistance;
		if (_shortStop is decimal currentStop && currentStop <= candle.ClosePrice + (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_shortStop = targetStop;
	}

	private void ExecuteEntries(ICandleMessage candle)
	{
		// Only trade when flat to avoid too frequent reversals.
		if (Position != 0)
			return;

		var bullishTrend = _lastLowerFractal is decimal lastLow
		&& _previousLowerFractal is decimal prevLow
		&& prevLow < lastLow;

		if (bullishTrend && OrderVolume > 0m)
		{
			BuyMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_longStop = _stopLossDistance > 0m ? candle.ClosePrice - _stopLossDistance : null;
			_longTake = _takeProfitDistance > 0m ? candle.ClosePrice + _takeProfitDistance : null;
			_shortStop = null;
			_shortTake = null;
			return;
		}

		var bearishTrend = _lastUpperFractal is decimal lastUp
		&& _previousUpperFractal is decimal prevUp
		&& prevUp > lastUp;

		if (bearishTrend && OrderVolume > 0m)
		{
			SellMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_shortStop = _stopLossDistance > 0m ? candle.ClosePrice + _stopLossDistance : null;
			_shortTake = _takeProfitDistance > 0m ? candle.ClosePrice - _takeProfitDistance : null;
			_longStop = null;
			_longTake = null;
		}
	}

	private void CloseAllPositions()
	{
		if (Position > 0)
		{
			SellMarket(Position);
		}
		else if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
		}

		ResetRiskLevels();
	}

	private void CloseLongPosition()
	{
		if (Position > 0)
		{
			SellMarket(Position);
			ResetRiskLevels();
		}
	}

	private void CloseShortPosition()
	{
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetRiskLevels();
		}
	}

	private void ResetRiskLevels()
	{
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		_entryPrice = null;
	}
}