GitHub で見る

ExFractals戦略

概要

ExFractals戦略は、ウィリアムズスタイルのフラクタルレベルをExVolの平均ボディモメンタムフィルターと組み合わせたブレイクアウトシステムです。アルゴリズムは最新の確認されたフラクタル高値と安値を継続的に監視し、ペアで平均化し、ExVolの読み取り値が動きの方向を確認しながら価格がそれらの平均化されたレベルを超えて閉じるとトレードを開始します。

トレードロジック

  1. フラクタル検出
    • キャンドルは閉じた後に処理されます。
    • 上向き(弱気)と下向き(強気)のフラクタルは、5本のキャンドルウィンドウの中央のキャンドルが隣接するものと比較して厳密な極値である場合に検出されます。
    • 戦略はタイムスタンプと共に各サイドごとに最新の2つの確認済みフラクタルを保存します。
    • 各サイドは最後の2つのフラクタル価格の平均に等しいアクション可能なレベルを生成します。同じフラクタルを2回使用しないよう重複するタイムスタンプは無視されます。
  2. ExVolフィルター
    • ExVol値は選択したルックバック期間のキャンドルボディ(終値マイナス始値)を価格ステップで表した単純平均に等しくなります。
    • 負のExVolは持続的な強気キャンドル(正の始値対終値)を示し、正のExVolは持続的な弱気キャンドルを示します。
  3. エントリー条件
    • **ロング:**最後の終値が平均上フラクタルレベルを上回り、ExVolが負。アクティブなショートポジションは閉じられ、新しいロングポジションが開かれます。
    • **ショート:**最後の終値が平均下フラクタルレベルを下回り、ExVolが正。アクティブなロングポジションは閉じられ、新しいショートポジションが開かれます。
  4. リスクとエグジットルール
    • 固定のストップロスとテイクプロフィットターゲットはエントリー価格から設定可能なpip距離に配置されます。
    • オプションのトレーリングストップはトレードが少なくともtrailing stop + trailing step pips以上利益を得た後にのみ移動します。ストップは最小トレーリングステップを守りながら一定のトレーリング距離を維持するために上げ/下げされます。
    • ストップロスまたはテイクプロフィットに価格が触れると、ポジション全体が閉じられます。

パラメーター

名前 説明 デフォルト
Candle Type 戦略が使用するキャンドルデータタイプ/時間軸。 1時間時間軸
ExVol Period キャンドルボディの平均化(ExVol)に使用される閉じたキャンドルの数。 15
Stop Loss エントリー価格からのpip単位のストップロス距離。0に設定して無効化。 40
Take Profit エントリー価格からのpip単位のテイクプロフィット距離。0に設定して無効化。 100
Trailing Stop pip単位のトレーリングストップ距離。0に設定してトレーリングを無効化。 30
Trailing Step トレーリングストップを移動する前に必要な追加の価格移動(pips単位)。トレーリングが有効な場合は正でなければなりません。 5
Volume 基底クラスStrategyから継承したデフォルト注文量。 1

追加注記

  • トレーリングロジックはMetaTrader実装を反映しています:ポジションが少なくともTrailingStop + TrailingStep pipsの利益になるまでストップは調整されません。
  • ExVol計算はインストゥルメントのPriceStepに依存します。ステップが利用できない場合はデフォルト値0.0001が使用されます。
  • 戦略はBuyMarketSellMarketを通じて成行注文を発行し、新しいポジションを開く前に既存のポジションを自動的に反転させます。
  • 最初のフラクタルペアを形成するのに十分な履歴キャンドル(少なくとも5つの閉じたキャンドル)をデータフィードが提供していることを確認してください。
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>
/// Fractal breakout strategy that averages recent fractal levels and filters entries with ExVol momentum.
/// </summary>
public class ExFractalsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _exPeriod;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;

	private readonly Queue<decimal> _bodyQueue = new();

	private decimal _bodySum;

	private decimal _h1;
	private decimal _h2;
	private decimal _h3;
	private decimal _h4;
	private decimal _h5;

	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal _l4;
	private decimal _l5;

	private DateTimeOffset _t1;
	private DateTimeOffset _t2;
	private DateTimeOffset _t3;
	private DateTimeOffset _t4;
	private DateTimeOffset _t5;

	private decimal? _upFractal1;
	private decimal? _upFractal2;
	private DateTimeOffset? _upTime1;
	private DateTimeOffset? _upTime2;

	private decimal? _downFractal1;
	private decimal? _downFractal2;
	private DateTimeOffset? _downTime1;
	private DateTimeOffset? _downTime2;

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

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

	/// <summary>
	/// Lookback period for the ExVol average body indicator.
	/// </summary>
	public int ExPeriod
	{
		get => _exPeriod.Value;
		set => _exPeriod.Value = value;
	}

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

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

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

	/// <summary>
	/// Minimum price improvement before the trailing stop is moved.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="ExFractalsStrategy"/>.
	/// </summary>
	public ExFractalsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles", "General");

		_exPeriod = Param(nameof(ExPeriod), 15)
			.SetGreaterThanZero()
			.SetDisplay("ExVol Period", "Average body lookback", "Indicators");

		_stopLossPips = Param(nameof(StopLossPips), 40m)
			.SetDisplay("Stop Loss", "Stop-loss in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 100m)
			.SetDisplay("Take Profit", "Take-profit in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 30m)
			.SetDisplay("Trailing Stop", "Trailing distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetDisplay("Trailing Step", "Extra movement before trailing", "Risk");

		Volume = 1m;
	}

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

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

		_bodyQueue.Clear();
		_bodySum = 0m;

		_h1 = _h2 = _h3 = _h4 = _h5 = 0m;
		_l1 = _l2 = _l3 = _l4 = _l5 = 0m;

		_t1 = _t2 = _t3 = _t4 = _t5 = default;

		_upFractal1 = _upFractal2 = null;
		_downFractal1 = _downFractal2 = null;
		_upTime1 = _upTime2 = null;
		_downTime1 = _downTime2 = null;

		_longEntryPrice = _shortEntryPrice = null;
		_longStop = _longTake = null;
		_shortStop = _shortTake = null;
	}

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

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
		{
			throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");
		}

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Shift candle history buffers so the third slot represents the confirmed fractal bar.
		_h1 = _h2;
		_h2 = _h3;
		_h3 = _h4;
		_h4 = _h5;
		_h5 = candle.HighPrice;

		_l1 = _l2;
		_l2 = _l3;
		_l3 = _l4;
		_l4 = _l5;
		_l5 = candle.LowPrice;

		_t1 = _t2;
		_t2 = _t3;
		_t3 = _t4;
		_t4 = _t5;
		_t5 = candle.OpenTime;

		if (candle.State != CandleStates.Finished)
		{
			return;
		}

		// Detect new fractal values when enough candles are collected.
		if (_t3 != default)
		{
			if (_h3 > _h1 && _h3 > _h2 && _h3 > _h4 && _h3 > _h5)
			{
				RegisterUpFractal(_h3, _t3);
			}

			if (_l3 < _l1 && _l3 < _l2 && _l3 < _l4 && _l3 < _l5)
			{
				RegisterDownFractal(_l3, _t3);
			}
		}

		var step = Security?.PriceStep ?? 0.0001m;
		if (step <= 0m)
		{
			step = 0.0001m;
		}

		var body = (candle.ClosePrice - candle.OpenPrice) / step;
		_bodyQueue.Enqueue(body);
		_bodySum += body;
		if (_bodyQueue.Count > ExPeriod)
		{
			_bodySum -= _bodyQueue.Dequeue();
		}

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		decimal? exVol = _bodyQueue.Count >= ExPeriod ? _bodySum / ExPeriod : null;
		var upperLevel = GetUpperLevel();
		var lowerLevel = GetLowerLevel();
		var price = candle.ClosePrice;

		if (exVol is decimal exVolValue && upperLevel is decimal up && price > up && exVolValue < 0m && Position <= 0)
		{
			var volume = Volume + Math.Max(0m, -Position);
			if (volume > 0m)
			{
				BuyMarket(volume);
				InitializeLongTargets(price, step);
			}
		}
		else if (exVol is decimal exVolValue2 && lowerLevel is decimal down && price < down && exVolValue2 > 0m && Position >= 0)
		{
			var volume = Volume + Math.Max(0m, Position);
			if (volume > 0m)
			{
				SellMarket(volume);
				InitializeShortTargets(price, step);
			}
		}

		ManagePosition(price, step);
	}

	private void RegisterUpFractal(decimal price, DateTimeOffset time)
	{
		if (_upFractal1 is null)
		{
			_upFractal1 = price;
			_upTime1 = time;
			return;
		}

		if (_upTime1 == time)
		{
			_upFractal1 = price;
			return;
		}

		if (_upFractal2 is null)
		{
			_upFractal2 = price;
			_upTime2 = time;
			return;
		}

		if (_upTime2 == time)
		{
			_upFractal2 = price;
			return;
		}

		_upFractal1 = _upFractal2;
		_upTime1 = _upTime2;
		_upFractal2 = price;
		_upTime2 = time;
	}

	private void RegisterDownFractal(decimal price, DateTimeOffset time)
	{
		if (_downFractal1 is null)
		{
			_downFractal1 = price;
			_downTime1 = time;
			return;
		}

		if (_downTime1 == time)
		{
			_downFractal1 = price;
			return;
		}

		if (_downFractal2 is null)
		{
			_downFractal2 = price;
			_downTime2 = time;
			return;
		}

		if (_downTime2 == time)
		{
			_downFractal2 = price;
			return;
		}

		_downFractal1 = _downFractal2;
		_downTime1 = _downTime2;
		_downFractal2 = price;
		_downTime2 = time;
	}

	private decimal? GetUpperLevel()
	{
		if (_upFractal1 is decimal first && _upFractal2 is decimal second && _upTime1 != _upTime2)
		{
			return (first + second) / 2m;
		}

		return null;
	}

	private decimal? GetLowerLevel()
	{
		if (_downFractal1 is decimal first && _downFractal2 is decimal second && _downTime1 != _downTime2)
		{
			return (first + second) / 2m;
		}

		return null;
	}

	private void InitializeLongTargets(decimal price, decimal step)
	{
		_longEntryPrice = price;
		_longStop = StopLossPips > 0m ? price - StopLossPips * step : null;
		_longTake = TakeProfitPips > 0m ? price + TakeProfitPips * step : null;

		_shortEntryPrice = null;
		_shortStop = null;
		_shortTake = null;
	}

	private void InitializeShortTargets(decimal price, decimal step)
	{
		_shortEntryPrice = price;
		_shortStop = StopLossPips > 0m ? price + StopLossPips * step : null;
		_shortTake = TakeProfitPips > 0m ? price - TakeProfitPips * step : null;

		_longEntryPrice = null;
		_longStop = null;
		_longTake = null;
	}

	private void ManagePosition(decimal price, decimal step)
	{
		if (Position > 0)
		{
			ApplyLongTrailing(price, step);

			if (_longTake is decimal take && price >= take)
			{
				SellMarket(Position);
				ResetLongState();
				return;
			}

			if (_longStop is decimal stop && price <= stop)
			{
				SellMarket(Position);
				ResetLongState();
				return;
			}
		}
		else if (Position < 0)
		{
			ApplyShortTrailing(price, step);

			if (_shortTake is decimal take && price <= take)
			{
				BuyMarket(-Position);
				ResetShortState();
				return;
			}

			if (_shortStop is decimal stop && price >= stop)
			{
				BuyMarket(-Position);
				ResetShortState();
				return;
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}
	}

	private void ApplyLongTrailing(decimal price, decimal step)
	{
		if (_longEntryPrice is not decimal entry || TrailingStopPips <= 0m)
		{
			return;
		}

		var trailingDistance = TrailingStopPips * step;
		var trailingStep = TrailingStepPips * step;

		if (price - entry <= trailingDistance + trailingStep)
		{
			return;
		}

		var threshold = price - (trailingDistance + trailingStep);
		if (_longStop is null || _longStop < threshold)
		{
			_longStop = price - trailingDistance;
		}
	}

	private void ApplyShortTrailing(decimal price, decimal step)
	{
		if (_shortEntryPrice is not decimal entry || TrailingStopPips <= 0m)
		{
			return;
		}

		var trailingDistance = TrailingStopPips * step;
		var trailingStep = TrailingStepPips * step;

		if (entry - price <= trailingDistance + trailingStep)
		{
			return;
		}

		var threshold = price + trailingDistance + trailingStep;
		if (_shortStop is null || _shortStop > threshold)
		{
			_shortStop = price + trailingDistance;
		}
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStop = null;
		_shortTake = null;
	}
}