Ver en GitHub

Estrategia del canal Graal Fractal

Descripción general

La Estrategia de canal Fractal de Graal es una versión StockSharp del asesor experto MetaTrader 4 "Graal-003". El algoritmo observa patrones fractales de cinco velas y confirma las rupturas utilizando canales de precios adaptativos. Cuando aparece un fractal alcista o bajista válido, la estrategia evalúa varios filtros (túnel fractal, envolvente de precio de cierre y supresión opcional del mercado plano) antes de entrar en la dirección de ruptura. Una superposición opcional de Williams %R replica la lógica de salida manual del robot original, mientras que las órdenes de stop de cobertura se pueden preparar para emular la protección contratendencia del EA.

Flujo de datos e indicadores.

  • Se suscribe al CandleType configurado (velas horarias por defecto).
  • Crea una cola continua de las últimas ChannelPeriod velas para calcular un canal de precio de cierre similar a Donchian utilizado para filtros planos y comprobaciones de orientación.
  • Detecta máximos y mínimos fractales de cinco barras directamente desde el flujo de velas.
  • Alimenta el indicador incorporado WilliamsPercentRange para monitorear señales de salida opcionales.

Flujo de trabajo comercial

  1. Detección de fractales: la estrategia rastrea cinco velas terminadas consecutivas. Cuando el máximo/mínimo de la barra media es el extremo en comparación con sus dos predecesores y dos seguidores, registra un fractal superior o inferior y marca una señal corta o larga pendiente.
  2. Envejecimiento de la señal: cada nueva vela aumenta la edad fractal. Si SignalAgeLimit barras pasan sin ejecución, la señal pendiente caduca.
  3. Evaluación del canal: el canal de cierre rodante proporciona tres filtros:
    • Túnel fractal: cuando UseFractalChannel está habilitado, el precio de cierre debe permanecer dentro de un porcentaje de la distancia entre el último máximo y mínimo fractal (DepthPercent).
    • Orientación alta/baja: con UseHighLowChannel, el cierre debe penetrar solo una porción limitada del sobre (OrientationPercent).
    • Bloqueo plano: si AllowFlatTrading está deshabilitado, las operaciones se suspenden mientras el ancho del canal se mantenga por debajo de FlatThresholdPips.
  4. Ejecución de la orden: una vez que pasan los filtros, la estrategia normaliza el OrderVolume deseado frente a las restricciones del instrumento y envía una orden de mercado en la dirección fractal.
  5. Paradas de cobertura: cuando UseCounterOrders está activo, el algoritmo coloca la orden de parada opuesta al precio fractal más/menos OffsetPips, reflejando la puesta en escena de contratendencia de EA.
  6. Williams sale: si UseWilliamsExit está habilitado, el valor %R Williams más reciente cierra posiciones largas cuando sube por encima de -WilliamsThreshold y posiciones cortas cuando cae por debajo de -100 + WilliamsThreshold.

Las distancias de parada de pérdidas y toma de ganancias son opcionales. Siempre que StopLossPips o TakeProfitPips sea positivo, la estrategia convierte la distancia del pip en una compensación de precio absoluta utilizando el tamaño del tick del instrumento (con el ajuste de 3/5 dígitos de EA) y delega la gestión de órdenes de protección a StartProtection.

Parámetros

Parámetro Predeterminado Descripción
OrderVolume 0.1 Tamaño base de la orden de mercado antes de la normalización frente a los límites del instrumento.
StopLossPips 500 Distancia de parada de protección en pips. Convertido a precio y aplicado a través de StartProtection.
TakeProfitPips 500 Tomar distancia de ganancias en pips. Convertido a precio y aplicado a través de StartProtection.
OffsetPips 5 Distancia adicional utilizada al organizar órdenes stop contratendencia.
ChannelPeriod 14 Número de velas recientes almacenadas para el canal de precio de cierre.
UseFractalChannel false Requiere que el precio permanezca dentro del corredor fractal interior antes de las entradas.
DepthPercent 25 Porcentaje del rango fractal que define el corredor interior.
UseHighLowChannel false Habilita el filtro de orientación de canal cerrado estilo Donchian.
OrientationPercent 20 Penetración permitida en el canal cercano cuando UseHighLowChannel es verdadero.
AllowFlatTrading true Permite operar incluso cuando el mercado está plano según el ancho del canal de cierre.
FlatThresholdPips 20 Ancho mínimo del canal (en pips) requerido cuando el comercio plano está deshabilitado.
UseWilliamsExit false Activa Williams reglas de salida basadas en %R.
WilliamsPeriod 14 Período retroactivo para el indicador Williams %R.
WilliamsThreshold 30 Umbral de sensibilidad (puntos porcentuales) para Williams %R salidas.
UseCounterOrders false Coloca la orden stop opuesta después de una entrada al mercado.
SinglePosition false Bloquea entradas adicionales en la misma dirección mientras una posición está abierta.
SignalAgeLimit 3 Número máximo de barras nuevas durante las cuales una señal fractal permanece válida.
CandleType H1 Serie de datos de velas utilizada para el análisis (el valor predeterminado es un período de una hora).

Notas de uso

  • La estrategia espera instrumentos con PriceStep, MinVolume y VolumeStep válidos para que la normalización de volumen y la conversión de pips funcionen correctamente.
  • Las órdenes contratendencia se cancelan automáticamente cuando se cierra la posición, cuando se detiene la estrategia o cuando se desactiva la función.
  • Williams Las salidas %R actúan como una red de seguridad y pueden cerrar posiciones incluso si la señal fractal original todavía está activa.
  • El algoritmo restablece todo el estado almacenado en caché (búferes fractales, historial de Williams, pedidos en etapas) cada vez que se activa OnReseted.

Diferencias con la versión MetaTrader

  • La implementación StockSharp utiliza suscripciones SubscribeCandles().Bind(...) de alto nivel en lugar de bucles de indicadores manuales.
  • Las paradas de protección dependen de StartProtection, por lo que no se requiere contabilidad de órdenes de parada/límite directas.
  • El volumen se normaliza según los límites de cambio antes de enviar los pedidos, coincidiendo con las convenciones StockSharp.

Reglas verificables: un fractal de cinco velas exige comparaciones estrictas entre la vela central y las dos velas a cada lado. Una vela que es a la vez fractal superior e inferior no genera entrada. El canal necesita ChannelPeriod cierres. El filtro fractal exige low+(high-low)*DepthPercent/100 <= close <= high-(high-low)*DepthPercent/100. El filtro de orientación permite compras en el OrientationPercent% superior del canal y ventas en el mismo porcentaje inferior. La señal caduca tras más de SignalAgeLimit velas cerradas. El giro usa una única orden de mercado por el valor absoluto de la posición anterior más OrderVolume normalizado. La orden stop contraria se crea tras un llenado real y únicamente si se conoce el nivel fractal opuesto.

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.MatchingEngine;
using StockSharp.Messages;

/// <summary>
/// Trades confirmed five-bar fractal signals with optional channel, Williams %R
/// and counter-stop filters. The indicator midpoint is not an entry signal.
/// </summary>
public class GraalFractalChannelStrategy : Strategy
{
    private readonly StrategyParam<decimal> _orderVolume;
    private readonly StrategyParam<decimal> _stopLossPips;
    private readonly StrategyParam<decimal> _takeProfitPips;
    private readonly StrategyParam<decimal> _offsetPips;
    private readonly StrategyParam<int> _channelPeriod;
    private readonly StrategyParam<bool> _useFractalChannel;
    private readonly StrategyParam<decimal> _depthPercent;
    private readonly StrategyParam<bool> _useHighLowChannel;
    private readonly StrategyParam<decimal> _orientationPercent;
    private readonly StrategyParam<bool> _allowFlatTrading;
    private readonly StrategyParam<decimal> _flatThresholdPips;
    private readonly StrategyParam<bool> _useWilliamsExit;
    private readonly StrategyParam<int> _williamsPeriod;
    private readonly StrategyParam<decimal> _williamsThreshold;
    private readonly StrategyParam<bool> _useCounterOrders;
    private readonly StrategyParam<bool> _singlePosition;
    private readonly StrategyParam<int> _signalAgeLimit;
    private readonly StrategyParam<DataType> _candleType;

    private readonly Queue<(decimal High, decimal Low)> _bars = new();
    private readonly Queue<decimal> _closes = new();
    private decimal? _upperFractal;
    private decimal? _lowerFractal;
    private Sides? _pendingSide;
    private int _pendingAge;
    private int _finishedBars;
    private Order _entryOrder;
    private Order _counterOrder;
    private bool _closing;

    public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }
    public decimal StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
    public decimal TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
    public decimal OffsetPips { get => _offsetPips.Value; set => _offsetPips.Value = value; }
    public int ChannelPeriod { get => _channelPeriod.Value; set => _channelPeriod.Value = value; }
    public bool UseFractalChannel { get => _useFractalChannel.Value; set => _useFractalChannel.Value = value; }
    public decimal DepthPercent { get => _depthPercent.Value; set => _depthPercent.Value = value; }
    public bool UseHighLowChannel { get => _useHighLowChannel.Value; set => _useHighLowChannel.Value = value; }
    public decimal OrientationPercent { get => _orientationPercent.Value; set => _orientationPercent.Value = value; }
    public bool AllowFlatTrading { get => _allowFlatTrading.Value; set => _allowFlatTrading.Value = value; }
    public decimal FlatThresholdPips { get => _flatThresholdPips.Value; set => _flatThresholdPips.Value = value; }
    public bool UseWilliamsExit { get => _useWilliamsExit.Value; set => _useWilliamsExit.Value = value; }
    public int WilliamsPeriod { get => _williamsPeriod.Value; set => _williamsPeriod.Value = value; }
    public decimal WilliamsThreshold { get => _williamsThreshold.Value; set => _williamsThreshold.Value = value; }
    public bool UseCounterOrders { get => _useCounterOrders.Value; set => _useCounterOrders.Value = value; }
    public bool SinglePosition { get => _singlePosition.Value; set => _singlePosition.Value = value; }
    public int SignalAgeLimit { get => _signalAgeLimit.Value; set => _signalAgeLimit.Value = value; }
    public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

    public GraalFractalChannelStrategy()
    {
        _orderVolume = Param(nameof(OrderVolume), 0.1m).SetGreaterThanZero().SetDisplay("Volume", "Entry volume", "Trading");
        _stopLossPips = Param(nameof(StopLossPips), 500m).SetNotNegative().SetDisplay("Stop Loss", "Pip distance", "Protection");
        _takeProfitPips = Param(nameof(TakeProfitPips), 500m).SetNotNegative().SetDisplay("Take Profit", "Pip distance", "Protection");
        _offsetPips = Param(nameof(OffsetPips), 5m).SetNotNegative().SetDisplay("Offset", "Counter stop offset in pips", "Protection");
        _channelPeriod = Param(nameof(ChannelPeriod), 14).SetGreaterThanZero().SetDisplay("Channel Period", "Completed closes", "Indicators");
        _useFractalChannel = Param(nameof(UseFractalChannel), false);
        _depthPercent = Param(nameof(DepthPercent), 25m);
        _useHighLowChannel = Param(nameof(UseHighLowChannel), false);
        _orientationPercent = Param(nameof(OrientationPercent), 20m);
        _allowFlatTrading = Param(nameof(AllowFlatTrading), true);
        _flatThresholdPips = Param(nameof(FlatThresholdPips), 20m);
        _useWilliamsExit = Param(nameof(UseWilliamsExit), false);
        _williamsPeriod = Param(nameof(WilliamsPeriod), 14).SetGreaterThanZero();
        _williamsThreshold = Param(nameof(WilliamsThreshold), 30m);
        _useCounterOrders = Param(nameof(UseCounterOrders), false);
        _singlePosition = Param(nameof(SinglePosition), false);
        _signalAgeLimit = Param(nameof(SignalAgeLimit), 3).SetNotNegative();
        _candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame());
    }

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

    protected override void OnReseted()
    {
        base.OnReseted();
        _bars.Clear();
        _closes.Clear();
        _upperFractal = null;
        _lowerFractal = null;
        _pendingSide = null;
        _pendingAge = 0;
        _finishedBars = 0;
        _entryOrder = null;
        _counterOrder = null;
        _closing = false;
    }

    protected override void OnStarted2(DateTime time)
    {
        base.OnStarted2(time);
        _bars.Clear();
        _closes.Clear();
        _upperFractal = null;
        _lowerFractal = null;
        _pendingSide = null;
        _pendingAge = 0;
        _finishedBars = 0;
        _entryOrder = null;
        _counterOrder = null;
        _closing = false;
        Volume = OrderVolume;

        var pip = PipSize();
        StartProtection(
            takeProfit: TakeProfitPips > 0m ? new Unit(TakeProfitPips * pip, UnitTypes.Absolute) : new Unit(),
            stopLoss: StopLossPips > 0m ? new Unit(StopLossPips * pip, UnitTypes.Absolute) : new Unit(),
            useMarketOrders: true, isLocalStop: true);

        var williams = new WilliamsR { Length = WilliamsPeriod };
        SubscribeCandles(CandleType).Bind(williams, ProcessCandle).Start();
    }

    private decimal PipSize()
    {
        var step = Security?.PriceStep ?? 0m;
        if (step <= 0m)
            throw new InvalidOperationException("Graal Fractal Channel requires a positive instrument price step.");
        return Security?.Decimals is 3 or 5 ? step * 10m : step;
    }

    private decimal NormalizeVolume(decimal volume)
    {
        if (Security?.VolumeStep is decimal step && step > 0m)
            volume = decimal.Floor(volume / step) * step;
        if (Security?.MinVolume is decimal min && min > 0m)
            volume = Math.Max(volume, min);
        if (Security?.MaxVolume is decimal max && max > 0m)
            volume = Math.Min(volume, max);
        return volume;
    }

    private static bool Pending(Order order) => order is not null &&
        order.State is not (OrderStates.Done or OrderStates.Failed);

    private void CancelCounter()
    {
        if (_counterOrder is not null && _counterOrder.State == OrderStates.Active)
            CancelOrder(_counterOrder);
        _counterOrder = null;
    }

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

        _finishedBars++;
        if (_pendingSide.HasValue && ++_pendingAge > SignalAgeLimit)
            _pendingSide = null;

        _bars.Enqueue((candle.HighPrice, candle.LowPrice));
        if (_bars.Count > 5)
            _bars.Dequeue();
        _closes.Enqueue(candle.ClosePrice);
        if (_closes.Count > ChannelPeriod)
            _closes.Dequeue();

        if (_bars.Count == 5)
        {
            var bars = _bars.ToArray();
            var upper = bars.Where((_, i) => i != 2).All(b => bars[2].High > b.High);
            var lower = bars.Where((_, i) => i != 2).All(b => bars[2].Low < b.Low);
            if (upper) _upperFractal = bars[2].High;
            if (lower) _lowerFractal = bars[2].Low;
            // A bar can be both a high and a low fractal: it is not a directional signal.
            if (upper != lower)
            {
                _pendingSide = upper ? Sides.Sell : Sides.Buy;
                _pendingAge = 0;
            }
            else if (upper)
                _pendingSide = null;
        }

        if (!UseCounterOrders || Position == 0m)
            CancelCounter();

        if (_closing)
        {
            if (Position != 0m)
                return;
            _closing = false;
        }

        if (UseWilliamsExit && _finishedBars >= WilliamsPeriod &&
            (Position > 0m && williams > -WilliamsThreshold ||
             Position < 0m && williams < -100m + WilliamsThreshold))
        {
            CancelCounter();
            _pendingSide = null;
            _closing = true;
            ClosePosition();
            return;
        }

        if (!_pendingSide.HasValue || Pending(_entryOrder) || !PassesFilters(candle.ClosePrice, _pendingSide.Value))
            return;

        var side = _pendingSide.Value;
        if (SinglePosition && (side == Sides.Buy ? Position > 0m : Position < 0m))
            return;

        var volume = NormalizeVolume(OrderVolume);
        if (volume <= 0m)
            return;

        // Reverse the existing net position in one order, rather than submitting
        // two uncoordinated default-volume orders.
        if (side == Sides.Buy && Position < 0m || side == Sides.Sell && Position > 0m)
            volume += Math.Abs(Position);

        CancelCounter();
        _entryOrder = new Order
        {
            Security = Security,
            Portfolio = Portfolio,
            Type = OrderTypes.Market,
            Side = side,
            Volume = volume,
            Comment = "Graal fractal entry",
        };
        RegisterOrder(_entryOrder);
        _pendingSide = null;
    }

    private bool PassesFilters(decimal close, Sides side)
    {
        if (_closes.Count < ChannelPeriod)
            return false;

        var highest = _closes.Max();
        var lowest = _closes.Min();
        var width = highest - lowest;
        if (!AllowFlatTrading && width < FlatThresholdPips * PipSize())
            return false;

        if (UseFractalChannel)
        {
            if (_upperFractal is not decimal high || _lowerFractal is not decimal low || high <= low)
                return false;
            var inner = (high - low) * DepthPercent / 100m;
            if (close < low + inner || close > high - inner)
                return false;
        }

        if (UseHighLowChannel)
        {
            if (width <= 0m)
                return false;
            var allowed = width * OrientationPercent / 100m;
            if (side == Sides.Buy && close < highest - allowed ||
                side == Sides.Sell && close > lowest + allowed)
                return false;
        }
        return true;
    }

    protected override void OnOwnTradeReceived(MyTrade trade)
    {
        base.OnOwnTradeReceived(trade);

        if (Position == 0m || !UseCounterOrders)
        {
            CancelCounter();
            return;
        }

        if (trade?.Order != _entryOrder || trade.Trade is null)
            return;

        CancelCounter();
        var side = Position > 0m ? Sides.Sell : Sides.Buy;
        var level = side == Sides.Sell ? _lowerFractal - OffsetPips * PipSize()
                                       : _upperFractal + OffsetPips * PipSize();
        if (level is not decimal price || price <= 0m)
            return;

        _counterOrder = new Order
        {
            Security = Security,
            Portfolio = Portfolio,
            Type = OrderTypes.Conditional,
            Side = side,
            Volume = Math.Abs(Position),
            Condition = new StopOrderCondition { ActivationPrice = Security.ShrinkPrice(price, ShrinkRules.Auto) },
            Comment = "Graal counter stop",
        };
        RegisterOrder(_counterOrder);
    }
}