This strategy reproduces the behaviour of the original my_ts15.mq5 expert advisor by managing trailing stop orders around an existing net position. A linear weighted moving average (LWMA) drives the stop placement and can be replaced by other smoothing methods. The logic continuously:
Reads the moving average value from a configurable number of completed candles.
Compares price progress with the moving average trail and price-based offsets.
Moves the protective stop order only when the new level improves the previous one by at least the specified step.
Optionally enforces a maximum loss distance by clamping the stop or immediately liquidating the position when the limit is broken.
The strategy does not produce entry signals. It is meant to run together with other components (manual or automated) that open positions on the same security.
Trading Logic
Subscribe to the selected candle series and bind a moving average indicator using the StockSharp high-level API.
As soon as a candle is finished, store the indicator result and obtain the value that is MaBarsTrail + MaShift bars behind the current bar.
Convert the point-based settings to absolute price distances using the instrument tick size.
For long positions, choose the lowest of:
The moving average minus its offset.
The current price minus the “in profit” offset.
Afterwards clamp the trail to the “in loss” distance and optionally to the maximum allowed loss.
For short positions, choose the highest of:
The moving average plus its offset.
The current price plus the “in profit” offset.
Afterwards clamp the trail to the “in loss” distance and optionally to the maximum allowed loss.
Update the stop order only when the improvement exceeds TrailStepPoints (unless it is zero, in which case every improvement is accepted).
If the price breaches the maximum loss distance and EnforceMaxStopLoss is enabled, the strategy closes the position immediately.
All price inputs use the candle price specified in MaPrice, matching the original MQL setting where the indicator is fed with the PRICE_WEIGHTED series.
Parameters
Name
Default
Description
MaPeriod
50
Length of the moving average used as the trailing backbone.
MaShift
0
Additional shift (in bars) applied when sampling the moving average value.
MaMethod
LinearWeighted
Smoothing method of the moving average (simple, exponential, smoothed, linear weighted).
MaPrice
Weighted
Candle price fed to the moving average.
MaBarsTrail
1
Number of completed bars between the current candle and the moving average sample.
TrailBehindMaPoints
5
Distance in points kept between the stop and the moving average.
TrailBehindPricePoints
30
Distance in points kept behind the price when the position is profitable.
TrailBehindNegativePoints
60
Distance in points kept behind the price when the position is losing.
TrailStepPoints
0
Minimum improvement (in points) required before moving the stop. Zero replicates the “always update” behaviour.
EnforceMaxStopLoss
false
If enabled, clamp the stop to the maximum allowed loss and liquidate the position when price exceeds that limit.
MaxStopLossPoints
100
Maximum allowed loss distance in points.
ShowIndicator
true
Draw the moving average and the trade markers on the chart when the UI is available.
CandleType
M1
Candle data type driving the calculations.
All point-based inputs are converted to price distances via the instrument pip size calculated from Security.PriceStep.
Conversion Notes
The MQL expert refreshed the MA handle manually. The StockSharp implementation uses BindEx to process the indicator without accessing internal buffers or calling GetValue.
Bid/Ask prices are not directly available from finished candles, therefore the trailing calculations use the candle price selected by MaPrice. This keeps the behaviour consistent because the original script fed the indicator with the same weighted price and compared it with Bid/Ask ticks.
PositionModify is replaced by cancelling and recreating protective stop orders (SellStop for long, BuyStop for short). The strategy stores the last stop level to mimic the MetaTrader trailing thresholds.
The optional forced close (pre_init) follows the original logic: once the market moves beyond MaxStopLossPoints, the position is closed immediately.
No entry logic has been added; users should combine this trailing module with their own signal provider.
Usage Tips
Attach the strategy to the same security that opens the positions.
Adjust the point distances to the instrument tick size (Forex symbols generally use “pip” values, CFDs may require different multipliers).
Set TrailStepPoints to a positive value to reduce order churn on illiquid instruments.
Disable EnforceMaxStopLoss if another risk manager already controls hard stop distances.
Keep ShowIndicator enabled while tuning the parameters to visualise the moving average and trailing behaviour.
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()