Ver no GitHub

Estratégia de Canal Graal Fractal

Visão geral

A Estratégia de Canal Fractal Graal é uma versão StockSharp do MetaTrader 4 consultor especialista "Graal-003". O algoritmo observa padrões fractais de cinco velas e confirma rompimentos usando canais de preços adaptativos. Quando um fractal válido de alta ou baixa aparece, a estratégia avalia vários filtros (túnel fractal, envelope de preço de fechamento e supressão opcional de mercado estável) antes de entrar na direção de rompimento. Uma sobreposição %R Williams opcional replica a lógica de saída manual do robô original, enquanto ordens de parada de hedge podem ser preparadas para emular a proteção contra tendência do EA.

Fluxo de dados e indicadores

  • Assina o CandleType configurado (velas horárias por padrão).
  • Cria uma fila contínua das últimas ChannelPeriod velas para calcular um canal de preço de fechamento semelhante a Donchian usado para filtros planos e verificações de orientação.
  • Detecta máximos e mínimos fractais de cinco barras diretamente do fluxo da vela.
  • Alimenta o indicador WilliamsPercentRange integrado para monitorar sinais de saída opcionais.

Fluxo de trabalho de negociação

  1. Detecção fractal – a estratégia rastreia cinco velas concluídas consecutivas. Quando a máxima/mínima da barra intermediária é o extremo em comparação com seus dois antecessores e dois seguidores, ela registra um fractal superior ou inferior e marca um sinal curto ou longo pendente.
  2. Envelhecimento do sinal – cada nova vela aumenta a idade fractal. Se SignalAgeLimit barras passarem sem execução, o sinal pendente expira.
  3. Avaliação de canal – o canal de fechamento contínuo fornece três filtros:
    • Túnel fractal: quando UseFractalChannel está ativado, o preço de fechamento deve permanecer dentro de uma porcentagem da distância entre a última máxima e mínima fractal (DepthPercent).
    • Orientação alta/baixa: com UseHighLowChannel, o fechamento deve penetrar apenas uma parte limitada do envelope (OrientationPercent).
    • Bloqueio plano: se AllowFlatTrading estiver desativado, as negociações serão suspensas enquanto a largura do canal permanecer abaixo de FlatThresholdPips.
  4. Execução da ordem – uma vez aprovados os filtros, a estratégia normaliza o OrderVolume desejado em relação às restrições do instrumento e envia uma ordem de mercado na direção fractal.
  5. Paradas de hedge – quando UseCounterOrders está ativo, o algoritmo coloca a ordem de stop oposta no preço fractal mais/menos OffsetPips, espelhando o teste de contratendência de EA.
  6. Williams sai – se UseWilliamsExit estiver ativado, o valor mais recente de Williams %R fecha posições longas quando subir acima de -WilliamsThreshold e posições curtas quando cair abaixo de -100 + WilliamsThreshold.

As distâncias Stop Loss e Take Profit são opcionais. Sempre que StopLossPips ou TakeProfitPips for positivo, a estratégia converte a distância do pip em uma compensação de preço absoluto usando o tamanho do tick do instrumento (com o ajuste de 3/5 dígitos do EA) e delega o gerenciamento de ordens de proteção para StartProtection.

Parâmetros

Parâmetro Padrão Descrição
OrderVolume 0.1 Tamanho base da ordem de mercado antes da normalização em relação aos limites do instrumento.
StopLossPips 500 Distância de parada protetora em pips. Convertido em preço e aplicado via StartProtection.
TakeProfitPips 500 Tire a distância do lucro em pips. Convertido em preço e aplicado via StartProtection.
OffsetPips 5 Distância extra usada ao preparar ordens de stop contra-tendência.
ChannelPeriod 14 Número de velas recentes armazenadas para o canal de preço de fechamento.
UseFractalChannel false Requer que o preço permaneça dentro do corredor fractal interno antes das entradas.
DepthPercent 25 Porcentagem do intervalo fractal que define o corredor interno.
UseHighLowChannel false Ativa o filtro de orientação de canal fechado estilo Donchian.
OrientationPercent 20 Penetração permitida no canal próximo quando UseHighLowChannel é verdadeiro.
AllowFlatTrading true Permite a negociação mesmo quando o mercado está estável de acordo com a largura do canal próximo.
FlatThresholdPips 20 Largura mínima do canal (em pips) necessária quando a negociação plana está desativada.
UseWilliamsExit false Ativa regras de saída baseadas em Williams %R.
WilliamsPeriod 14 Período de lookback para o indicador Williams %R.
WilliamsThreshold 30 Limite de sensibilidade (pontos percentuais) para Williams%R saídas.
UseCounterOrders false Coloca a ordem stop oposta após uma entrada no mercado.
SinglePosition false Bloqueia entradas adicionais na mesma direção enquanto uma posição está aberta.
SignalAgeLimit 3 Número máximo de novas barras durante as quais um sinal fractal permanece válido.
CandleType H1 Série de dados de velas usada para análise (o padrão é o período de uma hora).

Notas de uso

  • A estratégia espera instrumentos com PriceStep, MinVolume e VolumeStep válidos para que a normalização de volume e a conversão de pip funcionem corretamente.
  • As ordens contra-tendência são canceladas automaticamente quando a posição é fechada, quando a estratégia é interrompida ou quando o recurso é desativado.
  • Williams As saídas %R atuam como uma rede de segurança e podem fechar posições mesmo se o sinal fractal original ainda estiver ativo.
  • O algoritmo redefine todos os estados armazenados em cache (buffers fractais, histórico de Williams, pedidos preparados) sempre que OnReseted é acionado.

Diferenças da versão MetaTrader

  • A implementação StockSharp usa assinaturas SubscribeCandles().Bind(...) de alto nível em vez de loops de indicadores manuais.
  • As paradas de proteção dependem de StartProtection, portanto, nenhuma contabilidade direta de ordens de parada/limite é necessária.
  • O volume é normalizado em relação aos limites da exchange antes do envio dos pedidos, correspondendo às convenções StockSharp.

Regras verificáveis: um fractal de cinco candles exige comparação estrita do candle central com os dois vizinhos de cada lado. Um candle que seja fractal superior e inferior ao mesmo tempo não gera entrada. O canal exige ChannelPeriod fechamentos. O filtro fractal requer low+(high-low)*DepthPercent/100 <= close <= high-(high-low)*DepthPercent/100. A orientação permite compras nos OrientationPercent% superiores e vendas nos inferiores do canal. O sinal expira após mais de SignalAgeLimit candles fechados. A reversão usa uma única ordem a mercado pelo módulo da posição anterior mais OrderVolume normalizado. A stop oposta só é enviada após execução real e quando o fractal contrário é conhecido.

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);
    }
}