GitHub で見る

Graal フラクタル チャネル戦略

概要

Graal フラクタル チャネル戦略 は、MetaTrader 4 エキスパート アドバイザー「Graal-003」の StockSharp 移植です。このアルゴリズムは、5 つのローソク足のフラクタル パターンを監視し、適応価格チャネルを使用してブレイクアウトを確認します。有効な強気または弱気のフラクタルが表示されると、戦略はブレイクアウト方向に入る前にいくつかのフィルター (フラクタル トンネル、終値エンベロープ、およびオプションのフラット市場抑制) を評価します。オプションの Williams %R オーバーレイは、元のロボットの手動終了ロジックを複製します。一方、ヘッジストップ注文は、EA の逆トレンド保護をエミュレートするように段階的に設定できます。

データの流れと指標

  • 設定された CandleType をサブスクライブします (デフォルトでは時間ごとのキャンドル)。
  • 最後の ChannelPeriod ローソク足のローリング キューを構築し、フラット フィルターと方向性チェックに使用される Donchian のような終値チャネルを計算します。
  • ローソク足の流れから 5 バー フラクタルの高値と安値を直接検出します。
  • 組み込みの WilliamsPercentRange インジケーターをフィードして、オプションの終了信号を監視します。

取引ワークフロー

  1. フラクタル検出 – この戦略は、5 つの連続した完成したキャンドルを追跡します。中央のバーの高値/安値が、その 2 つの先行バーと 2 つの後続バーと比較して極端である場合、上部または下部のフラクタルを記録し、保留中のショートまたはロングシグナルをマークします。
  2. 信号の経年変化 – 新しいキャンドルが出現するたびに、フラクタルの年齢が増加します。 SignalAgeLimit バーが実行されずに経過すると、保留中のシグナルは期限切れになります。
  3. チャネル評価 – ローリングクローズチャネルは 3 つのフィルターを提供します。
    • フラクタル トンネル: UseFractalChannel が有効な場合、終値は最新のフラクタル高値と安値の間の距離 (DepthPercent) のパーセンテージ以内に収まる必要があります。
    • 高/低方向: UseHighLowChannel では、終値はエンベロープの限られた部分 (OrientationPercent) のみを貫通する必要があります。
    • フラット ブロッキング: AllowFlatTrading が無効になっている場合、チャネル幅が FlatThresholdPips を下回っている間、取引は一時停止されます。
  4. 注文執行 – フィルターが通過すると、ストラテジーは商品の制約に対して目的の OrderVolume を正規化し、フラクタル方向に成行注文を送信します。
  5. ヘッジストップ – UseCounterOrders がアクティブな場合、アルゴリズムはフラクタル価格プラス/マイナス OffsetPips で反対のストップ注文を配置し、EA の逆トレンドのステージングを反映します。
  6. Williams が終了 – UseWilliamsExit が有効な場合、最新の Williams %R 値は、-WilliamsThreshold を超えるとロングポジションを決済し、-100 + WilliamsThreshold を下回るとショートポジションを決済します。

ストップロスとテイクプロフィットの距離はオプションです。 StopLossPips または TakeProfitPips が正の場合、ストラテジーは商品ティック サイズを使用してピップ距離を絶対価格オフセットに変換し(EA から 3/5 桁の調整を加えて)、保護注文の管理を StartProtection に委任します。

パラメーター

パラメータ デフォルト 説明
OrderVolume 0.1 金融商品の制限に対して正規化する前のベース成行注文サイズ。
StopLossPips 500 ピップ単位の保護停止距離。価格に変換され、StartProtection 経由で適用されます。
TakeProfitPips 500 利食い距離をピップス単位で指定します。価格に変換され、StartProtection 経由で適用されます。
OffsetPips 5 カウンタートレンドのストップ注文をステージングするときに使用される追加の距離。
ChannelPeriod 14 終値チャネル用に保存された最近のローソク足の数。
UseFractalChannel false エントリー前に価格が内部フラクタルコリドー内に留まる必要があります。
DepthPercent 25 内側の回廊を定義するフラクタル範囲のパーセンテージ。
UseHighLowChannel false Donchian スタイルのクローズ チャネル指向フィルターを有効にします。
OrientationPercent 20 UseHighLowChannel が true の場合、閉じたチャネルへの侵入が許可されます。
AllowFlatTrading true クローズチャネル幅に応じて、市場が横ばいの場合でも取引が可能になります。
FlatThresholdPips 20 フラット取引が無効な場合に必要な最小チャネル幅 (ピップ単位)。
UseWilliamsExit false Williams %R ベースの終了ルールをアクティブ化します。
WilliamsPeriod 14 Williams %R インジケーターのルックバック期間。
WilliamsThreshold 30 Williams %R の感度しきい値 (パーセント ポイント) が終了しました。
UseCounterOrders false 市場エントリー後に反対の逆指値注文を出します。
SinglePosition false ポジションがオープンしている間、同じ方向への追加エントリーをブロックします。
SignalAgeLimit 3 フラクタル信号が有効である間の新しいバーの最大数。
CandleType H1 分析に使用されるローソク足データシリーズ (デフォルトは 1 時間の時間枠)。

使用上の注意

  • この戦略では、ボリュームの正規化とピップ変換が正しく機能するように、有効な PriceStep、MinVolume、および VolumeStep を持つインストゥルメントが期待されます。
  • カウンタートレンド注文は、ポジションが決済されたとき、戦略が停止したとき、または機能が無効になったときに自動的にキャンセルされます。
  • Williams %R イグジットはセーフティ ネットとして機能し、元のフラクタル シグナルがまだアクティブであってもポジションを閉じることができます。
  • このアルゴリズムは、OnReseted がトリガーされるたびに、キャッシュされたすべての状態 (フラクタル バッファー、Williams 履歴、ステージングされた注文) をリセットします。

MetaTrader バージョンとの違い

  • StockSharp 実装では、手動インジケーター ループの代わりに高レベルの SubscribeCandles().Bind(...) サブスクリプションを使用します。
  • プロテクティブストップは StartProtection に依存しているため、直接のストップ注文/指値注文の記録は必要ありません。
  • 数量は、注文が送信される前に、StockSharp の規則に従って、為替制限に対して正規化されます。

検証可能なルール: 5本足フラクタルは中央の足を左右2本ずつと厳密に比較します。高値・安値の両フラクタルを同時に満たす足では方向シグナルを出しません。終値チャネルの判定には ChannelPeriod 本の確定足が必要です。フラクタルフィルターは low+(high-low)*DepthPercent/100 <= close <= high-(high-low)*DepthPercent/100 を要求します。方向フィルターでは買いはチャネル上位 OrientationPercent%、売りは下位同率の範囲に限定されます。新たに確定した足が SignalAgeLimit 本を超えるとシグナルは失効します。反転は旧ポジション絶対量と正規化済み OrderVolume の合計を一つの成行注文で発注します。逆方向ストップは約定後かつ逆側フラクタルが既知の場合のみ発注します。

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