TRAYLERv — это перенос эксперта MetaTrader 4 TRAYLERv, который изначально выступал помощником по сопровождению сделок. Он не генерировал сигналы, а автоматически подтягивал защитные ордера и позволял быстро удалить все отложенные заявки. В версии для StockSharp реализовано то же поведение с использованием высокоуровневого API: стратегия подписывается на свечи, отслеживает фракталы Билла Уильямса и управляет ордерами через стандартные методы Buy/Sell.
Стратегия не открывает позиции самостоятельно. Предполагается, что сделки создаются вручную либо другим алгоритмом. После появления позиции TRAYLERv берёт на себя поддержку стопов и тейк-профитов по оригинальной логике, сохраняя названия параметров и их смысл.
Логика работы
Подписка на выбранный тип свечей (по умолчанию минутные) и накопление истории. После появления пяти завершённых баров стратегия начинает фиксировать локальные максимумы и минимумы, точно повторяя расчёт MT4-индикатора iFractals.
После закрытия каждой свечи с чётной минутой проверяется текущая позиция:
Длинная позиция: ищется ближайший нисходящий фрактал в пределах StopFractalDepth баров (по умолчанию 7). Если найден, регистрируется или переносится стоп-заявка SellStop ниже минимума фрактала с учётом текущего спрэда и двух шагов цены. Если фрактала нет, берётся минимум бара трёх свечей назад и уменьшается на два шага. При наличии прибыли и включённом параметре UseTakeProfit ищется восходящий фрактал глубиной до TakeProfitFractalDepth (21 бар) и ставится SellLimit немного ниже уровня фрактала.
Короткая позиция: выполняются зеркальные действия — стоп перемещается по верхним фракталам, тейк-профит рассчитывается по нижним фракталам, а к цене добавляется запас над максимумом, чтобы исключить ложные срабатывания.
Включение DeleteAllPendingOrders приводит к отмене всех активных отложенных заявок на подключении. Параметр DeleteOwnPendingOrders ограничивает очистку только инструментом стратегии. Оба переключателя полностью соответствуют опциям MT4-советника.
При отсутствии позиции все защитные ордера, созданные стратегией, отменяются, чтобы не оставлять «хвостов» в стакане заявок.
Управление рисками
Защитные ордера выставляются функциями SellStop, BuyStop, SellLimit, BuyLimit с объёмом, равным абсолютной величине текущей позиции.
Стопы и тейк-профиты независимы. Если выключить UseTakeProfit, лимитные ордера удаляются, но механизм подтягивания стопов продолжит работу.
Для корректировки уровней используется текущий спрэд, вычисляемый по лучшим котировкам. Если он недоступен, применяется минимальный шаг цены, чтобы не ставить ордер прямо по рыночной стоимости.
Перед регистрацией цены округляются к шагу цены инструмента, а объёмы приводятся к VolumeStep и учитывают ограничения MinVolume/MaxVolume.
Параметры
Параметр
Описание
Значение по умолчанию
OrderVolume
Рекомендованный объём позиции. Сохранён для совместимости, в расчётах не участвует.
0.1
DeleteAllPendingOrders
При значении true удаляет все отложенные заявки после каждой свечи.
false
DeleteOwnPendingOrders
При значении true удаляет только заявки по текущему инструменту.
false
UseTakeProfit
Включает управление тейк-профитом на основе фракталов.
true
EnableSound
Наследованный флаг звуковых уведомлений MT4, в версии StockSharp не используется.
true
ShowCommentary
Флаг отображения комментариев в MT4, оставлен для полноты настроек.
true
StopFractalDepth
Глубина поиска фрактала для подтягивания стоп-лосса.
7
TakeProfitFractalDepth
Глубина поиска фрактала для тейк-профита.
21
CandleType
Тип свечей, используемый в расчётах. По умолчанию — минутные.
1 минута
Особенности реализации
Используется высокоуровневая подписка SubscribeCandles().Bind(...), поэтому расчёты ведутся только по завершённым барам — так же, как это делал оригинальный советник на тиках.
Фракталы рассчитываются вручную на основе списка последних свечей, что исключает необходимость подключать дополнительные индикаторы и повторяет поведение iFractals из MetaTrader.
Все уровни проходят нормализацию к шагу цены, а объёмы — к шагу объёма. Это гарантирует, что сформированные ордера валидны для любой торговой площадки.
Python-реализация не предусмотрена, поэтому папка PY отсутствует согласно требованиям по конвертации.
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
public class TraylerStrategy : Strategy
{
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _cooldownCandles;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevClose;
private decimal _prevEma;
private bool _hasPrev;
private int _cooldownRemaining;
public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }
public int CooldownCandles { get => _cooldownCandles.Value; set => _cooldownCandles.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public TraylerStrategy()
{
_emaPeriod = Param(nameof(EmaPeriod), 50).SetDisplay("EMA Period", "EMA lookback", "Indicators");
_cooldownCandles = Param(nameof(CooldownCandles), 200).SetDisplay("Cooldown", "Candles between signals", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame()).SetDisplay("Candle Type", "Candle timeframe", "General");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevClose = default;
_prevEma = default;
_hasPrev = default;
_cooldownRemaining = default;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevClose = 0;
_prevEma = 0;
_hasPrev = false;
_cooldownRemaining = 0;
var ema = new ExponentialMovingAverage { Length = EmaPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ema, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal ema)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
if (!_hasPrev) { _prevClose = close; _prevEma = ema; _hasPrev = true; return; }
if (_cooldownRemaining > 0)
{
_cooldownRemaining--;
_prevClose = close;
_prevEma = ema;
return;
}
if (_prevClose <= _prevEma && close > ema && Position <= 0)
{
if (Position < 0) BuyMarket();
BuyMarket();
_cooldownRemaining = CooldownCandles;
}
else if (_prevClose >= _prevEma && close < ema && Position >= 0)
{
if (Position > 0) SellMarket();
SellMarket();
_cooldownRemaining = CooldownCandles;
}
_prevClose = close;
_prevEma = ema;
}
}
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 trayler_strategy(Strategy):
def __init__(self):
super(trayler_strategy, self).__init__()
self._ema_period = self.Param("EmaPeriod", 50).SetDisplay("EMA Period", "EMA lookback", "Indicators")
self._cooldown_candles = self.Param("CooldownCandles", 200).SetDisplay("Cooldown", "Candles between signals", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
self._cooldown_remaining = 0
@property
def ema_period(self): return self._ema_period.Value
@property
def cooldown_candles(self): return self._cooldown_candles.Value
@property
def candle_type(self): return self._candle_type.Value
def OnReseted(self):
super(trayler_strategy, self).OnReseted()
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(trayler_strategy, self).OnStarted2(time)
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
self._cooldown_remaining = 0
ema = ExponentialMovingAverage()
ema.Length = self.ema_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema, self.process_candle).Start()
def process_candle(self, candle, ema):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
ema_val = float(ema)
if not self._has_prev:
self._prev_close = close
self._prev_ema = ema_val
self._has_prev = True
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
self._prev_close = close
self._prev_ema = ema_val
return
if self._prev_close <= self._prev_ema and close > ema_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self.cooldown_candles
elif self._prev_close >= self._prev_ema and close < ema_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self.cooldown_candles
self._prev_close = close
self._prev_ema = ema_val
def CreateClone(self):
return trayler_strategy()