Auf GitHub ansehen

Cross Line Trader Strategie

Überblick

Die Strategie emuliert den originalen MetaTrader-Expert "Cross Line Trader", indem sie auf Preisinteraktionen mit benutzerdefinierten synthetischen Linien reagiert. Anstatt auf manuelle Chart-Objekte zu hören, empfängt die StockSharp-Version alle Linienbeschreibungen über einen einzigen Parameter, analysiert diese beim Start und überwacht kontinuierlich abgeschlossene Kerzen. Wenn sich ein Kerzenöffnungspreis durch eine aktive Linie bewegt, platziert die Strategie eine Marktorder in der entsprechenden Richtung und deaktiviert diese Linie, damit sie nicht erneut ausgelöst werden kann.

Handelslogik

  1. Die Strategie abonniert den im Parameter Candle Type ausgewählten Kerzentyp und verarbeitet nur Kerzen im Finished-Zustand, um Intrabar-Rauschen zu vermeiden.
  2. Synthetische Linien werden aus dem Parameter Line Definitions erstellt. Jede Linie hält ihren eigenen Zustand (aktiv/abgelaufen, Anzahl der verarbeiteten Bars und Geometrie).
  3. Bei Trend- oder Horizontal-Linien vergleicht der Algorithmus die vorherige Kerzenöffnung mit der nächsten relativ zur Preisverlaufsbahn der Linie:
    • Ein Long-Signal tritt auf, wenn die vorherige Öffnung unter der Linie liegt und die aktuelle Öffnung darüber wechselt.
    • Ein Short-Signal tritt auf, wenn die vorherige Öffnung über der Linie liegt und die aktuelle Öffnung darunter wechselt.
  4. Vertical-Linien funktionieren wie zeitgesteuerte Auslöser. Sobald die konfigurierte Anzahl von Bars abgelaufen ist, eröffnet die Strategie sofort eine Position zum aktuellen Kerzenöffnungspreis.
  5. Die Richtung wird gemäß Direction Mode aufgelöst:
    • FromLabel vergleicht jede Linienbezeichnung mit Buy Label und Sell Label.
    • ForceBuy und ForceSell behandeln alle Linien als dieselbe Richtung unabhängig von Bezeichnungen.
  6. Jeder erfolgreiche Auslöser sendet eine Marktorder mit dem Volumen aus Trade Volume, protokolliert die Aktivierung und markiert die Linie als inaktiv.
  7. Optionale Stop-Loss- und Take-Profit-Abstände werden auf jeder neuen Kerze angewendet, indem der letzte Einstiegspreis gegen Kerzenhochs und -tiefs bewertet wird.

Format der Liniendefinition

Der Line Definitions-String verwendet Semikolons zur Trennung von Einträgen. Jeder Eintrag muss folgendes Format haben:

Name|Type|Label|BasePrice|SlopePerBar|Length|Ray
  • Name – In Protokollen angezeigter Bezeichner. Beliebige Zeichenkette ohne Semikolons.
  • TypeHorizontal, Trend oder Vertical (Groß-/Kleinschreibung ignoriert).
  • Label – Freitext, der verwendet wird, wenn Direction Mode FromLabel ist.
  • BasePrice – Anfangspreis der Linie bei der ersten verarbeiteten Kerze. Erforderlich für jede nicht-vertikale Linie (dezimal, invariante Kultur).
  • SlopePerBar – Preisänderung pro Kerze für eine Trendlinie. Verwenden Sie 0 für horizontale Linien.
  • Length – Bedeutung hängt vom Linientyp ab:
    • Bei Trend- oder horizontalen Linien ohne Ray definiert es, wie viele Bars der rechte Anker vom Start entfernt ist. Nach diesem Zähler läuft die Linie automatisch ab.
    • Bei Ray-Linien wird der Wert ignoriert, da die Linie unbegrenzt weitergeht.
    • Bei vertikalen Linien gibt es an, wie viele Bars gewartet werden soll, bevor ausgelöst wird. Der minimale akzeptierte Wert ist 1.
  • Raytrue hält die Linie unbegrenzt nach rechts aktiv, false beschränkt sie auf die angegebene Länge.

Beispiel:

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

Das Beispiel erstellt eine steigende Kauf-Trendlinie, ein horizontales Verkaufsniveau, das nie abläuft, und einen einmaligen vertikalen Auslöser für die nächste Kerze.

Parameter

  • Candle Type – Marktdatentyp für Berechnungen. Standard: 1-Minuten-Zeitrahmen.
  • Trade Volume – Ordergröße für neue Einstiege. Muss positiv sein.
  • Direction Mode – Bestimmt, wie die Einstiegsseite ausgewählt wird (FromLabel, ForceBuy, ForceSell).
  • Buy Label / Sell Label – Bezeichnungswerte zur Identifizierung von Linien, wenn Direction Mode FromLabel ist.
  • Line Definitions – Roher String, der jede synthetische Linie beschreibt (siehe Format oben).
  • Stop Loss Offset – Abstand in Preiseinheiten für Schutzausstiege bei Long- und Short-Positionen (0 deaktiviert die Prüfung).
  • Take Profit Offset – Preisabstand für Gewinnziele (0 deaktiviert die Prüfung).

Risikomanagement

Die Strategie platziert keine separaten Stop- oder Take-Profit-Orders. Stattdessen überwacht sie jede abgeschlossene Kerze:

  • Long-Positionen werden geschlossen, wenn das Kerzentief EntryPrice - StopLossOffset unterschreitet oder das Hoch EntryPrice + TakeProfitOffset überschreitet.
  • Short-Positionen werden geschlossen, wenn das Kerzenhoch EntryPrice + StopLossOffset überschreitet oder das Tief unter EntryPrice - TakeProfitOffset fällt.

Wenn beide Offsets null sind, wird die Position nur durch das entgegengesetzte Signal oder manuelle Intervention geschlossen.

Implementierungshinweise

  • Alle Kommentare im Quellcode sind auf Englisch, um die Konsistenz mit den Projektrichtlinien zu wahren.
  • Die Strategie ignoriert ungültige Liniendefinitionen stillschweigend; stellen Sie sicher, dass das Format korrekt ist, um fehlende Auslöser zu vermeiden.
  • Das Neustarten der Strategie löscht den internen Zustand, sodass Linienzähler und Aktivierungstimer bei der ersten verarbeiteten Kerze neu beginnen.
  • Der Ansatz konzentriert sich auf Kerzenöffnungspreise, genau wie der ursprüngliche EA, und reagiert nicht auf Intrabar-Berührungen.

Verwendung

  1. Das Handelsinstrument und den gewünschten Kerzentyp konfigurieren.
  2. Line Definitions anpassen, um jede manuelle Linie zu beschreiben, gegen die gehandelt werden soll.
  3. Direction Mode so einstellen, dass entweder Bezeichnungen verwendet werden oder einseitiger Handel erzwungen wird.
  4. Optional Stop-Loss- und Take-Profit-Offsets für automatische Ausstiege festlegen.
  5. Die Strategie starten und die Protokolle überwachen: Jede ausgelöste Linie wird zusammen mit ihrer Richtung und dem Aktivierungspreis gemeldet.
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;
	}
}