GitHub で見る

クロスライン・トレーダー戦略

概要

この戦略は、ユーザー定義の合成ラインとの価格インタラクションに反応することで、オリジナルの MetaTrader エキスパート「Cross Line Trader」をエミュレートします。手動のチャートオブジェクトをリスンする代わりに、StockSharp バージョンは単一のパラメーターですべてのライン説明を受け取り、起動時にそれらを解析して完成したロウソク足を継続的に監視します。ロウソク足の始値がアクティブなラインを通過すると、戦略は対応する方向に成行注文を出し、そのラインを非アクティブ化して再び発火できないようにします。

取引ロジック

  1. 戦略は Candle Type パラメーターで選択されたロウソク足タイプをサブスクライブし、インtraバーノイズを避けるために Finished 状態のロウソク足のみを処理します。
  2. 合成ラインは Line Definitions パラメーターから作成されます。各ラインは自身の状態(アクティブ/期限切れ、処理済みバー数、ジオメトリ)を保持します。
  3. Trend または Horizontal ラインの場合、アルゴリズムはラインの価格軌跡に対して前のロウソク足の始値と次の始値を比較します:
    • 前の始値がラインの下にあり、現在の始値がその上に移動するとロングシグナルが発生します。
    • 前の始値がラインの上にあり、現在の始値がその下に移動するとショートシグナルが発生します。
  4. Vertical ラインはタイマートリガーとして機能します。設定されたバー数が経過すると、戦略は現在のロウソク足の始値で即座にポジションを開きます。
  5. 方向は Direction Mode に従って決定されます:
    • FromLabel は各ラインラベルを Buy LabelSell Label と比較します。
    • ForceBuyForceSell はラベルに関係なくすべてのラインを同じ方向として扱います。
  6. 各成功したトリガーは Trade Volume のボリュームで成行注文を送信し、アクティベーションを記録し、ラインを非アクティブとしてマークします。
  7. オプションのストップロスとテイクプロフィット距離は、最後のエントリー価格をロウソク足の高値と安値に対して評価することで、各新しいロウソク足に適用されます。

ライン定義フォーマット

Line Definitions 文字列はセミコロンを使ってエントリーを区切ります。各エントリーは次に従う必要があります:

Name|Type|Label|BasePrice|SlopePerBar|Length|Ray
  • Name – ログに表示される識別子。セミコロンを含まない任意の文字列。
  • TypeHorizontalTrend または Vertical(大文字小文字を区別しない)。
  • LabelDirection ModeFromLabel の場合に使用される自由テキスト。
  • BasePrice – 最初に処理されたロウソク足でのラインの初期価格。すべての非垂直ラインに必要(10進数、不変カルチャー)。
  • SlopePerBar – トレンドラインのロウソク足ごとの価格変化。水平ラインには 0 を使用。
  • Length – 意味はラインタイプによって異なります:
    • rayのないトレンドまたは水平ラインの場合、右アンカーが始点から何バー離れているかを定義します。このカウント後、ラインは自動的に期限切れになります。
    • rayラインの場合、ラインが無限に延びるため値は無視されます。
    • 垂直ラインの場合、発火前に待機するバー数を指定します。最小受け入れ値は 1
  • Raytrue はラインを右方向に無期限にアクティブに保ち、false は指定された長さに制限します。

例:

TrendLine|Trend|Buy|1.1000|0.0005|8|false;HorizontalSell|Horizontal|Sell|1.1050|0|0|true;VerticalImpulse|Vertical|Buy|0|0|1|false

この例では、上昇する買いトレンドライン、期限切れのない水平売りレベル、次のロウソク足の1回限りの垂直トリガーが作成されます。

パラメーター

  • Candle Type – 計算に使用される市場データタイプ。デフォルトは1分の時間軸。
  • Trade Volume – 新規エントリーの注文サイズ。正の値でなければなりません。
  • Direction Mode – エントリー側の選択方法を決定します(FromLabelForceBuyForceSell)。
  • Buy Label / Sell LabelDirection ModeFromLabel の場合にラインを識別するためのラベル値。
  • Line Definitions – 各合成ラインを説明する生の文字列(上記のフォーマットを参照)。
  • Stop Loss Offset – ロングとショートポジションの保護的終了のための価格単位での距離(0 で確認を無効化)。
  • Take Profit Offset – 利益目標の価格距離(0 で確認を無効化)。

リスク管理

戦略は別途ストップまたはテイクプロフィット注文を出しません。代わりに完成した各ロウソク足を監視します:

  • ロングポジションは、ロウソク足の安値が EntryPrice - StopLossOffset を下回るか、高値が EntryPrice + TakeProfitOffset を超えると決済されます。
  • ショートポジションは、ロウソク足の高値が EntryPrice + StopLossOffset を超えるか、安値が EntryPrice - TakeProfitOffset を下回ると決済されます。

両方のオフセットがゼロの場合、ポジションは反対のシグナルまたは手動介入によってのみ決済されます。

実装上の注意

  • ソースコードのすべてのコメントはプロジェクトガイドラインとの一貫性を保つために英語で書かれています。
  • 戦略は無効なライン定義を静かに無視します。トリガーを見逃さないようにフォーマットが正しいことを確認してください。
  • 戦略を再起動すると内部状態がクリアされるため、ラインカウンターとアクティベーションタイマーは最初に処理されたロウソク足から再開します。
  • アプローチはオリジナルEAと同様にロウソク足の始値に焦点を当てており、インtraバータッチには反応しません。

使用方法

  1. 取引銘柄と希望するロウソク足タイプを設定します。
  2. 取引したいすべての手動ラインを説明するように Line Definitions を調整します。
  3. ラベルを信頼するか片方向取引を強制するかを選択するために Direction Mode を設定します。
  4. 自動終了のためにオプションでストップロスとテイクプロフィットのオフセットを設定します。
  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;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that opens positions when price crosses predefined synthetic lines.
/// It replicates the idea of the original MQL Cross Line Trader by checking finished candles.
/// </summary>
public class CrossLineTraderStrategy : Strategy
{
	public enum LineDirectionModes
	{
		FromLabel,
		ForceBuy,
		ForceSell,
	}

	private enum LineTypes
	{
		Horizontal,
		Trend,
		Vertical,
	}

	private enum TradeDirections
	{
		Buy,
		Sell,
	}

	private sealed class LineState
	{
		public LineState(string name, string label, LineTypes type, decimal basePrice, decimal slopePerBar, int length, bool ray)
		{
			Name = name.IsEmptyOrWhiteSpace() ? type.ToString() : name;
			Label = label ?? string.Empty;
			Type = type;
			BasePrice = basePrice;
			SlopePerBar = slopePerBar;
			Length = Math.Max(0, length);
			Ray = ray;
			IsActive = true;
			StepsProcessed = 0;
		}

		public string Name { get; }

		public string Label { get; }

		public LineTypes Type { get; }

		public decimal BasePrice { get; }

		public decimal SlopePerBar { get; }

		public int Length { get; }

		public bool Ray { get; }

		public bool IsActive { get; set; }

		public int StepsProcessed { get; set; }

		public decimal GetPrice(int index)
		{
			if (Type == LineTypes.Vertical)
				return 0m;

			var clampedIndex = Math.Max(0, index);

			if (!Ray && Length > 0)
				clampedIndex = Math.Min(clampedIndex, Length);

			return BasePrice + SlopePerBar * clampedIndex;
		}
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<LineDirectionModes> _directionMode;
	private readonly StrategyParam<string> _buyLabel;
	private readonly StrategyParam<string> _sellLabel;
	private readonly StrategyParam<string> _lineDefinitions;
	private readonly StrategyParam<decimal> _stopLossOffset;
	private readonly StrategyParam<decimal> _takeProfitOffset;

	private List<LineState> _lines = new();
	private decimal? _previousOpen;
	private decimal _entryPrice;

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

	/// <summary>
	/// Volume sent with market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Defines how the strategy resolves trade direction for every line.
	/// </summary>
	public LineDirectionModes DirectionMode
	{
		get => _directionMode.Value;
		set => _directionMode.Value = value;
	}

	/// <summary>
	/// Text label that identifies buy lines when DirectionMode is FromLabel.
	/// </summary>
	public string BuyLabel
	{
		get => _buyLabel.Value;
		set => _buyLabel.Value = value;
	}

	/// <summary>
	/// Text label that identifies sell lines when DirectionMode is FromLabel.
	/// </summary>
	public string SellLabel
	{
		get => _sellLabel.Value;
		set => _sellLabel.Value = value;
	}

	/// <summary>
	/// Raw definition of synthetic lines. Each line uses the format:
	/// Name|Type|Label|BasePrice|SlopePerBar|Length|Ray
	/// </summary>
	public string LineDefinitions
	{
		get => _lineDefinitions.Value;
		set => _lineDefinitions.Value = value;
	}

	/// <summary>
	/// Protective stop distance in price units.
	/// </summary>
	public decimal StopLossOffset
	{
		get => _stopLossOffset.Value;
		set => _stopLossOffset.Value = value;
	}

	/// <summary>
	/// Protective take profit distance in price units.
	/// </summary>
	public decimal TakeProfitOffset
	{
		get => _takeProfitOffset.Value;
		set => _takeProfitOffset.Value = value;
	}

	/// <summary>
	/// Constructor that configures strategy parameters.
	/// </summary>
	public CrossLineTraderStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for intersections", "Data");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetDisplay("Trade Volume", "Order volume used for entries", "Trading")
			.SetGreaterThanZero();

		_directionMode = Param(nameof(DirectionMode), LineDirectionModes.FromLabel)
			.SetDisplay("Direction Mode", "How to pick trade direction for a line", "Trading");

		_buyLabel = Param(nameof(BuyLabel), "Buy")
			.SetDisplay("Buy Label", "Text that marks buy lines when mode uses labels", "Trading");

		_sellLabel = Param(nameof(SellLabel), "Sell")
			.SetDisplay("Sell Label", "Text that marks sell lines when mode uses labels", "Trading");

		_lineDefinitions = Param(nameof(LineDefinitions),
			"TrendLine|Trend|Buy|64000|50|20|false;HorizontalSell|Horizontal|Sell|68000|0|0|true;HorizontalBuy|Horizontal|Buy|62000|0|0|true")
			.SetDisplay("Line Definitions", "Encoded collection of synthetic lines", "Lines")
			;

		_stopLossOffset = Param(nameof(StopLossOffset), 0m)
			.SetDisplay("Stop Loss Offset", "Price distance for protective exit", "Risk")
			.SetNotNegative();

		_takeProfitOffset = Param(nameof(TakeProfitOffset), 0m)
			.SetDisplay("Take Profit Offset", "Price distance for profit taking", "Risk")
			.SetNotNegative();
	}

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

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

		_lines = new List<LineState>();
		_previousOpen = null;
		_entryPrice = 0m;
	}

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

		_lines = ParseLineDefinitions(LineDefinitions);
		_previousOpen = null;
		_entryPrice = 0m;

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (TradeVolume <= 0)
			return;

		var currentOpen = candle.OpenPrice;

		foreach (var line in _lines)
		{
			if (!line.IsActive)
				continue;

			var previousIndex = line.StepsProcessed;
			var currentIndex = previousIndex + 1;

			if (line.Type == LineTypes.Vertical)
			{
				if (currentIndex >= Math.Max(1, line.Length))
				{
					var direction = ResolveDirection(line);

					if (direction != null && TryOpenPosition(direction.Value, line, candle))
						line.IsActive = false;
				}

				line.StepsProcessed = currentIndex;
				continue;
			}

			if (!line.Ray && line.Length > 0 && previousIndex >= line.Length)
			{
				line.IsActive = false;
				continue;
			}

			if (_previousOpen is null)
			{
				line.StepsProcessed = currentIndex;
				continue;
			}

			var previousLinePrice = line.GetPrice(previousIndex);
			var currentLinePrice = line.GetPrice(currentIndex);
			var directionForLine = ResolveDirection(line);

			if (directionForLine is null)
			{
				line.StepsProcessed = currentIndex;
				continue;
			}

			var crossUp = line.Type == LineTypes.Horizontal
				? _previousOpen.Value <= previousLinePrice && currentOpen > previousLinePrice
				: _previousOpen.Value <= previousLinePrice && currentOpen > currentLinePrice;

			var crossDown = line.Type == LineTypes.Horizontal
				? _previousOpen.Value >= previousLinePrice && currentOpen < previousLinePrice
				: _previousOpen.Value >= previousLinePrice && currentOpen < currentLinePrice;

			if (crossUp && directionForLine == TradeDirections.Buy && Position <= 0)
			{
				if (TryOpenPosition(TradeDirections.Buy, line, candle))
					line.IsActive = false;
			}
			else if (crossDown && directionForLine == TradeDirections.Sell && Position >= 0)
			{
				if (TryOpenPosition(TradeDirections.Sell, line, candle))
					line.IsActive = false;
			}

			line.StepsProcessed = currentIndex;

			if (!line.Ray && line.Length > 0 && currentIndex >= line.Length)
				line.IsActive = false;
		}

		ManageProtectiveExits(candle);

		_previousOpen = currentOpen;
	}

	private TradeDirections? ResolveDirection(LineState line)
	{
		switch (DirectionMode)
		{
			case LineDirectionModes.ForceBuy:
				return TradeDirections.Buy;
			case LineDirectionModes.ForceSell:
				return TradeDirections.Sell;
			case LineDirectionModes.FromLabel:
				if (!BuyLabel.IsEmptyOrWhiteSpace() &&
					line.Label.EqualsIgnoreCase(BuyLabel))
					return TradeDirections.Buy;

				if (!SellLabel.IsEmptyOrWhiteSpace() &&
					line.Label.EqualsIgnoreCase(SellLabel))
					return TradeDirections.Sell;
				break;
		}

		return null;
	}

	private bool TryOpenPosition(TradeDirections direction, LineState line, ICandleMessage candle)
	{
		if (direction == TradeDirections.Buy)
		{
			if (Position > 0)
				return false;

			BuyMarket(TradeVolume);
			_entryPrice = candle.OpenPrice;
			// BUY triggered
		}
		else
		{
			if (Position < 0)
				return false;

			SellMarket(TradeVolume);
			_entryPrice = candle.OpenPrice;
			// SELL triggered
		}

		return true;
	}

	private void ManageProtectiveExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var volume = Math.Abs(Position);

			if (StopLossOffset > 0m && candle.LowPrice <= _entryPrice - StopLossOffset)
			{
				SellMarket(volume);
				return;
			}

			if (TakeProfitOffset > 0m && candle.HighPrice >= _entryPrice + TakeProfitOffset)
			{
				SellMarket(volume);
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);

			if (StopLossOffset > 0m && candle.HighPrice >= _entryPrice + StopLossOffset)
			{
				BuyMarket(volume);
				return;
			}

			if (TakeProfitOffset > 0m && candle.LowPrice <= _entryPrice - TakeProfitOffset)
			{
				BuyMarket(volume);
			}
		}
	}

	private List<LineState> ParseLineDefinitions(string raw)
	{
		var result = new List<LineState>();

		if (raw.IsEmptyOrWhiteSpace())
			return result;

		var entries = raw.Split(new[] { '\n', '\r', ';' }, StringSplitOptions.RemoveEmptyEntries);

		foreach (var entry in entries)
		{
			var parts = entry.Split('|');

			if (parts.Length < 7)
				continue;

			var name = parts[0].Trim();
			var typeText = parts[1].Trim();
			var label = parts[2].Trim();
			var basePriceText = parts[3].Trim();
			var slopeText = parts[4].Trim();
			var lengthText = parts[5].Trim();
			var rayText = parts[6].Trim();

			if (!Enum.TryParse<LineTypes>(typeText, true, out var type))
				continue;

			if (!decimal.TryParse(basePriceText, NumberStyles.Number, CultureInfo.InvariantCulture, out var basePrice))
				continue;

			if (!decimal.TryParse(slopeText, NumberStyles.Number, CultureInfo.InvariantCulture, out var slope))
				slope = 0m;

			if (!int.TryParse(lengthText, NumberStyles.Integer, CultureInfo.InvariantCulture, out var length))
				length = 0;

			if (!bool.TryParse(rayText, out var ray))
				ray = false;

			if (type == LineTypes.Vertical && length <= 0)
				length = 1;

			result.Add(new LineState(name, label, type, basePrice, slope, length, ray));
		}

		return result;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position == 0)
		{
			_entryPrice = 0m;
			return;
		}

		_entryPrice = trade.Trade.Price;
	}
}