Cross Line Trader Strategy
Overview
The strategy emulates the original MetaTrader "Cross Line Trader" expert by reacting to price interactions with user-defined synthetic lines. Instead of listening to manual chart objects, the StockSharp version receives all line descriptions through a single parameter, parses them at start and continuously monitors finished candles. When a candle open moves through an active line, the strategy places a market order in the corresponding direction and deactivates that line so it cannot trigger again.
Trading logic
- The strategy subscribes to the candle type selected in the Candle Type parameter and only processes candles in the
Finished state to avoid intrabar noise.
- Synthetic lines are created from the Line Definitions parameter. Each line keeps its own state (active/expired, number of processed bars and geometry).
- For Trend or Horizontal lines the algorithm compares the previous candle open with the next one relative to the line's price trajectory:
- A long signal occurs when the previous open is below the line and the current open moves above it.
- A short signal occurs when the previous open is above the line and the current open moves below it.
- Vertical lines behave like timed triggers. Once the configured number of bars has elapsed the strategy opens a position immediately at the current candle open.
- Direction is resolved according to Direction Mode:
FromLabel compares each line label against Buy Label and Sell Label.
ForceBuy and ForceSell treat all lines as the same direction regardless of labels.
- Every successful trigger sends a market order with the volume from Trade Volume, logs the activation and marks the line as inactive.
- Optional stop-loss and take-profit distances are applied on every new candle by evaluating the last entry price against candle highs and lows.
The Line Definitions string uses semicolons to separate entries. Each entry must follow:
Name|Type|Label|BasePrice|SlopePerBar|Length|Ray
- Name – identifier shown in logs. Any string without semicolons.
- Type –
Horizontal, Trend or Vertical (case-insensitive).
- Label – free text used when Direction Mode is
FromLabel.
- BasePrice – initial price of the line at the first processed candle. Required for every non-vertical line (decimal, invariant culture).
- SlopePerBar – price change per candle for a trend line. Use
0 for horizontal lines.
- Length – meaning depends on the line type:
- For trend or horizontal lines without a ray it defines how many bars the right anchor is away from the start. After this count the line expires automatically.
- For ray lines the value is ignored because the line extends indefinitely.
- For vertical lines it specifies how many bars to wait before firing. The minimum accepted value is
1.
- Ray –
true keeps the line active indefinitely to the right, false restricts it to the specified length.
Example:
TrendLine|Trend|Buy|1.1000|0.0005|8|false;HorizontalSell|Horizontal|Sell|1.1050|0|0|true;VerticalImpulse|Vertical|Buy|0|0|1|false
The example creates a rising buy trend line, a horizontal sell level that never expires and a one-off vertical trigger for the next candle.
Parameters
- Candle Type – market data type used for calculations. Defaults to 1-minute time frame.
- Trade Volume – order size for new entries. Must be positive.
- Direction Mode – determines how the entry side is selected (
FromLabel, ForceBuy, ForceSell).
- Buy Label / Sell Label – label values for identifying lines when
Direction Mode is FromLabel.
- Line Definitions – raw string that describes every synthetic line (see format above).
- Stop Loss Offset – distance in price units for protective exits on long and short positions (0 disables the check).
- Take Profit Offset – price distance for profit targets (0 disables the check).
Risk management
The strategy does not place separate stop or take profit orders. Instead it monitors every finished candle:
- Long positions close if the candle low breaches
EntryPrice - StopLossOffset or the high exceeds EntryPrice + TakeProfitOffset.
- Short positions close if the candle high breaches
EntryPrice + StopLossOffset or the low goes below EntryPrice - TakeProfitOffset.
If both offsets are zero the position will only be closed by the opposite signal or manual intervention.
Implementation notes
- All comments in the source code are in English to keep consistency with the project guidelines.
- The strategy ignores invalid line definitions silently; ensure the format is correct to avoid missing triggers.
- Re-starting the strategy clears the internal state, so line counters and activation timers begin again from the first processed candle.
- The approach focuses on candle open prices just like the original EA and will not react to intrabar touches.
Usage
- Configure the trading security and desired candle type.
- Adjust Line Definitions to describe every manual line you want to trade against.
- Set Direction Mode to either rely on labels or to force one-sided trading.
- Optionally set stop-loss and take-profit offsets for automatic exits.
- Start the strategy and monitor the logs: each triggered line is reported together with its direction and activation price.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class cross_line_trader_strategy(Strategy):
DIR_FROM_LABEL = 0
DIR_FORCE_BUY = 1
DIR_FORCE_SELL = 2
LINE_HORIZONTAL = 0
LINE_TREND = 1
LINE_VERTICAL = 2
def __init__(self):
super(cross_line_trader_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._trade_volume = self.Param("TradeVolume", 1.0)
self._direction_mode = self.Param("DirectionMode", self.DIR_FROM_LABEL)
self._buy_label = self.Param("BuyLabel", "Buy")
self._sell_label = self.Param("SellLabel", "Sell")
self._line_definitions = self.Param("LineDefinitions",
"TrendLine|Trend|Buy|64000|50|20|false;HorizontalSell|Horizontal|Sell|68000|0|0|true;HorizontalBuy|Horizontal|Buy|62000|0|0|true")
self._stop_loss_offset = self.Param("StopLossOffset", 0.0)
self._take_profit_offset = self.Param("TakeProfitOffset", 0.0)
self._lines = []
self._previous_open = None
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def TradeVolume(self):
return self._trade_volume.Value
@property
def DirectionMode(self):
return self._direction_mode.Value
@property
def BuyLabel(self):
return self._buy_label.Value
@property
def SellLabel(self):
return self._sell_label.Value
@property
def LineDefinitions(self):
return self._line_definitions.Value
@property
def StopLossOffset(self):
return self._stop_loss_offset.Value
@property
def TakeProfitOffset(self):
return self._take_profit_offset.Value
def OnStarted2(self, time):
super(cross_line_trader_strategy, self).OnStarted2(time)
self._lines = self._parse_line_definitions(self.LineDefinitions)
self._previous_open = None
self._entry_price = 0.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def OnOwnTradeReceived(self, trade):
super(cross_line_trader_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Trade is None:
return
pos = float(self.Position)
if pos == 0:
self._entry_price = 0.0
return
self._entry_price = float(trade.Trade.Price)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
if float(self.TradeVolume) <= 0:
return
current_open = float(candle.OpenPrice)
for line in self._lines:
if not line["is_active"]:
continue
previous_index = line["steps_processed"]
current_index = previous_index + 1
if line["type"] == self.LINE_VERTICAL:
if current_index >= max(1, line["length"]):
direction = self._resolve_direction(line)
if direction is not None and self._try_open_position(direction, candle):
line["is_active"] = False
line["steps_processed"] = current_index
continue
if not line["ray"] and line["length"] > 0 and previous_index >= line["length"]:
line["is_active"] = False
continue
if self._previous_open is None:
line["steps_processed"] = current_index
continue
prev_line_price = self._get_line_price(line, previous_index)
curr_line_price = self._get_line_price(line, current_index)
dir_for_line = self._resolve_direction(line)
if dir_for_line is None:
line["steps_processed"] = current_index
continue
if line["type"] == self.LINE_HORIZONTAL:
cross_up = self._previous_open <= prev_line_price and current_open > prev_line_price
cross_down = self._previous_open >= prev_line_price and current_open < prev_line_price
else:
cross_up = self._previous_open <= prev_line_price and current_open > curr_line_price
cross_down = self._previous_open >= prev_line_price and current_open < curr_line_price
pos = float(self.Position)
if cross_up and dir_for_line == 1 and pos <= 0:
if self._try_open_position(1, candle):
line["is_active"] = False
elif cross_down and dir_for_line == -1 and pos >= 0:
if self._try_open_position(-1, candle):
line["is_active"] = False
line["steps_processed"] = current_index
if not line["ray"] and line["length"] > 0 and current_index >= line["length"]:
line["is_active"] = False
self._manage_protective_exits(candle)
self._previous_open = current_open
def _resolve_direction(self, line):
mode = self.DirectionMode
if mode == self.DIR_FORCE_BUY:
return 1
elif mode == self.DIR_FORCE_SELL:
return -1
elif mode == self.DIR_FROM_LABEL:
buy_label = self.BuyLabel
sell_label = self.SellLabel
label = line["label"]
if buy_label and label.lower() == buy_label.lower():
return 1
if sell_label and label.lower() == sell_label.lower():
return -1
return None
def _try_open_position(self, direction, candle):
pos = float(self.Position)
if direction == 1:
if pos > 0:
return False
self.BuyMarket(float(self.TradeVolume))
self._entry_price = float(candle.OpenPrice)
else:
if pos < 0:
return False
self.SellMarket(float(self.TradeVolume))
self._entry_price = float(candle.OpenPrice)
return True
def _manage_protective_exits(self, candle):
pos = float(self.Position)
if pos > 0:
volume = abs(pos)
if float(self.StopLossOffset) > 0 and float(candle.LowPrice) <= self._entry_price - float(self.StopLossOffset):
self.SellMarket(volume)
return
if float(self.TakeProfitOffset) > 0 and float(candle.HighPrice) >= self._entry_price + float(self.TakeProfitOffset):
self.SellMarket(volume)
elif pos < 0:
volume = abs(pos)
if float(self.StopLossOffset) > 0 and float(candle.HighPrice) >= self._entry_price + float(self.StopLossOffset):
self.BuyMarket(volume)
return
if float(self.TakeProfitOffset) > 0 and float(candle.LowPrice) <= self._entry_price - float(self.TakeProfitOffset):
self.BuyMarket(volume)
def _get_line_price(self, line, index):
if line["type"] == self.LINE_VERTICAL:
return 0.0
clamped = max(0, index)
if not line["ray"] and line["length"] > 0:
clamped = min(clamped, line["length"])
return line["base_price"] + line["slope"] * clamped
def _parse_line_definitions(self, raw):
result = []
if raw is None or raw.strip() == "":
return result
entries = raw.replace("\n", ";").replace("\r", ";").split(";")
for entry in entries:
entry = entry.strip()
if not entry:
continue
parts = entry.split("|")
if len(parts) < 7:
continue
name = parts[0].strip()
type_text = parts[1].strip().lower()
label = parts[2].strip()
try:
base_price = float(parts[3].strip())
except:
continue
try:
slope = float(parts[4].strip())
except:
slope = 0.0
try:
length = int(parts[5].strip())
except:
length = 0
ray_text = parts[6].strip().lower()
ray = ray_text == "true"
if type_text == "horizontal":
line_type = self.LINE_HORIZONTAL
elif type_text == "trend":
line_type = self.LINE_TREND
elif type_text == "vertical":
line_type = self.LINE_VERTICAL
else:
continue
if line_type == self.LINE_VERTICAL and length <= 0:
length = 1
result.append({
"name": name if name else type_text,
"label": label,
"type": line_type,
"base_price": base_price,
"slope": slope,
"length": max(0, length),
"ray": ray,
"is_active": True,
"steps_processed": 0
})
return result
def OnReseted(self):
super(cross_line_trader_strategy, self).OnReseted()
self._lines = []
self._previous_open = None
self._entry_price = 0.0
def CreateClone(self):
return cross_line_trader_strategy()