Ver en GitHub

Estrategia Fractured Fractals

Port del clásico asesor experto MetaTrader "Fractured Fractals". La estrategia rastrea fractales de Williams confirmados, coloca órdenes stop en nuevos niveles de ruptura y sigue un stop de protección en el fractal opuesto.

Detalles

  • Fuente: Convertido de MQL/20127/Fractured Fractals.mq5.
  • Régimen de mercado: Continuación de ruptura en cualquier instrumento compatible con StockSharp.
  • Tipos de órdenes: Usa órdenes stop para entradas y órdenes stop de protección para salidas.
  • Dimensionamiento de posición: Basado en riesgo, controlado por MaximumRiskPercent y la lógica de racha adaptativa DecreaseFactor.
  • Parámetros predeterminados:
    • MaximumRiskPercent = 2%
    • DecreaseFactor = 10
    • ExpirationHours = 1 hora
    • CandleType = Marco temporal de 1 hora
  • Indicadores principales: Fractales nativos de Williams de cinco barras calculados al vuelo.
  • Tipo de estrategia: Ruptura larga/corta con gestión dinámica de stops.

Lógica de la estrategia

Seguimiento de la secuencia de fractales

  • Mantiene colas de los últimos cinco máximos y mínimos de velas para simular el búfer iFractals en MT5.
  • Cada fractal confirmado desplaza tres ranuras rodantes: más joven, media y antigua. Los valores duplicados se ignoran usando el paso de precio del instrumento como tolerancia.
  • Las señales largas requieren que el fractal alto más reciente supere el fractal medio; las señales cortas requieren que el fractal bajo más reciente sea inferior al anterior.

Órdenes de entrada y vencimiento

  • Cuando no existe posición larga ni orden de compra stop pendiente, la estrategia coloca un buy stop en el fractal alto más reciente con un stop loss en el fractal bajo más reciente.
  • Simétricamente, las entradas cortas colocan un sell stop en el fractal bajo más reciente con un stop de protección en el fractal alto más reciente.
  • Las órdenes pendientes heredan una expiración definida por ExpirationHours. Si el tiempo de la vela supera la expiración o la jerarquía de fractales invalida la configuración (nuevo fractal alto más bajo para largos o fractal bajo más alto para cortos), la orden se cancela.
  • El bot mantiene el libro limpio cancelando órdenes opuestas una vez que se abre una posición.

Stops de protección con trailing

  • Cada fractal opuesto confirmado actualiza la orden de stop de protección: las posiciones largas siguen el fractal bajo más reciente, las posiciones cortas siguen el fractal alto más reciente.
  • Los stops solo se ajustan — los nuevos niveles deben mejorar sobre el precio de la orden existente antes de que se produzca un reemplazo.
  • Cuando la posición se cierra, cualquier orden de stop restante se cancela inmediatamente.

Gestión de riesgo y control de racha

  • CalculateOrderVolume replica el cálculo de riesgo de MT5: riesgo por unidad = precio de entrada menos precio de stop (o viceversa para cortos).
  • El riesgo monetario objetivo equivale a Portfolio.CurrentValue * MaximumRiskPercent / 100 con un respaldo a la propiedad Volume cuando la valoración del portafolio no está disponible.
  • El volumen resultante se normaliza por tamaño de lote, paso de volumen, volumen mínimo y restricciones de volumen máximo expuestas por Security.
  • Después de una operación perdedora el contador de racha se incrementa; las operaciones rentables o planas reinician el contador. Si ocurre más de una pérdida consecutiva, el tamaño se escala hacia abajo en losses / DecreaseFactor.

Seguimiento del resultado de las operaciones

  • OnOwnTradeReceived agrega ejecuciones para determinar cuándo se completa un ciclo de posición y si terminó positivo, negativo o plano.
  • El contador de racha y la última marca de tiempo rentable reflejan la lógica original, permitiendo extensiones adicionales (p. ej., análisis) si se desea.

Notas de uso

  1. Adjunte la estrategia a cualquier par instrumento/portafolio, ajuste CandleType a la resolución deseada y configure los parámetros de riesgo según el tamaño de la cuenta.
  2. Asegúrese de que el adaptador/broker admita órdenes stop; de lo contrario, reemplace las órdenes de protección con salidas manuales en UpdateTrailingStops.
  3. Dado que la implementación procesa solo velas completadas, los picos intra-barra más pequeños que la resolución de la vela no desencadenarán órdenes exactamente como en las pruebas de MT5 basadas en ticks.
  4. Considere habilitar el registro para revisar los mensajes de comentarios producidos por el port en C#, reflejando la retroalimentación del experto original.
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 "Fractured Fractals" MetaTrader strategy using high-level StockSharp API.
/// Places stop orders on newly confirmed fractals and trails the stop with the opposite fractal.
/// </summary>
public class FracturedFractalsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _maximumRiskPercent;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<int> _expirationHours;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _highBuffer = new();
	private readonly List<decimal> _lowBuffer = new();

	private decimal? _lastUpFractal;
	private decimal? _lastDownFractal;
	private decimal? _upYoungest;
	private decimal? _upMiddle;
	private decimal? _upOld;
	private decimal? _downYoungest;
	private decimal? _downMiddle;
	private decimal? _downOld;

	private decimal? _buyStopLevel;
	private decimal? _sellStopLevel;
	private decimal? _longStopLevel;
	private decimal? _shortStopLevel;
	private DateTimeOffset? _buyStopExpiry;
	private DateTimeOffset? _sellStopExpiry;
	private decimal _buyStopVolume;
	private decimal _sellStopVolume;

	private decimal _entryPrice;
	private int _consecutiveLosses;

	/// <summary>
	/// Maximum risk per trade expressed as percentage of portfolio value.
	/// </summary>
	public decimal MaximumRiskPercent
	{
		get => _maximumRiskPercent.Value;
		set => _maximumRiskPercent.Value = value;
	}

	/// <summary>
	/// Factor that reduces position size after consecutive losing trades.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Pending order lifetime in hours.
	/// </summary>
	public int ExpirationHours
	{
		get => _expirationHours.Value;
		set => _expirationHours.Value = value;
	}

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

	/// <summary>
	/// Initialize <see cref="FracturedFractalsStrategy"/> with default parameters.
	/// </summary>
	public FracturedFractalsStrategy()
	{
		_maximumRiskPercent = Param(nameof(MaximumRiskPercent), 2m)
		.SetRange(0.0001m, 100m)
		.SetDisplay("Max Risk %", "Maximum risk per trade", "Risk");

		_decreaseFactor = Param(nameof(DecreaseFactor), 10m)
		.SetRange(0m, 1000m)
		.SetDisplay("Decrease Factor", "Loss streak position size dampener", "Risk");

		_expirationHours = Param(nameof(ExpirationHours), 1)
		.SetRange(0, 240)
		.SetDisplay("Expiration", "Pending order lifetime (hours)", "General");

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

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

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

		_highBuffer.Clear();
		_lowBuffer.Clear();

		_lastUpFractal = null;
		_lastDownFractal = null;
		_upYoungest = null;
		_upMiddle = null;
		_upOld = null;
		_downYoungest = null;
		_downMiddle = null;
		_downOld = null;

		_buyStopLevel = null;
		_sellStopLevel = null;
		_longStopLevel = null;
		_shortStopLevel = null;
		_buyStopExpiry = null;
		_sellStopExpiry = null;
		_buyStopVolume = 0m;
		_sellStopVolume = 0m;

		_entryPrice = 0m;
		_consecutiveLosses = 0;
	}

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

		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;

		_highBuffer.Add(candle.HighPrice);
		_lowBuffer.Add(candle.LowPrice);

		if (_highBuffer.Count > 5)
			_highBuffer.RemoveAt(0);
		if (_lowBuffer.Count > 5)
			_lowBuffer.RemoveAt(0);

		if (_highBuffer.Count < 5 || _lowBuffer.Count < 5)
			return;

		DetectFractals();

		// Check protective stop levels
		CheckProtectiveStops(candle);

		// Validate pending levels
		ValidatePendingLevels(candle.CloseTime);

		// Check if pending buy/sell stop levels are triggered
		CheckPendingTriggers(candle);

		// Update trailing stops
		UpdateTrailingStops();

		// Try to set new pending levels
		if (Position == 0)
		{
			if (!TrySetBuyStopLevel(candle.CloseTime))
				TrySetSellStopLevel(candle.CloseTime);
		}
	}

	private void DetectFractals()
	{
		var highs = _highBuffer.ToArray();
		var lows = _lowBuffer.ToArray();

		decimal? upFractal = null;
		decimal? downFractal = null;

		if (highs[2] > highs[0] && highs[2] > highs[1] && highs[2] > highs[3] && highs[2] > highs[4])
			upFractal = highs[2];

		if (lows[2] < lows[0] && lows[2] < lows[1] && lows[2] < lows[3] && lows[2] < lows[4])
			downFractal = lows[2];

		if (upFractal is decimal up && !AreEqual(_lastUpFractal, up))
		{
			_lastUpFractal = up;
			_upOld = _upMiddle;
			_upMiddle = _upYoungest;
			_upYoungest = up;
		}

		if (downFractal is decimal down && !AreEqual(_lastDownFractal, down))
		{
			_lastDownFractal = down;
			_downOld = _downMiddle;
			_downMiddle = _downYoungest;
			_downYoungest = down;
		}
	}

	private void CheckProtectiveStops(ICandleMessage candle)
	{
		if (Position > 0 && _longStopLevel.HasValue)
		{
			if (candle.LowPrice <= _longStopLevel.Value)
			{
				SellMarket(Math.Abs(Position));
				_longStopLevel = null;
				_consecutiveLosses++;
				return;
			}
		}

		if (Position < 0 && _shortStopLevel.HasValue)
		{
			if (candle.HighPrice >= _shortStopLevel.Value)
			{
				BuyMarket(Math.Abs(Position));
				_shortStopLevel = null;
				_consecutiveLosses++;
				return;
			}
		}
	}

	private void UpdateTrailingStops()
	{
		if (Position > 0 && _downYoungest.HasValue)
		{
			if (!_longStopLevel.HasValue || _downYoungest.Value > _longStopLevel.Value)
				_longStopLevel = _downYoungest.Value;
		}
		else if (Position <= 0)
		{
			_longStopLevel = null;
		}

		if (Position < 0 && _upYoungest.HasValue)
		{
			if (!_shortStopLevel.HasValue || _upYoungest.Value < _shortStopLevel.Value)
				_shortStopLevel = _upYoungest.Value;
		}
		else if (Position >= 0)
		{
			_shortStopLevel = null;
		}
	}

	private void ValidatePendingLevels(DateTimeOffset currentTime)
	{
		if (_buyStopLevel.HasValue && _upYoungest.HasValue)
		{
			if (_upYoungest.Value < _buyStopLevel.Value && !AreEqual(_upYoungest, _buyStopLevel.Value))
			{
				_buyStopLevel = null;
				_buyStopExpiry = null;
			}
		}

		if (_sellStopLevel.HasValue && _downYoungest.HasValue)
		{
			if (_downYoungest.Value > _sellStopLevel.Value && !AreEqual(_downYoungest, _sellStopLevel.Value))
			{
				_sellStopLevel = null;
				_sellStopExpiry = null;
			}
		}

		if (_buyStopLevel.HasValue && _buyStopExpiry.HasValue && currentTime >= _buyStopExpiry.Value)
		{
			_buyStopLevel = null;
			_buyStopExpiry = null;
		}

		if (_sellStopLevel.HasValue && _sellStopExpiry.HasValue && currentTime >= _sellStopExpiry.Value)
		{
			_sellStopLevel = null;
			_sellStopExpiry = null;
		}

		if (Position != 0)
		{
			_buyStopLevel = null;
			_sellStopLevel = null;
			_buyStopExpiry = null;
			_sellStopExpiry = null;
		}
	}

	private void CheckPendingTriggers(ICandleMessage candle)
	{
		if (_buyStopLevel.HasValue && candle.HighPrice >= _buyStopLevel.Value && Position <= 0)
		{
			var buyLevel = _buyStopLevel.Value;
			var vol = _buyStopVolume > 0m ? _buyStopVolume : Volume;
			if (vol > 0m)
			{
				if (Position < 0)
					BuyMarket(Math.Abs(Position));
				BuyMarket(vol);
				_entryPrice = buyLevel;
				_longStopLevel = _downYoungest;
			}
			_buyStopLevel = null;
			_buyStopExpiry = null;
		}

		if (_sellStopLevel.HasValue && candle.LowPrice <= _sellStopLevel.Value && Position >= 0)
		{
			var sellLevel = _sellStopLevel.Value;
			var vol = _sellStopVolume > 0m ? _sellStopVolume : Volume;
			if (vol > 0m)
			{
				if (Position > 0)
					SellMarket(Math.Abs(Position));
				SellMarket(vol);
				_entryPrice = sellLevel;
				_shortStopLevel = _upYoungest;
			}
			_sellStopLevel = null;
			_sellStopExpiry = null;
		}
	}

	private bool TrySetBuyStopLevel(DateTimeOffset time)
	{
		if (Position > 0 || _buyStopLevel.HasValue)
			return false;

		if (_upYoungest is not decimal up || _upMiddle is not decimal middle || _downYoungest is not decimal stop)
			return false;

		if (up <= middle || stop >= up)
			return false;

		var volume = CalculateOrderVolume(up, stop, Sides.Buy);
		if (volume <= 0m)
			return false;

		_buyStopLevel = up;
		_buyStopVolume = volume;
		_buyStopExpiry = ExpirationHours > 0 ? time + TimeSpan.FromHours(ExpirationHours) : null;
		return true;
	}

	private void TrySetSellStopLevel(DateTimeOffset time)
	{
		if (Position < 0 || _sellStopLevel.HasValue)
			return;

		if (_downYoungest is not decimal down || _downMiddle is not decimal middle || _upYoungest is not decimal stop)
			return;

		if (down >= middle || stop <= down)
			return;

		var volume = CalculateOrderVolume(down, stop, Sides.Sell);
		if (volume <= 0m)
			return;

		_sellStopLevel = down;
		_sellStopVolume = volume;
		_sellStopExpiry = ExpirationHours > 0 ? time + TimeSpan.FromHours(ExpirationHours) : null;
	}

	private decimal CalculateOrderVolume(decimal entryPrice, decimal stopPrice, Sides direction)
	{
		var riskPerUnit = direction == Sides.Buy ? entryPrice - stopPrice : stopPrice - entryPrice;
		if (riskPerUnit <= 0m)
			return 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		if (portfolioValue <= 0m)
			portfolioValue = Volume > 0m ? Volume * entryPrice : 0m;

		var riskAmount = portfolioValue * (MaximumRiskPercent / 100m);
		if (riskAmount <= 0m)
			return 0m;

		var volume = riskAmount / riskPerUnit;

		if (DecreaseFactor > 0m && _consecutiveLosses > 1)
			volume -= volume * (_consecutiveLosses / DecreaseFactor);

		if (volume <= 0m)
			return 0m;

		return Math.Max(volume, Volume > 0 ? Volume : 1m);
	}

	private bool AreEqual(decimal? first, decimal second)
	{
		if (first is not decimal value)
			return false;

		var step = Security?.PriceStep ?? 0.00000001m;
		return Math.Abs(value - second) <= step / 2m;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0m && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0m)
			_entryPrice = 0m;
	}
}