GitHub で見る

終値Fractals戦略

概要

この戦略は、Vladimir Karputov氏によるMetaTrader 5のエキスパートアドバイザー "Fractals at Close prices" をStockSharpに移植したものです。高値や安値ではなく終値のみを使ってビル・ウィリアムズ流のフラクタルを検出するため、5本連続の終値を分析します。直近2つの強気・弱気フラクタルを比較して現在のトレンドを判定します。最新の強気フラクタルが前の強気フラクタルより高い位置に現れると、ロングポジションを建てます。最新の弱気フラクタルが前の弱気フラクタルより低い位置に形成されると、ショートポジションを建てます。新しいトレードに入る前に必ず反対ポジションを閉じるため、戦略は常に最大1方向のみのポジションを保持します。

取引は、設定可能な開始時刻と終了時刻の間のみ許可されます。現在の時刻がこの窓の外にある場合、すべてのオープンポジションが即座に閉じられ、元のEAの動作を再現します。時間フィルターは、イントラデイ窓 (開始 < 終了)、深夜をまたぐ夜間セッション (開始 > 終了)、終日取引 (開始 == 終了) をサポートしています。

インジケーターロジック

  • 完了した各ローソク足が終値の5要素ローリングキューに追加されます。
  • 5つの値が揃うと、中間の終値 (2本前のローソク足) が評価されます:
    • 中間の終値が2本前の古い終値よりも厳密に大きく、2本の新しい終値以上である場合、強気フラクタルが記録されます。
    • 中間の終値が2本前の古い終値よりも厳密に小さく、2本の新しい終値以下である場合、弱気フラクタルが記録されます。
  • 最新と前の強気フラクタル、および最新と前の弱気フラクタルが後の比較のために保存されます。
  • 最新の強気フラクタルが前のものより高い場合に強気トレンドが検出されます。最新の弱気フラクタルが前のものより低い場合に弱気トレンドが検出されます。

トレーディングルール

  1. ロングエントリー
    • アクティブなショートポジションを成行で決済する。
    • ロングポジションが開いていない場合、強気フラクタルシーケンスを確認した終値で OrderVolume を成行で買う。
  2. ショートエントリー
    • アクティブなロングポジションを成行で決済する。
    • ショートポジションが開いていない場合、弱気フラクタルシーケンスが確認されたとき OrderVolume を成行で売る。
  3. セッション制御
    • シグナルを適用する前に、戦略は candle.OpenTime.Hour が取引窓内にあるかを確認します。そうでない場合、CloseAllPositions が呼ばれ、そのバーは無視されます。

リスク管理

  • ストップロスとテイクプロフィットの距離はpipsで表現されます。実装はMT5アプローチを再現します:シンボルのポイントは、インストゥルメントが3桁または5桁の小数点以下を持つ場合、10倍されます。結果のpip値は設定された距離で掛け算されます。
  • ポジションに入る際、初期のストップロスとテイクプロフィットレベルが内部に保存されます。StockSharpはMT5スタイルの保護注文を自動管理しないため、戦略は完了した各ローソク足を監視し、価格範囲が保存されたレベルに触れると成行で退場します。
  • トレーリングストップは元のEAルールに従います。利益が TrailingStop + TrailingStep を超えると、新しいストップが close ± TrailingStop として計算されます。トレーリングストップは、前のストップからの動きが少なくとも TrailingStep である場合のみ前進します。
  • 取引時間が終了すると、トレーリング状態に関わらずすべてのポジションが閉じられます。これは、許可されたセッション外でEAが CloseAllPositions を呼ぶことを再現します。

パラメーター

名前 説明 デフォルト
OrderVolume 各成行注文に使用するボリューム。 0.1
StartHour 取引がアクティブになる時間 (0-23)。EndHour と等しい場合、戦略は終日実行されます。 10
EndHour 取引が新しいシグナルの受け入れを停止する時間 (0-23)。 22
StopLossPips pipsで表現したストップロス距離。0 はストップを無効にします。 30
TakeProfitPips pipsで表現したテイクプロフィット距離。0 はテイクを無効にします。 50
TrailingStopPips pipsでの基本トレーリングストップ距離。0 はトレーリングを無効にします。 15
TrailingStepPips トレーリングストップが前進する前に必要な追加利益 (pips)。 5
CandleType 戦略がサブスクライブするローソク足データタイプ。デフォルトは1時間の時間軸ローソク足。 1 hour TimeFrame

実装メモ

  • 戦略はハイレベルAPIで SubscribeCandles を使用し、プロジェクトのガイドラインに従ってインジケーターを手動で登録しません。
  • 保護的な退場 (ストップ、テイクプロフィット、トレーリングストップ) は、StockSharpがMT5の保護注文を自動管理しないため、ローソク足が終了した後に成行注文を送信することで実行されます。
  • セッションフィルタリング、フラクタル検出、トレーリングロジックは、時間フィルターが満たされていない場合のすべてのポジションのクローズを含め、EAの構造に厳密に従います。
  • pip拡大縮小ロジックは、3桁または5桁の小数点以下を持つインストゥルメントでシンボルポイントを10倍することでMT5実装を反映し、等価な価格距離を確保します。

使用のヒント

  1. 戦略をシンボルにアタッチし、OrderVolume を好みのロットサイズに設定する。
  2. MetaTrader 5で使用した時間軸と一致するローソク足タイプを選ぶ (元のEAはどの時間軸でも機能します)。
  3. ブローカーのセッションまたは希望の時間に合わせて取引窓を調整する。
  4. インストゥルメントのボラティリティを反映するようにpipベースの距離を調整する。TrailingStepPips が大きいとトレーリング頻度が減り、小さい値だとストップが価格に更に近く追従します。
  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>
/// Strategy converted from the MT5 "Fractals at Close prices" expert advisor.
/// Detects bullish and bearish fractal sequences built on close prices and trades trend reversals.
/// Includes configurable trading hours and manual risk management with trailing stops.
/// </summary>
public class FractalsAtClosePricesStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeWindow = new(6);

	private decimal? _lastUpperFractal;
	private decimal? _previousUpperFractal;
	private decimal? _lastLowerFractal;
	private decimal? _previousLowerFractal;

	private decimal _pipValue;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingStopDistance;
	private decimal _trailingStepDistance;

	private decimal? _entryPrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Trading volume used for every market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Hour when the strategy can start opening positions.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour when the strategy stops opening positions.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

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

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

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

	/// <summary>
	/// Minimum price improvement required before moving the trailing stop.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

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

	/// <summary>
	/// Initializes <see cref="FractalsAtClosePricesStrategy"/> parameters.
	/// </summary>
	public FractalsAtClosePricesStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume used for entries", "General")
		;

		_startHour = Param(nameof(StartHour), 0)
		.SetRange(0, 23)
		.SetDisplay("Start Hour", "Hour when trading can start (0-23)", "Trading Hours");

		_endHour = Param(nameof(EndHour), 0)
		.SetRange(0, 23)
		.SetDisplay("End Hour", "Hour when trading stops (0-23)", "Trading Hours");

		_stopLossPips = Param(nameof(StopLossPips), 200)
		.SetRange(0, 1000)
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk Management")
		;

		_takeProfitPips = Param(nameof(TakeProfitPips), 400)
		.SetRange(0, 1000)
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk Management")
		;

		_trailingStopPips = Param(nameof(TrailingStopPips), 15)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Stop (pips)", "Base distance for the trailing stop", "Risk Management")
		;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
		.SetRange(0, 1000)
		.SetDisplay("Trailing Step (pips)", "Additional move required before trailing", "Risk Management")
		;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles processed by the strategy", "General");
	}

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

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

		_closeWindow.Clear();
		_lastUpperFractal = null;
		_previousUpperFractal = null;
		_lastLowerFractal = null;
		_previousLowerFractal = null;

		_pipValue = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingStopDistance = 0m;
		_trailingStepDistance = 0m;

		_entryPrice = null;
		ResetRiskLevels();
	}

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

		var priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;

		_pipValue = priceStep;
		if (decimals == 3 || decimals == 5)
		{
			// MT5 version multiplies point value by 10 when the symbol uses 3 or 5 decimals.
			_pipValue *= 10m;
		}

		_stopLossDistance = StopLossPips == 0 ? 0m : StopLossPips * _pipValue;
		_takeProfitDistance = TakeProfitPips == 0 ? 0m : TakeProfitPips * _pipValue;
		_trailingStopDistance = TrailingStopPips == 0 ? 0m : TrailingStopPips * _pipValue;
		_trailingStepDistance = TrailingStepPips == 0 ? 0m : TrailingStepPips * _pipValue;

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

		UpdateFractals(candle);

		if (!IsWithinTradingHours(candle.OpenTime))
		{
			CloseAllPositions();
			return;
		}

		ApplyRiskManagement(candle);

		// no bound indicators, skip IsFormedAndOnlineAndAllowTrading()

		ExecuteEntries(candle);
	}

	private void UpdateFractals(ICandleMessage candle)
	{
		// Maintain a rolling window of the five most recent closes.
		_closeWindow.Add(candle.ClosePrice);
		while (_closeWindow.Count > 5)
			_closeWindow.RemoveAt(0);

		if (_closeWindow.Count < 5)
		{
			return;
		}

		var window = _closeWindow;
		var center = window[2];

		var isUpper = center > window[0]
		&& center > window[1]
		&& center >= window[3]
		&& center >= window[4];

		if (isUpper)
		{
			_previousUpperFractal = _lastUpperFractal;
			_lastUpperFractal = center;
		}

		var isLower = center < window[0]
		&& center < window[1]
		&& center <= window[3]
		&& center <= window[4];

		if (isLower)
		{
			_previousLowerFractal = _lastLowerFractal;
			_lastLowerFractal = center;
		}
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.Hour;

		if (StartHour == EndHour)
		{
			// Trade the entire day when start and end hours are equal.
			return true;
		}

		if (StartHour < EndHour)
		{
			return hour >= StartHour && hour < EndHour;
		}

		return hour >= StartHour || hour < EndHour;
	}

	private void ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				// Close the long position if the stop-loss level is breached.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			if (_longTake is decimal take && candle.HighPrice >= take)
			{
				// Close the long position when the take-profit level is hit.
				SellMarket(Position);
				ResetRiskLevels();
				return;
			}

			UpdateLongTrailingStop(candle);
		}
		else if (Position < 0)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				// Cover the short position if the stop-loss level is breached.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				// Cover the short position when the take-profit level is hit.
				BuyMarket(Math.Abs(Position));
				ResetRiskLevels();
				return;
			}

			UpdateShortTrailingStop(candle);
		}
	}

	private void UpdateLongTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = candle.ClosePrice - entry;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice - _trailingStopDistance;
		if (_longStop is decimal currentStop && currentStop >= candle.ClosePrice - (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_longStop = targetStop;
	}

	private void UpdateShortTrailingStop(ICandleMessage candle)
	{
		if (_trailingStopDistance <= 0m || _entryPrice is not decimal entry)
		{
			return;
		}

		var profitDistance = entry - candle.ClosePrice;
		if (profitDistance <= _trailingStopDistance + _trailingStepDistance)
		{
			return;
		}

		var targetStop = candle.ClosePrice + _trailingStopDistance;
		if (_shortStop is decimal currentStop && currentStop <= candle.ClosePrice + (_trailingStopDistance + _trailingStepDistance))
		{
			// Skip updates until price improved by the trailing step.
			return;
		}

		_shortStop = targetStop;
	}

	private void ExecuteEntries(ICandleMessage candle)
	{
		// Only trade when flat to avoid too frequent reversals.
		if (Position != 0)
			return;

		var bullishTrend = _lastLowerFractal is decimal lastLow
		&& _previousLowerFractal is decimal prevLow
		&& prevLow < lastLow;

		if (bullishTrend && OrderVolume > 0m)
		{
			BuyMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_longStop = _stopLossDistance > 0m ? candle.ClosePrice - _stopLossDistance : null;
			_longTake = _takeProfitDistance > 0m ? candle.ClosePrice + _takeProfitDistance : null;
			_shortStop = null;
			_shortTake = null;
			return;
		}

		var bearishTrend = _lastUpperFractal is decimal lastUp
		&& _previousUpperFractal is decimal prevUp
		&& prevUp > lastUp;

		if (bearishTrend && OrderVolume > 0m)
		{
			SellMarket(OrderVolume);
			_entryPrice = candle.ClosePrice;
			_shortStop = _stopLossDistance > 0m ? candle.ClosePrice + _stopLossDistance : null;
			_shortTake = _takeProfitDistance > 0m ? candle.ClosePrice - _takeProfitDistance : null;
			_longStop = null;
			_longTake = null;
		}
	}

	private void CloseAllPositions()
	{
		if (Position > 0)
		{
			SellMarket(Position);
		}
		else if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
		}

		ResetRiskLevels();
	}

	private void CloseLongPosition()
	{
		if (Position > 0)
		{
			SellMarket(Position);
			ResetRiskLevels();
		}
	}

	private void CloseShortPosition()
	{
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			ResetRiskLevels();
		}
	}

	private void ResetRiskLevels()
	{
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		_entryPrice = null;
	}
}