Стратегия Cross Line Trader
Обзор
Стратегия повторяет логику советника MetaTrader «Cross Line Trader»: она реагирует на пересечения цены с набором заранее описанных линий. В версии для StockSharp линии не рисуются вручную на графике, а задаются одной строкой параметра. При запуске строка разбирается, после чего стратегия отслеживает только завершённые свечи. Как только новая свеча открывается по другую сторону активной линии, отправляется рыночный ордер в соответствующем направлении, а сама линия выключается, чтобы не сработать повторно.
Торговая логика
- Подписка идёт на тип свечей из параметра Candle Type; обрабатываются только свечи в состоянии
Finished, что исключает внутридневной шум.
- Линии создаются из параметра Line Definitions, каждая хранит собственное состояние: активность, счётчик обработанных баров и геометрию.
- Для линий типа Trend и Horizontal стратегия сравнивает открытия двух соседних свечей относительно траектории линии:
- Лонг активируется, если предыдущее открытие было ниже линии, а текущее — выше.
- Шорт активируется, если предыдущее открытие было выше линии, а текущее — ниже.
- Vertical-линии работают как таймеры: по истечении заданного числа баров позиция открывается немедленно по текущему открытию свечи.
- Направление сделки определяется параметром Direction Mode:
FromLabel сопоставляет подпись линии со значениями Buy Label и Sell Label.
ForceBuy и ForceSell принудительно используют только одну сторону независимо от подписи.
- После удачного срабатывания подаётся рыночный ордер объёмом из Trade Volume, событие записывается в лог, а линия деактивируется.
- Параметры Stop Loss Offset и Take Profit Offset задают защитные расстояния: на каждой новой свече стратегия проверяет минимум/максимум относительно последней цены входа и при необходимости закрывает позицию.
Формат описания линий
Параметр Line Definitions состоит из записей, разделённых точкой с запятой. Каждая запись имеет вид:
Name|Type|Label|BasePrice|SlopePerBar|Length|Ray
- Name — произвольное имя для логов, не должно содержать точек с запятой.
- Type —
Horizontal, Trend или Vertical (без учёта регистра).
- Label — текст для сопоставления в режиме
FromLabel.
- BasePrice — базовая цена линии для первой обрабатываемой свечи (десятичное число в invariant culture).
- SlopePerBar — изменение цены на один бар для трендовой линии; для горизонтальной укажите
0.
- Length — интерпретация зависит от типа:
- Для трендовых и горизонтальных линий без луча задаёт, насколько далеко вправо расположен конечный бар; после достижения значения линия отключается.
- Для лучей (
Ray = true) параметр игнорируется — линия активна бесконечно.
- Для вертикальных линий определяет, через сколько баров сработает триггер (минимум
1).
- Ray —
true оставляет линию активной бесконечно вправо, false ограничивает область действия значением Length.
Пример строки:
TrendLine|Trend|Buy|1.1000|0.0005|8|false;HorizontalSell|Horizontal|Sell|1.1050|0|0|true;VerticalImpulse|Vertical|Buy|0|0|1|false
Здесь задана восходящая линия для покупок, горизонтальный уровень для продаж (без срока действия) и вертикальный триггер на следующую свечу.
Параметры
- Candle Type — тип свечей, используемый в расчётах; по умолчанию таймфрейм 1 минута.
- Trade Volume — объём рыночных заявок при открытии позиции (должен быть больше нуля).
- Direction Mode — способ выбора направления (
FromLabel, ForceBuy, ForceSell).
- Buy Label / Sell Label — подписи, определяющие сделки при режиме
FromLabel.
- Line Definitions — строка с описанием всех линий (см. формат выше).
- Stop Loss Offset — защитное расстояние в ценовых единицах; значение
0 отключает контроль.
- Take Profit Offset — расстояние до цели по прибыли;
0 отключает контроль.
Управление рисками
Отдельные стоп- и тейк-приказы не выставляются. Вместо этого стратегия на каждой закрывшейся свече проверяет:
- Для лонга: минимум <=
EntryPrice - StopLossOffset или максимум >= EntryPrice + TakeProfitOffset — позиция закрывается продажей по рынку.
- Для шорта: максимум >=
EntryPrice + StopLossOffset или минимум <= EntryPrice - TakeProfitOffset — позиция закрывается покупкой по рынку.
Если оба смещения равны нулю, позиция закрывается только встречным сигналом или вручную.
Особенности реализации
- Все комментарии в исходнике написаны на английском языке согласно требованиям проекта.
- Некорректные записи в строке линий пропускаются без ошибок, поэтому рекомендуется тщательно проверять формат.
- Перезапуск стратегии сбрасывает внутренние счётчики, и ожидание по линиям начинается заново с первой новой свечи.
- Алгоритм ориентируется исключительно на цены открытия, как и оригинальный эксперт, поэтому внутрисвечные касания не учитываются.
Порядок использования
- Выберите торговый инструмент и подходящий тип свечей.
- Заполните Line Definitions, описав все нужные уровни и трендовые линии.
- Настройте Direction Mode и, при необходимости, подписи для покупок и продаж.
- Укажите защитные расстояния, если требуется автоматический выход.
- Запустите стратегию и следите за журналом: каждая сработавшая линия будет отмечена направлением и ценой срабатывания.
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()