GitHub で見る

Fractured Fractals 戦略

クラシック MetaTrader エキスパートアドバイザー "Fractured Fractals" のポートです。この戦略は確認されたウィリアムズフラクタルを追跡し、新鮮なブレイクアウトレベルにストップ注文を置き、逆フラクタルで保護的なストップを追います。

詳細

  • ソース: MQL/20127/Fractured Fractals.mq5 から変換。
  • 市場レジーム: StockSharp でサポートされているあらゆる銘柄でのブレイクアウト継続。
  • 注文タイプ: エントリーにはストップ注文、決済には保護的なストップ注文を使用。
  • ポジションサイジング: リスクベース、MaximumRiskPercent と適応的な連敗ロジック DecreaseFactor で制御。
  • デフォルトパラメーター:
    • MaximumRiskPercent = 2%
    • DecreaseFactor = 10
    • ExpirationHours = 1 時間
    • CandleType = 1 時間の時間軸
  • コアインジケーター: オンザフライで計算されるネイティブの 5 バーウィリアムズフラクタル。
  • 戦略タイプ: 動的ストップ管理付きのロング/ショートブレイクアウト。

戦略ロジック

フラクタルシーケンスの追跡

  • MT5 の iFractals バッファを模倣するために、最後の 5 本のローソク足の高値と安値のキューを維持します。
  • 確認された各フラクタルは、最若、中間、古い 3 つのローリングスロットをシフトします。重複値は銘柄の価格ステップを許容度として使用して無視されます。
  • ロングシグナルは最新のアップフラクタルが中間フラクタルを超えることを要求します;ショートシグナルは最新のダウンフラクタルが前のものより低いことを要求します。

エントリー注文と有効期限

  • ロングポジションまたは保留中の buy stop が存在しない場合、戦略は最新のアップフラクタルに buy stop を置き、最新のダウンフラクタルにストップロスを設定します。
  • 対称的に、ショートエントリーは最新のダウンフラクタルに sell stop を置き、最新のアップフラクタルに保護的なストップを設定します。
  • 保留中の注文は ExpirationHours で定義された有効期限を継承します。ローソク足の時間が有効期限を超えるか、フラクタル階層がセットアップを無効にする場合(ロングの新しい低いアップフラクタル、ショートの高いダウンフラクタル)、注文はキャンセルされます。
  • ポジションが開かれると、ボットは逆方向の注文をキャンセルしてブックをクリーンに保ちます。

保護的なトレーリングストップ

  • 確認された各逆方向フラクタルは保護的なストップ注文を更新します:ロングポジションは最新のダウンフラクタルを追跡し、ショートポジションは最新のアップフラクタルを追跡します。
  • ストップは締まる方向のみ — 新しいレベルが置換が発生する前に既存の注文価格より改善しなければなりません。
  • ポジションがクローズされると、残りのストップ注文はすぐにキャンセルされます。

リスク管理と連敗制御

  • CalculateOrderVolume は MT5 のリスク計算を複製します:単位当たりのリスク = エントリー価格マイナスストップ価格(ショートの場合は逆)。
  • 目標の金銭的リスクは Portfolio.CurrentValue * MaximumRiskPercent / 100 に等しく、ポートフォリオの評価が利用できない場合は Volume プロパティにフォールバックします。
  • 結果のボリュームはロットサイズ、ボリュームステップ、最小ボリューム、および Security によって公開される最大ボリューム制約によって正規化されます。
  • 負けトレードの後、連敗カウンターが増加します;利益のあるトレードまたは均等なトレードはカウンターをリセットします。複数の連続した損失が発生した場合、サイズは losses / DecreaseFactor でスケールダウンされます。

取引結果の追跡

  • OnOwnTradeReceived はポジションサイクルが完了したとき、それが正、負、または均等に終わったかどうかを決定するためにフィルを集計します。
  • 連敗カウンターと最後の利益タイムスタンプは元のロジックを反映し、希望する場合にさらなる拡張(例:分析)を可能にします。

使用上の注意

  1. 戦略を任意の証券/ポートフォリオのペアに接続し、CandleType を希望の解像度に調整し、口座サイズに応じてリスクパラメーターを設定します。
  2. アダプター/ブローカーがストップ注文をサポートしていることを確認してください;そうでない場合は、保護注文を UpdateTrailingStops での手動決済に置き換えてください。
  3. 実装は完成したローソク足のみを処理するため、ローソク足の解像度より小さいイントラバーのスパイクはティックベースの MT5 テストと同様には注文をトリガーしません。
  4. C# ポートによって生成されたコメントメッセージを確認するためにログを有効にすることを検討してください。これは元のエキスパートのフィードバックを反映しています。
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;
	}
}