GitHub で見る

SendClose戦略

概要

SendCloseはフラクタルベースのブレイクアウト戦略で、元のMT5エキスパートアドバイザーの動作を再現します。アルゴリズムは交互のフラクタルピボットをリンクすることで動的なサポートとレジスタンスラインを継続的に構築し、価格がそれらの予測レベルに再び訪れた瞬間に反応します。StockSharpのポートはコアメカニクスを維持しています: トレンドラインは交互の上/下フラクタルシーケンスから生成され、ブレイクアウトは成行エントリーをトリガーし、別個のオフセットラインがポジション清算を強制するために使用されます。

フラクタル検出ワークフロー

  1. 5本ローソク足ウィンドウ – 戦略は最後の5本の完了したローソク足のローリングバッファを保持します。ウィンドウがいっぱいになると、中央のローソク足を2本の古い隣と2本の新しい隣に対して評価します。
  2. 上方フラクタルルール – 中央のローソク足の高値が2本の新しいローソク足の高値より大きく、2本の古いローソク足の高値より厳密に大きい場合、上方フラクタルを形成します。これはMT5の iFractals ロジックと一致します(新しい側では>=、古い側では>)。
  3. 下方フラクタルルール – 同様に、中央のローソク足の安値が新しいローソク足と比較して以下で、2本の古いローソク足より厳密に低い場合、下方フラクタルです。
  4. フラクタルキュー – 新たに確認された各フラクタルは、最新から最古の順に並んだ6要素のFIFOキューに押し込まれます。このキューは後で必要な交互パターンを見つけるためにスキャンされます。

トレンドライン構築

  • 売りライン – アルゴリズムは最新の 上方フラクタル → 下方フラクタル → 上方フラクタル シーケンスを探します。ラインは最初と最後の上方フラクタルを通って引かれ、スイングローで分離された2つのスイングハイを効果的に接続します。
  • 買いライン – 対称的に、下方フラクタル → 上方フラクタル → 下方フラクタル チェーンを検索し、周囲のスイングローを接続します。
  • プロジェクション – 保存されたエンドポイント(時間と価格)は、後の任意のタイムスタンプのライン値を補間または外挿するために使用されます。市場が現在のローソク足クローズで予測に達すると、取引決定が行われます。
  • クローズライン – 売りラインを上方に、買いラインを LineOffsetSteps * PriceStep 分下方にシフトすることで2つの補助レベルが計算されます。これらは元のClose1/Close2ラインと同様に強制エグジットトリガーとして機能します。

取引ロジック

  • エントリー条件
    • 価格が売りラインに触れ、競合するロングポジションがない場合に売ります。既存のショートポジションは MaxPositions 制限に達するまで増やすことができます。
    • 価格が買いラインに触れ、競合するショートポジションがない場合に買います。既存のロングポジションは同じ制限まで増やすことができます。
  • エグジット条件
    • クローズラインに触れる価格はオープンポジションを即座にクローズし、Close1/Close2に触れると完全なエグジットが発行されるMT5の動作をエミュレートします。
    • エントリーシグナルは新しい注文を配置する前に反対のポジションをフラット化しようとし、StockSharp内のヘッジングからネッティングへの適応を反映します。
  • タッチ検出 – MT5のティック精度はローソク足データで近似されます。レベルはローソク足の高値と安値の間にある場合に「タッチされた」とみなされます。

パラメーター

名前 説明
EnableSellLine 上部(売り)フラクタルラインに基づく注文を有効または無効にします。
EnableBuyLine 下部(買い)フラクタルラインに基づく注文を有効または無効にします。
EnableCloseSellLine 価格が売りラインプラスオフセットを上回ったときにポジションをクローズするClose1レベルを切り替えます。
EnableCloseBuyLine 価格が買いラインマイナスオフセットを下回ったときにポジションをクローズするClose2レベルを切り替えます。
MaxPositions 一方向でオープンしたままにできる最大ロット数。この上限を超える追加エントリーは無視されます。
OrderVolume 各成行注文のボリューム。値は銘柄の契約サイズと一致する必要があります。
LineOffsetSteps Close1/Close2レベルを計算する際に使用する価格ステップ単位のオフセット。デフォルトの15はMT5からの 15*Point() シフトを複製します。
CandleType 分析に使用するローソク足シリーズ。取引しようとしているチャートに一致する時間軸を選択してください(例: M15, H1)。

実装ノート

  • 戦略はフラクタルを評価する前に確認されたMT5バーに依存していた元のEAを尊重するために、完了したローソク足で実行されます。
  • MT5のティック精度(bid/askとの同等性)はローソク足の範囲で近似されます。より高い精度が必要な場合は、ローソク足の代わりにティックデータを提供してください。
  • MaxPositions パラメーターはStockSharpのネットポジションで動作します。したがってネッティング口座に適しています。ヘッジング口座は MaxPositions を増やすことでスケーリングをシミュレートできます。
  • クローズラインはエントリーの前に評価されます。同じローソク足でエグジットとエントリーの両方がトリガーされる場合、エグジットが優先され、競合する注文を防ぎます。

使用ガイドライン

  1. StockSharpターミナルで目的のシンボルと時間軸を設定し、銘柄が PriceStep 情報を提供していることを確認してください。オフセットロジックはこれに依存します。
  2. 分析したい時間軸に合わせて CandleType を調整してください。デフォルトは30分で、ノイズと応答性のバランスを提供します。
  3. OrderVolume を1トレードあたり送信したいポジションサイズに設定してください。先物の場合は契約数を、FX CFDの場合はロットサイズを使用してください。
  4. LineOffsetSteps を銘柄のボラティリティに合わせて調整してください。オフセットが大きいほど、Close1/Close2のエグジットをトリガーするためにより強い動きが必要です。
  5. MaxPositions を増やすときはオープンロット数を監視してください。戦略はこの上限を超えませんが、トレンド市場ではポジションをピラミッドにし続ける可能性があります。

MT5バージョンとの違い

  • StockSharpはネットポジションで動作するため、コードは同時買い/売りチケットを維持する代わりに、新しいトレードを開く前に反対のポジションをフラット化します。
  • チャートオブジェクトは自動的に描画されません。チャート上の可視化が必要な場合は、チャートモジュールを接続し、生成されたライン値を手動でプロットしてください。
  • ローソク足ベースのタッチ検出は、MT5のティックチェックよりわずかに遅く発火する場合があります。特に広いローソク足のある速い市場で顕著です。

リスク管理

戦略は組み込みストップロスなしで成行注文を出します。常に資本ストップ、取引時間フィルター、手動監視などの外部リスクコントロールで補完してください。ライブ展開前にターゲット銘柄と時間軸で広範なバックテストを実施してください。

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>
/// SendClose strategy replicates fractal breakout lines with close-based exits.
/// This class recreates the MT5 SendClose expert using StockSharp high level API.
/// </summary>
public class SendCloseStrategy : Strategy
{
	private enum FractalTypes
	{
		Up,
		Down
	}

	private readonly struct FractalPoint
	{
		public FractalPoint(FractalTypes type, DateTimeOffset time, decimal price)
		{
			Type = type;
			Time = time;
			Price = price;
		}

		public FractalTypes Type { get; }
		public DateTimeOffset Time { get; }
		public decimal Price { get; }
	}

	private readonly struct FractalLine
	{
		public FractalLine(FractalPoint recent, FractalPoint older)
		{
			if (recent.Time < older.Time)
			{
				Recent = older;
				Older = recent;
			}
			else
			{
				Recent = recent;
				Older = older;
			}
		}

		public FractalPoint Recent { get; }
		public FractalPoint Older { get; }

		public decimal GetPrice(DateTimeOffset time)
		{
			var totalSeconds = (decimal)(Recent.Time - Older.Time).TotalSeconds;
			if (totalSeconds == 0m)
				return Recent.Price;

			var offsetSeconds = (decimal)(time - Older.Time).TotalSeconds;
			return Older.Price + (Recent.Price - Older.Price) * (offsetSeconds / totalSeconds);
		}
	}

	private readonly StrategyParam<bool> _enableSellLine;
	private readonly StrategyParam<bool> _enableBuyLine;
	private readonly StrategyParam<bool> _enableCloseSellLine;
	private readonly StrategyParam<bool> _enableCloseBuyLine;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _lineOffsetSteps;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _h0;
	private decimal _h1;
	private decimal _h2;
	private decimal _h3;
	private decimal _h4;

	private decimal _l0;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal _l4;

	private DateTimeOffset _t0;
	private DateTimeOffset _t1;
	private DateTimeOffset _t2;
	private DateTimeOffset _t3;
	private DateTimeOffset _t4;

	private int _bufferCount;

	private FractalPoint? _fractal0;
	private FractalPoint? _fractal1;
	private FractalPoint? _fractal2;
	private FractalPoint? _fractal3;
	private FractalPoint? _fractal4;
	private FractalPoint? _fractal5;

	private FractalLine? _sellLine;
	private FractalLine? _buyLine;

	/// <summary>
	/// Enable sell breakout line.
	/// </summary>
	public bool EnableSellLine
	{
		get => _enableSellLine.Value;
		set => _enableSellLine.Value = value;
	}

	/// <summary>
	/// Enable buy breakout line.
	/// </summary>
	public bool EnableBuyLine
	{
		get => _enableBuyLine.Value;
		set => _enableBuyLine.Value = value;
	}

	/// <summary>
	/// Enable upper close line (based on sell trend line).
	/// </summary>
	public bool EnableCloseSellLine
	{
		get => _enableCloseSellLine.Value;
		set => _enableCloseSellLine.Value = value;
	}

	/// <summary>
	/// Enable lower close line (based on buy trend line).
	/// </summary>
	public bool EnableCloseBuyLine
	{
		get => _enableCloseBuyLine.Value;
		set => _enableCloseBuyLine.Value = value;
	}

	/// <summary>
	/// Maximum number of lots that can remain open.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Order volume per entry.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Offset in price steps for close lines.
	/// </summary>
	public int LineOffsetSteps
	{
		get => _lineOffsetSteps.Value;
		set => _lineOffsetSteps.Value = value;
	}

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

	/// <summary>
	/// Initialize <see cref="SendCloseStrategy"/>.
	/// </summary>
	public SendCloseStrategy()
	{
		_enableSellLine = Param(nameof(EnableSellLine), true)
			.SetDisplay("Sell Line", "Enable sell fractal breakout line", "General");

		_enableBuyLine = Param(nameof(EnableBuyLine), true)
			.SetDisplay("Buy Line", "Enable buy fractal breakout line", "General");

		_enableCloseSellLine = Param(nameof(EnableCloseSellLine), true)
			.SetDisplay("Close Line 1", "Enable closing line above sell trend", "General");

		_enableCloseBuyLine = Param(nameof(EnableCloseBuyLine), true)
			.SetDisplay("Close Line 2", "Enable closing line below buy trend", "General");

		_maxPositions = Param(nameof(MaxPositions), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum number of simultaneous lots", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 0.10m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume per signal", "Risk");

		_lineOffsetSteps = Param(nameof(LineOffsetSteps), 60)
			.SetGreaterThanZero()
			.SetDisplay("Offset Steps", "Offset in price steps for close levels", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");
	}

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

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

		// Clear buffers that hold recent highs, lows, and times.
		_h0 = _h1 = _h2 = _h3 = _h4 = 0m;
		_l0 = _l1 = _l2 = _l3 = _l4 = 0m;
		_t0 = _t1 = _t2 = _t3 = _t4 = default;
		_bufferCount = 0;

		// Reset stored fractal points and active lines.
		_fractal0 = _fractal1 = _fractal2 = _fractal3 = _fractal4 = _fractal5 = null;
		_sellLine = null;
		_buyLine = null;
	}

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

		// Subscribe to candle data and process each completed candle.
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Work only with completed candles to match the MT5 expert behaviour.
		if (candle.State != CandleStates.Finished)
			return;

		// Update internal buffers and detect new fractal points.
		UpdateBuffers(candle);
		UpdateFractalLines();

		// Ensure trading is allowed before evaluating signals.
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var offset = GetOffset();
		var shouldClose = false;

		// Check closing logic derived from the upper fractal trend line.
		if (EnableCloseSellLine && _sellLine is { } sellLine)
		{
			var closePrice = GetLinePrice(sellLine, candle.CloseTime) + offset;
			if (IsTouched(closePrice, candle))
				shouldClose = true;
		}

		// Check closing logic derived from the lower fractal trend line.
		if (EnableCloseBuyLine && _buyLine is { } buyLine)
		{
			var closePrice = GetLinePrice(buyLine, candle.CloseTime) - offset;
			if (IsTouched(closePrice, candle))
				shouldClose = true;
		}

		// Close any open position if price reached one of the close lines.
		if (shouldClose && Position != 0m)
		{
			if (Position > 0) SellMarket(); else BuyMarket();
			return;
		}

		// Entry logic for sell breakout.
		if (EnableSellLine && _sellLine is { } sellEntryLine)
		{
			var sellPrice = GetLinePrice(sellEntryLine, candle.CloseTime);
			if (IsTouched(sellPrice, candle))
			{
				if (Position > 0m)
				{
					// Flatten long positions before attempting to go short.
					SellMarket();
				}
				else if (CanIncreaseShort())
				{
					SellMarket(OrderVolume);
				}
			}
		}

		// Entry logic for buy breakout.
		if (EnableBuyLine && _buyLine is { } buyEntryLine)
		{
			var buyPrice = GetLinePrice(buyEntryLine, candle.CloseTime);
			if (IsTouched(buyPrice, candle))
			{
				if (Position < 0m)
				{
					// Flatten short positions before attempting to go long.
					BuyMarket();
				}
				else if (CanIncreaseLong())
				{
					BuyMarket(OrderVolume);
				}
			}
		}
	}

	private void UpdateBuffers(ICandleMessage candle)
	{
		// Shift buffers to keep the latest five candles for fractal detection.
		_h4 = _h3;
		_h3 = _h2;
		_h2 = _h1;
		_h1 = _h0;
		_h0 = candle.HighPrice;

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

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

		if (_bufferCount < 5)
		{
			_bufferCount++;
			return;
		}

		// Identify new fractal points once enough candles are available.
		if (IsUpFractal())
			RegisterFractal(new FractalPoint(FractalTypes.Up, _t2, _h2));

		if (IsDownFractal())
			RegisterFractal(new FractalPoint(FractalTypes.Down, _t2, _l2));
	}

	private void UpdateFractalLines()
	{
		// Build the sell line using the most recent up-down-up pattern.
		if (TryBuildLine(FractalTypes.Up, out var sellLine))
			_sellLine = sellLine;

		// Build the buy line using the most recent down-up-down pattern.
		if (TryBuildLine(FractalTypes.Down, out var buyLine))
			_buyLine = buyLine;
	}

	private bool IsUpFractal()
	{
		return _h2 >= _h3 && _h2 > _h4 && _h2 >= _h1 && _h2 > _h0;
	}

	private bool IsDownFractal()
	{
		return _l2 <= _l3 && _l2 < _l4 && _l2 <= _l1 && _l2 < _l0;
	}

	private void RegisterFractal(FractalPoint point)
	{
		// Skip duplicates that can appear on flat sequences.
		if (_fractal0 is { } latest && latest.Time == point.Time && latest.Type == point.Type)
			return;

		_fractal5 = _fractal4;
		_fractal4 = _fractal3;
		_fractal3 = _fractal2;
		_fractal2 = _fractal1;
		_fractal1 = _fractal0;
		_fractal0 = point;
	}

	private bool TryBuildLine(FractalTypes target, out FractalLine line)
	{
		line = default;
		FractalPoint? latest = null;
		FractalPoint? middle = null;
		FractalPoint? oldest = null;

		foreach (var item in EnumerateFractals())
		{
			if (item is not { } point)
				continue;

			if (latest is null)
			{
				if (point.Type == target)
					latest = point;
				continue;
			}

			if (middle is null)
			{
				if (point.Type != target)
					middle = point;
				continue;
			}

			if (point.Type == target)
			{
				oldest = point;
				break;
			}
		}

		if (latest is not { } latestPoint || middle is null || oldest is not { } oldestPoint)
			return false;

		if (latestPoint.Time == oldestPoint.Time)
			return false;

		line = new FractalLine(latestPoint, oldestPoint);
		return true;
	}

	private IEnumerable<FractalPoint?> EnumerateFractals()
	{
		yield return _fractal0;
		yield return _fractal1;
		yield return _fractal2;
		yield return _fractal3;
		yield return _fractal4;
		yield return _fractal5;
	}

	private bool CanIncreaseShort()
	{
		if (OrderVolume <= 0m || MaxPositions <= 0)
			return false;

		var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
		return lots < MaxPositions;
	}

	private bool CanIncreaseLong()
	{
		if (OrderVolume <= 0m || MaxPositions <= 0)
			return false;

		var lots = OrderVolume == 0m ? 0m : Math.Abs(Position) / OrderVolume;
		return lots < MaxPositions;
	}

	private decimal GetOffset()
	{
		var step = Security?.PriceStep ?? 1m;
		return step * LineOffsetSteps;
	}

	private static bool IsTouched(decimal price, ICandleMessage candle)
	{
		return price <= candle.HighPrice && price >= candle.LowPrice;
	}

	private static decimal GetLinePrice(FractalLine line, DateTimeOffset time)
	{
		return line.GetPrice(time);
	}
}