Ver en GitHub

Estrategia Operador de Línea Cruzada

Descripción general

La estrategia emula el experto original de MetaTrader "Cross Line Trader" reaccionando a las interacciones de precio con líneas sintéticas definidas por el usuario. En lugar de escuchar objetos de gráfico manuales, la versión StockSharp recibe todas las descripciones de líneas a través de un único parámetro, las analiza al inicio y monitorea continuamente las velas terminadas. Cuando el apertura de una vela se mueve a través de una línea activa, la estrategia coloca una orden de mercado en la dirección correspondiente y desactiva esa línea para que no pueda volver a dispararse.

Lógica de trading

  1. La estrategia se suscribe al tipo de vela seleccionado en el parámetro Candle Type y solo procesa velas en estado Finished para evitar el ruido intrabarra.
  2. Las líneas sintéticas se crean a partir del parámetro Line Definitions. Cada línea mantiene su propio estado (activa/expirada, número de barras procesadas y geometría).
  3. Para líneas Trend u Horizontal, el algoritmo compara la apertura de la vela anterior con la siguiente relativa a la trayectoria de precio de la línea:
    • Una señal larga ocurre cuando la apertura anterior está por debajo de la línea y la apertura actual se mueve por encima.
    • Una señal corta ocurre cuando la apertura anterior está por encima de la línea y la apertura actual se mueve por debajo.
  4. Las líneas Vertical funcionan como disparadores temporizados. Una vez que ha transcurrido el número configurado de barras, la estrategia abre una posición inmediatamente a la apertura de la vela actual.
  5. La dirección se resuelve según Direction Mode:
    • FromLabel compara cada etiqueta de línea con Buy Label y Sell Label.
    • ForceBuy y ForceSell tratan todas las líneas en la misma dirección independientemente de las etiquetas.
  6. Cada disparador exitoso envía una orden de mercado con el volumen de Trade Volume, registra la activación y marca la línea como inactiva.
  7. Las distancias opcionales de stop-loss y take-profit se aplican en cada nueva vela evaluando el último precio de entrada frente a los máximos y mínimos de la vela.

Formato de definición de líneas

La cadena Line Definitions usa punto y coma para separar entradas. Cada entrada debe seguir:

Name|Type|Label|BasePrice|SlopePerBar|Length|Ray
  • Name – identificador que se muestra en los registros. Cualquier cadena sin punto y coma.
  • TypeHorizontal, Trend o Vertical (sin distinción de mayúsculas).
  • Label – texto libre usado cuando Direction Mode es FromLabel.
  • BasePrice – precio inicial de la línea en la primera vela procesada. Requerido para cada línea no vertical (decimal, cultura invariante).
  • SlopePerBar – cambio de precio por vela para una línea de tendencia. Use 0 para líneas horizontales.
  • Length – el significado depende del tipo de línea:
    • Para líneas de tendencia u horizontales sin ray, define cuántas barras está el ancla derecha desde el inicio. Después de este conteo, la línea expira automáticamente.
    • Para líneas ray, el valor se ignora porque la línea se extiende indefinidamente.
    • Para líneas verticales, especifica cuántas barras esperar antes de disparar. El valor mínimo aceptado es 1.
  • Raytrue mantiene la línea activa indefinidamente a la derecha, false la restringe a la longitud especificada.

Ejemplo:

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

El ejemplo crea una línea de tendencia de compra ascendente, un nivel horizontal de venta que nunca expira y un disparador vertical único para la próxima vela.

Parámetros

  • Candle Type – tipo de dato de mercado usado para los cálculos. Por defecto el marco temporal de 1 minuto.
  • Trade Volume – tamaño de la orden para nuevas entradas. Debe ser positivo.
  • Direction Mode – determina cómo se selecciona el lado de entrada (FromLabel, ForceBuy, ForceSell).
  • Buy Label / Sell Label – valores de etiqueta para identificar líneas cuando Direction Mode es FromLabel.
  • Line Definitions – cadena bruta que describe cada línea sintética (ver formato arriba).
  • Stop Loss Offset – distancia en unidades de precio para salidas de protección en posiciones largas y cortas (0 deshabilita la comprobación).
  • Take Profit Offset – distancia de precio para objetivos de beneficio (0 deshabilita la comprobación).

Gestión de riesgo

La estrategia no coloca órdenes de stop o take profit por separado. En cambio, monitorea cada vela terminada:

  • Las posiciones largas se cierran si el mínimo de la vela supera EntryPrice - StopLossOffset o el máximo excede EntryPrice + TakeProfitOffset.
  • Las posiciones cortas se cierran si el máximo de la vela supera EntryPrice + StopLossOffset o el mínimo cae por debajo de EntryPrice - TakeProfitOffset.

Si ambos offsets son cero, la posición solo se cerrará por la señal opuesta o intervención manual.

Notas de implementación

  • Todos los comentarios en el código fuente están en inglés para mantener la coherencia con las directrices del proyecto.
  • La estrategia ignora silenciosamente las definiciones de línea inválidas; asegúrese de que el formato sea correcto para evitar perder disparadores.
  • Reiniciar la estrategia limpia el estado interno, por lo que los contadores de líneas y los temporizadores de activación comienzan de nuevo desde la primera vela procesada.
  • El enfoque se centra en los precios de apertura de las velas, igual que el EA original, y no reaccionará a los toques intrabarra.

Uso

  1. Configurar el instrumento de trading y el tipo de vela deseado.
  2. Ajustar Line Definitions para describir cada línea manual con la que desea operar.
  3. Configurar Direction Mode para confiar en etiquetas o forzar el trading unidireccional.
  4. Opcionalmente establecer offsets de stop-loss y take-profit para salidas automáticas.
  5. Iniciar la estrategia y monitorear los registros: cada línea disparada se reporta junto con su dirección y precio de activación.
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;
	}
}