Виртуальный трейлинг-стоп — портирование эксперта MetaTrader Virtual Trailing Stop.mq5 (MQL ID 21362) на платформу StockSharp. Исходный советник не открывал сделки, а лишь сопровождал уже существующие позиции, рассчитывая «виртуальные» стоп-приказы. Версия на C# повторяет этот подход: стратегия отслеживает лучшие котировки bid/ask и закрывает позицию рыночными заявками при срабатывании стоп-лосса, тейк-профита или трейлинг-стопа.
Стратегия никогда не инициирует входов. Её задача — обеспечить автоматическое сопровождение позиций, созданных вручную или другими модулями, но в инфраструктуре StockSharp (Designer, Shell, Runner).
Логика работы
Подписка на Level1 – стратегия получает поток лучших цен и сохраняет последние значения bid/ask.
Перевод параметров в цену – все входы задаются в пипсах. Значение умножается на PriceStep. Для инструментов с 3 и 5 знаками после запятой используется дополнительный множитель ×10, чтобы соответствовать определению pip в MetaTrader.
Контроль стоп-лосса – длинная позиция закрывается, если bid ≤ цена входа − StopLoss, короткая – если ask ≥ цена входа + StopLoss.
Контроль тейк-профита – длинная позиция закрывается при bid ≥ цена входа + TakeProfit, короткая – при ask ≤ цена входа − TakeProfit.
Активация трейлинга – как только плавающая прибыль достигла TrailingStart пипсов, формируется виртуальный уровень трейлинг-стопа (bid − TrailingStop для long, ask + TrailingStop для short).
Сдвиг трейлинга – при увеличении прибыли минимум на TrailingStep пипсов уровень сдвигается вслед за ценой. Нулевое значение означает непрерывный трейлинг.
Выход по трейлингу – позиция закрывается, когда цена касается уровня трейлинга и сделка остаётся прибыльной (аналог проверки Profit()>0 в исходном коде).
Никакие стоп-ордера на биржу не отправляются — выход всегда осуществляется рыночной заявкой, сохраняя «виртуальный» характер сопровождения.
Параметры
Параметр
Назначение
Значение по умолчанию
StopLossPips
Расстояние до стоп-лосса в пипсах. 0 отключает жёсткий стоп.
0
TakeProfitPips
Расстояние до тейк-профита в пипсах. 0 отключает тейк-профит.
0
TrailingStopPips
Дистанция между ценой и трейлинг-уровнем в пипсах.
5
TrailingStartPips
Минимальная прибыль (в пипсах) для запуска трейлинга.
5
TrailingStepPips
Минимальный шаг обновления трейлинга (в пипсах). 0 — обновление при каждом выгодном тике.
1
Параметры реализованы через StrategyParam, поэтому доступны для оптимизации.
Особенности реализации
Используются только данные уровня 1 (DataType.Level1); графические объекты из MetaTrader не переносятся.
Перевод пипсов в цену зависит от Security.PriceStep и Security.Decimals. При отсутствии информации по тик-сайзу применяется значение 1.
Для длинных и коротких позиций поддерживаются отдельные переменные трейлинг-уровня.
Автоматическое открытие тестовых позиций, присутствовавшее в исходном советнике, опущено, так как в StockSharp используется учёт чистой позиции.
Рекомендации по использованию
Запускайте стратегию на инструменте, по которому уже есть открытые сделки или они появятся от других стратегий/ручных действий.
Комбинируйте с собственными модулями входа, чтобы получать знакомый по MetaTrader функционал сопровождения в продуктах StockSharp.
Для инструментов с крупным тиком подбирайте значения в пипсах в соответствии с PriceStep; при TrailingStopPips = 1 трейлинг сдвигается на один шаг цены.
Состав поставки
CS/VirtualTrailingStopLevel1Strategy.cs — исходный код стратегии.
README.md, README_zh.md, README_ru.md — документация на английском, китайском и русском языках.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Virtual Trailing Stop Level1 strategy (simplified). Uses EMA with
/// percentage-based trailing stop for position management.
/// </summary>
public class VirtualTrailingStopLevel1Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<decimal> _trailingPercent;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
public decimal TrailingPercent
{
get => _trailingPercent.Value;
set => _trailingPercent.Value = value;
}
public VirtualTrailingStopLevel1Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles", "General");
_emaLength = Param(nameof(EmaLength), 15)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "EMA period", "Indicators");
_trailingPercent = Param(nameof(TrailingPercent), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Trailing %", "Trailing stop percent", "Risk");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaLength };
decimal highSinceEntry = 0;
decimal lowSinceEntry = decimal.MaxValue;
decimal prevClose = 0;
decimal prevEma = 0;
var hasPrev = false;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, (ICandleMessage candle, decimal emaVal) =>
{
if (candle.State != CandleStates.Finished)
return;
if (!hasPrev)
{
prevClose = candle.ClosePrice;
prevEma = emaVal;
hasPrev = true;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
prevClose = candle.ClosePrice;
prevEma = emaVal;
return;
}
var close = candle.ClosePrice;
// Trailing stop management
if (Position > 0)
{
if (candle.HighPrice > highSinceEntry) highSinceEntry = candle.HighPrice;
if (close < highSinceEntry * (1m - TrailingPercent / 100m))
{
SellMarket();
highSinceEntry = 0;
lowSinceEntry = decimal.MaxValue;
return;
}
}
else if (Position < 0)
{
if (candle.LowPrice < lowSinceEntry) lowSinceEntry = candle.LowPrice;
if (close > lowSinceEntry * (1m + TrailingPercent / 100m))
{
BuyMarket();
highSinceEntry = 0;
lowSinceEntry = decimal.MaxValue;
return;
}
}
// Entry based on EMA
var bullishCross = prevClose <= prevEma && close > emaVal;
var bearishCross = prevClose >= prevEma && close < emaVal;
if (bullishCross && Position <= 0)
{
BuyMarket();
highSinceEntry = candle.HighPrice;
lowSinceEntry = decimal.MaxValue;
}
else if (bearishCross && Position >= 0)
{
SellMarket();
lowSinceEntry = candle.LowPrice;
highSinceEntry = 0;
}
prevClose = close;
prevEma = emaVal;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class virtual_trailing_stop_level1_strategy(Strategy):
def __init__(self):
super(virtual_trailing_stop_level1_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles", "General")
self._ema_length = self.Param("EmaLength", 15) \
.SetDisplay("EMA Length", "EMA period", "Indicators")
self._trailing_percent = self.Param("TrailingPercent", 1.5) \
.SetDisplay("Trailing %", "Trailing stop percent", "Risk")
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def EmaLength(self):
return self._ema_length.Value
@property
def TrailingPercent(self):
return self._trailing_percent.Value
def OnReseted(self):
super(virtual_trailing_stop_level1_strategy, self).OnReseted()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(virtual_trailing_stop_level1_strategy, self).OnStarted2(time)
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
ema = ExponentialMovingAverage()
ema.Length = self.EmaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _on_process(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
ev = float(ema_value)
if not self._has_prev:
self._prev_close = close
self._prev_ema = ev
self._has_prev = True
return
if self.Position > 0:
if high > self._high_since_entry:
self._high_since_entry = high
if close < self._high_since_entry * (1.0 - self.TrailingPercent / 100.0):
self.SellMarket()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = close
self._prev_ema = ev
return
elif self.Position < 0:
if low < self._low_since_entry:
self._low_since_entry = low
if close > self._low_since_entry * (1.0 + self.TrailingPercent / 100.0):
self.BuyMarket()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = close
self._prev_ema = ev
return
bullish_cross = self._prev_close <= self._prev_ema and close > ev
bearish_cross = self._prev_close >= self._prev_ema and close < ev
if bullish_cross and self.Position <= 0:
self.BuyMarket()
self._high_since_entry = high
self._low_since_entry = 1e18
elif bearish_cross and self.Position >= 0:
self.SellMarket()
self._low_since_entry = low
self._high_since_entry = 0.0
self._prev_close = close
self._prev_ema = ev
def CreateClone(self):
return virtual_trailing_stop_level1_strategy()