Стратегия повторяет логику советника my_ts15.mq5: она не открывает сделки самостоятельно, а занимается только управлением защитного стоп-ордера уже существующей позиции. В качестве «направляющей» используется скользящая средняя (по умолчанию линейно-взвешенная), вокруг которой формируется динамический трейлинг. Алгоритм:
Получает значение скользящей средней по завершённым свечам.
Сравнивает прогресс цены и положение скользящей, вычисляя две альтернативные точки для стопа.
Передвигает стоп только тогда, когда новое значение лучше предыдущего не менее чем на TrailStepPoints.
При необходимости ограничивает максимальный убыток: стоп «прижимается» к границе, а при пробое позиция закрывается немедленно.
Таким образом, модуль можно запускать параллельно с любым сигнализатором или ручной торговлей на том же инструменте.
Логика сопровождения
Подписаться на выбранный тип свечей и привязать индикатор скользящей средней через BindEx, избегая ручного доступа к буферам.
После закрытия свечи сохранить значение индикатора и взять выборку, отстоящую на MaBarsTrail + MaShift баров назад.
Пересчитать все значения, заданные в пунктах, в абсолютные цены через шаг цены инструмента.
Для длинной позиции взять минимум из двух кандидатов: «МА минус отступ» и «цена минус прибыльный отступ». Дополнительно ограничить уровень отрицательным отступом и, при необходимости, максимальным убытком.
Для короткой позиции взять максимум из «МА плюс отступ» и «цена плюс прибыльный отступ», далее применить негативный и максимальный пороги.
Передвигать стоп только при улучшении больше либо равном TrailStepPoints (если значение равно нулю — любое улучшение).
При включённом EnforceMaxStopLoss и пробое цены за предел MaxStopLossPoints позиция закрывается сразу же.
Используемая цена определяется параметром MaPrice, что соответствует оригинальному советнику, в котором LWMA рассчитывалась по PRICE_WEIGHTED.
Параметры
Параметр
Значение по умолчанию
Описание
MaPeriod
50
Длина скользящей средней, управляющей трейлингом.
MaShift
0
Дополнительный сдвиг (в барах) при запросе значения МА.
MaMethod
LinearWeighted
Метод сглаживания МА (простая, экспоненциальная, сглаженная, линейно-взвешенная).
MaPrice
Weighted
Цена свечи, подаваемая на вход индикатора.
MaBarsTrail
1
Количество завершённых баров между текущей свечой и значением МА.
TrailBehindMaPoints
5
Отступ в пунктах между стопом и скользящей средней.
TrailBehindPricePoints
30
Отступ в пунктах от текущей цены при движении в прибыль.
TrailBehindNegativePoints
60
Отступ в пунктах от текущей цены при движении в убыток.
TrailStepPoints
0
Минимальное улучшение (в пунктах) для смещения стопа. Ноль — обновлять всегда.
EnforceMaxStopLoss
false
Включает контроль максимального убытка.
MaxStopLossPoints
100
Максимальная допустимая дистанция убытка в пунктах.
ShowIndicator
true
Рисовать индикатор и сделки на графике при наличии UI.
CandleType
M1
Тип свечей, управляющий расчётами.
Все величины в пунктах пересчитываются в абсолютные цены через шаг цены Security.PriceStep.
Особенности конвертации
В MQL-версии скользящая средняя обновлялась через ручной вызов Refresh и доступ к буферу. Здесь индикатор обрабатывается автоматически через BindEx, что соответствует требованиям проекта.
В оригинале использовались Bid/Ask. В StockSharp для закрытых свечей доступен только выбранный тип цены, поэтому сравнительный анализ выполняется на основе значения MaPrice. Это обеспечивает сопоставимость с исходным алгоритмом, поскольку индикатор строится по той же цене.
Операции PositionModify заменены отменой и повторной регистрацией стоп-ордеров (SellStop для лонга, BuyStop для шорта). Храним последнюю цену стопа, чтобы воспроизвести проверку trail - sl >= step.
Флаг pre_init реализован как EnforceMaxStopLoss: если цена ушла дальше допустимого максимума, позиция закрывается немедленно.
Логика входа отсутствует — стратегия рассчитана на совместную работу с другими модулями.
Рекомендации по использованию
Запускайте стратегию на том же инструменте, где открываются позиции.
Проверяйте соответствие значений в пунктах шагу цены. Для большинства валютных пар pip равен десятикратному шагу.
Чтобы уменьшить количество модификаций на малоликвидных инструментах, задайте положительное TrailStepPoints.
Если риск-менеджмент решается другой системой, оставьте EnforceMaxStopLoss выключенным.
При подборе параметров держите ShowIndicator включённым, чтобы визуально оценивать работу трейлинга.
namespace StockSharp.Samples.Strategies;
using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// My TS15 strategy: WMA trend following with trailing stop management.
/// Enters on price crossing WMA, exits with trailing stop logic.
/// </summary>
public class MyTs15Strategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _trailMultiplier;
private readonly StrategyParam<int> _signalCooldownCandles;
private decimal _entryPrice;
private decimal _bestPrice;
private bool _wasBullish;
private bool _hasPrevSignal;
private int _candlesSinceTrade;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal TrailMultiplier { get => _trailMultiplier.Value; set => _trailMultiplier.Value = value; }
public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }
public MyTs15Strategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(120).TimeFrame())
.SetDisplay("Candle Type", "Candle timeframe", "General");
_maPeriod = Param(nameof(MaPeriod), 100)
.SetGreaterThanZero()
.SetDisplay("MA Period", "WMA period", "Indicators");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR period for trailing", "Indicators");
_trailMultiplier = Param(nameof(TrailMultiplier), 3m)
.SetDisplay("Trail Multiplier", "ATR multiplier for trailing stop", "Risk");
_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 12)
.SetGreaterThanZero()
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0m;
_bestPrice = 0m;
_wasBullish = false;
_hasPrevSignal = false;
_candlesSinceTrade = SignalCooldownCandles;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_entryPrice = 0;
_bestPrice = 0;
_hasPrevSignal = false;
_candlesSinceTrade = SignalCooldownCandles;
var wma = new WeightedMovingAverage { Length = MaPeriod };
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(wma, atr, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, decimal wmaValue, decimal atrValue)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
var trailDist = atrValue * TrailMultiplier;
var isBullish = close > wmaValue;
if (_candlesSinceTrade < SignalCooldownCandles)
_candlesSinceTrade++;
// Trailing stop check
if (Position > 0)
{
if (close > _bestPrice) _bestPrice = close;
if (_bestPrice - close > trailDist)
{
SellMarket();
_entryPrice = 0;
_bestPrice = 0;
_candlesSinceTrade = 0;
return;
}
}
else if (Position < 0)
{
if (close < _bestPrice) _bestPrice = close;
if (close - _bestPrice > trailDist)
{
BuyMarket();
_entryPrice = 0;
_bestPrice = 0;
_candlesSinceTrade = 0;
return;
}
}
// Entry signals
if (_hasPrevSignal && isBullish != _wasBullish && _candlesSinceTrade >= SignalCooldownCandles)
{
if (isBullish && Position <= 0)
{
BuyMarket();
_entryPrice = close;
_bestPrice = close;
_candlesSinceTrade = 0;
}
else if (!isBullish && Position >= 0)
{
SellMarket();
_entryPrice = close;
_bestPrice = close;
_candlesSinceTrade = 0;
}
}
_wasBullish = isBullish;
_hasPrevSignal = true;
}
}
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 WeightedMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class my_ts15_strategy(Strategy):
def __init__(self):
super(my_ts15_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 100) \
.SetDisplay("MA Period", "WMA period", "Indicators")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "ATR period for trailing", "Indicators")
self._trail_multiplier = self.Param("TrailMultiplier", 3.0) \
.SetDisplay("Trail Multiplier", "ATR multiplier for trailing stop", "Risk")
self._signal_cooldown = self.Param("SignalCooldownCandles", 12) \
.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading")
self._wma = None
self._atr = None
self._entry_price = 0.0
self._best_price = 0.0
self._was_bullish = False
self._has_prev_signal = False
self._candles_since_trade = 0
@property
def ma_period(self):
return self._ma_period.Value
@property
def atr_period(self):
return self._atr_period.Value
@property
def trail_multiplier(self):
return self._trail_multiplier.Value
@property
def signal_cooldown(self):
return self._signal_cooldown.Value
def OnReseted(self):
super(my_ts15_strategy, self).OnReseted()
self._wma = None
self._atr = None
self._entry_price = 0.0
self._best_price = 0.0
self._was_bullish = False
self._has_prev_signal = False
self._candles_since_trade = self.signal_cooldown
def OnStarted2(self, time):
super(my_ts15_strategy, self).OnStarted2(time)
self._wma = WeightedMovingAverage()
self._wma.Length = self.ma_period
self._atr = AverageTrueRange()
self._atr.Length = self.atr_period
self._entry_price = 0.0
self._best_price = 0.0
self._has_prev_signal = False
self._candles_since_trade = self.signal_cooldown
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(120)))
subscription.Bind(self._wma, self._atr, self._process_candle)
subscription.Start()
def _process_candle(self, candle, wma_value, atr_value):
if candle.State != CandleStates.Finished:
return
if not self._wma.IsFormed or not self._atr.IsFormed:
return
close = float(candle.ClosePrice)
wma_val = float(wma_value)
atr_val = float(atr_value)
trail_dist = atr_val * self.trail_multiplier
is_bullish = close > wma_val
if self._candles_since_trade < self.signal_cooldown:
self._candles_since_trade += 1
if self.Position > 0:
if close > self._best_price:
self._best_price = close
if self._best_price - close > trail_dist:
self.SellMarket()
self._entry_price = 0.0
self._best_price = 0.0
self._candles_since_trade = 0
return
elif self.Position < 0:
if close < self._best_price:
self._best_price = close
if close - self._best_price > trail_dist:
self.BuyMarket()
self._entry_price = 0.0
self._best_price = 0.0
self._candles_since_trade = 0
return
if self._has_prev_signal and is_bullish != self._was_bullish and self._candles_since_trade >= self.signal_cooldown:
if is_bullish and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
self._best_price = close
self._candles_since_trade = 0
elif not is_bullish and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._best_price = close
self._candles_since_trade = 0
self._was_bullish = is_bullish
self._has_prev_signal = True
def CreateClone(self):
return my_ts15_strategy()