SAR RSI MTS Strategy
Overview
The SAR RSI MTS Strategy is a direct translation of the original MetaTrader 5 expert advisor "SAR RSI MTS" into the StockSharp high-level API. The system follows the direction of the Parabolic SAR indicator and confirms entries with the Relative Strength Index (RSI). It works on completed candles only (default timeframe is 1 hour) and respects a configurable cap on the net position size.
Indicators and Data
- Parabolic SAR (
Acceleration = SarStep, AccelerationStep = SarStep, AccelerationMax = SarMax).
- Relative Strength Index with customizable period and neutral level (default 50).
- Candles supplied by
CandleType, which defaults to hourly time frame data.
Internally the strategy computes a pip value from the security metadata. If the symbol has 3 or 5 decimal places it multiplies the price step by 10, matching the pip handling of the original MQL program.
Entry Logic
A new trade is evaluated at the close of each finished candle once both indicators have produced valid values:
Long setup
- Parabolic SAR value from the previous bar is below the current close and the current SAR has increased compared to the previous value.
- RSI is above the neutral threshold and is rising compared to its previous reading.
- If the account is already net short the strategy first buys enough volume to flip the position and then opens a new long sized according to the
Volume parameter, respecting the MaxPosition limit.
Short setup
- Previous Parabolic SAR value is above the current close and the current SAR has decreased.
- RSI is below the neutral threshold and is falling compared to its previous value.
- Existing long exposure is flattened before establishing the new short. Additional shorts are allowed until the absolute position reaches
MaxPosition.
All comparisons use the instrument precision so that equality tests match the original CompareDoubles helper from MQL.
Exit and Risk Management
Risk controls are evaluated before checking for new entries on every finished candle:
- Fixed stop-loss in pips converted into price units and applied to the average entry price of the current net position.
- Fixed take-profit in pips, handled symmetrically to the stop-loss.
- Trailing stop that becomes active only after unrealized profit exceeds
TrailingStop + TrailingStep. The stop is moved in discrete steps, mimicking the "Trailing" routine from the MQL strategy.
- If none of the above applies the trailing state is reset whenever the position becomes flat.
All exits close the entire net position (long or short). When a protective rule triggers, the strategy skips signal evaluation for the same bar, mirroring the behaviour of broker-side stop orders in the original implementation.
Parameters
| Parameter |
Description |
StopLossPips |
Stop-loss distance expressed in pips. A value of 0 disables the protective stop. |
TakeProfitPips |
Take-profit distance in pips. Disabled when set to 0. |
TrailingStopPips |
Distance of the trailing stop. Disabled when set to 0. |
TrailingStepPips |
Minimum price improvement required before the trailing stop is advanced. |
SarStep |
Acceleration step for Parabolic SAR; also used as the initial acceleration factor. |
SarMax |
Maximum acceleration factor for Parabolic SAR. |
RsiPeriod |
Lookback period for the RSI indicator. |
RsiNeutralLevel |
RSI threshold separating bullish and bearish bias (default 50). |
CandleType |
Candle subscription used for calculations (default 1 hour). |
MaxPosition |
Maximum absolute net position allowed by the strategy. |
Additional Notes
- The default configuration reproduces the original EA inputs: 10 pip stop, 40 pip target, 15/5 pip trailing stop, Parabolic SAR
0.05/0.5, and RSI period 14.
- Volume is controlled by the base
Strategy.Volume property. Position scaling honours MaxPosition and automatically handles reversals.
- Indicator bindings and order routing rely entirely on the StockSharp high-level API without manual series access, ensuring compliance with the project guidelines.
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Parabolic SAR and RSI strategy translated from the original MQL implementation.
/// </summary>
public class SarRsiMtsStrategy : Strategy
{
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<decimal> _sarStep;
private readonly StrategyParam<decimal> _sarMax;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiNeutralLevel;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _maxPosition;
private decimal? _previousSar;
private decimal? _previousRsi;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
private decimal _pipSize;
private decimal _entryPrice;
private DateTimeOffset _lastTradeTime;
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Trailing step distance expressed in pips.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Parabolic SAR acceleration step.
/// </summary>
public decimal SarStep
{
get => _sarStep.Value;
set => _sarStep.Value = value;
}
/// <summary>
/// Parabolic SAR maximum acceleration.
/// </summary>
public decimal SarMax
{
get => _sarMax.Value;
set => _sarMax.Value = value;
}
/// <summary>
/// RSI lookback period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// RSI neutral level used for bullish or bearish confirmation.
/// </summary>
public decimal RsiNeutralLevel
{
get => _rsiNeutralLevel.Value;
set => _rsiNeutralLevel.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum absolute net position allowed by the strategy.
/// </summary>
public decimal MaxPosition
{
get => _maxPosition.Value;
set => _maxPosition.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="SarRsiMtsStrategy"/> class.
/// </summary>
public SarRsiMtsStrategy()
{
_stopLossPips = Param(nameof(StopLossPips), 10m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 40m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 15m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Trailing step distance in pips", "Risk");
_sarStep = Param(nameof(SarStep), 0.05m)
.SetGreaterThanZero()
.SetDisplay("SAR Step", "Parabolic SAR acceleration step", "Indicators");
_sarMax = Param(nameof(SarMax), 0.5m)
.SetGreaterThanZero()
.SetDisplay("SAR Maximum", "Parabolic SAR maximum acceleration", "Indicators");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "Lookback period for RSI", "Indicators");
_rsiNeutralLevel = Param(nameof(RsiNeutralLevel), 50m)
.SetDisplay("RSI Neutral", "Neutral RSI threshold separating bullish and bearish bias", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type for indicator calculations", "General");
_maxPosition = Param(nameof(MaxPosition), 5m)
.SetGreaterThanZero()
.SetDisplay("Max Position", "Maximum absolute net position allowed", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_previousSar = null;
_previousRsi = null;
_longTrailingStop = null;
_shortTrailingStop = null;
_pipSize = 0;
_entryPrice = 0;
_lastTradeTime = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
var parabolicSar = new ParabolicSar
{
Acceleration = SarStep,
AccelerationStep = SarStep,
AccelerationMax = SarMax
};
var rsi = new RelativeStrengthIndex
{
Length = RsiPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(parabolicSar, rsi, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, parabolicSar);
DrawIndicator(area, rsi);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal sarValue, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (ManageRisk(candle))
return;
if (sarValue == 0m || rsiValue == 0m)
return;
if (!_previousSar.HasValue || !_previousRsi.HasValue)
{
_previousSar = sarValue;
_previousRsi = rsiValue;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousSar = sarValue;
_previousRsi = rsiValue;
return;
}
// Cooldown: skip if a trade was placed within the last ~240 candles (5-min candles = ~1200 min)
if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime) < TimeSpan.FromMinutes(1200))
{
_previousSar = sarValue;
_previousRsi = rsiValue;
return;
}
var sarPrev = _previousSar.Value;
var rsiPrev = _previousRsi.Value;
var price = candle.ClosePrice;
var buySignal = sarPrev < price
&& !AreClose(sarPrev, price)
&& sarValue > sarPrev
&& rsiValue > RsiNeutralLevel
&& rsiValue > rsiPrev
&& !AreClose(rsiValue, rsiPrev);
if (buySignal)
{
EnterLong(candle);
}
else
{
var sellSignal = sarPrev > price
&& !AreClose(sarPrev, price)
&& sarValue < sarPrev
&& rsiValue < RsiNeutralLevel
&& rsiValue < rsiPrev
&& !AreClose(rsiValue, rsiPrev);
if (sellSignal)
EnterShort(candle);
}
_previousSar = sarValue;
_previousRsi = rsiValue;
}
private void EnterLong(ICandleMessage candle)
{
var tradeVolume = Volume;
if (tradeVolume <= 0m)
return;
var maxPosition = MaxPosition;
if (maxPosition <= 0m)
return;
var current = Position;
var target = current < 0 ? Math.Min(maxPosition, tradeVolume) : Math.Min(maxPosition, current + tradeVolume);
var required = target - current;
if (required <= 0m)
return;
BuyMarket(required);
_longTrailingStop = null;
_shortTrailingStop = null;
_lastTradeTime = candle.OpenTime;
}
private void EnterShort(ICandleMessage candle)
{
var tradeVolume = Volume;
if (tradeVolume <= 0m)
return;
var maxPosition = MaxPosition;
if (maxPosition <= 0m)
return;
var current = Position;
var target = current > 0 ? -Math.Min(maxPosition, tradeVolume) : Math.Max(-maxPosition, current - tradeVolume);
var required = current - target;
if (required <= 0m)
return;
SellMarket(required);
_longTrailingStop = null;
_shortTrailingStop = null;
_lastTradeTime = candle.OpenTime;
}
private bool ManageRisk(ICandleMessage candle)
{
if (Position > 0m)
{
var entryPrice = _entryPrice;
if (entryPrice <= 0m)
return false;
var trailingTriggered = UpdateLongTrailing(candle, entryPrice);
if (trailingTriggered)
return true;
var stopDistance = GetPriceOffset(StopLossPips);
if (stopDistance > 0m)
{
var stopPrice = entryPrice - stopDistance;
if (candle.LowPrice <= stopPrice)
{
SellMarket(Position);
ResetTrailing();
return true;
}
}
var takeDistance = GetPriceOffset(TakeProfitPips);
if (takeDistance > 0m)
{
var takePrice = entryPrice + takeDistance;
if (candle.HighPrice >= takePrice)
{
SellMarket(Position);
ResetTrailing();
return true;
}
}
}
else if (Position < 0m)
{
var entryPrice = _entryPrice;
if (entryPrice <= 0m)
return false;
var trailingTriggered = UpdateShortTrailing(candle, entryPrice);
if (trailingTriggered)
return true;
var stopDistance = GetPriceOffset(StopLossPips);
if (stopDistance > 0m)
{
var stopPrice = entryPrice + stopDistance;
if (candle.HighPrice >= stopPrice)
{
BuyMarket(Math.Abs(Position));
ResetTrailing();
return true;
}
}
var takeDistance = GetPriceOffset(TakeProfitPips);
if (takeDistance > 0m)
{
var takePrice = entryPrice - takeDistance;
if (candle.LowPrice <= takePrice)
{
BuyMarket(Math.Abs(Position));
ResetTrailing();
return true;
}
}
}
else
{
ResetTrailing();
}
return false;
}
private bool UpdateLongTrailing(ICandleMessage candle, decimal entryPrice)
{
var trailingDistance = GetPriceOffset(TrailingStopPips);
if (trailingDistance <= 0m)
{
_longTrailingStop = null;
return false;
}
var trailingStep = GetPriceOffset(TrailingStepPips);
var profit = candle.ClosePrice - entryPrice;
if (profit >= trailingDistance + trailingStep)
{
var candidate = candle.ClosePrice - trailingDistance;
var threshold = candle.ClosePrice - (trailingDistance + trailingStep);
if (!_longTrailingStop.HasValue || _longTrailingStop.Value < threshold)
_longTrailingStop = candidate;
}
if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
{
SellMarket(Position);
ResetTrailing();
return true;
}
return false;
}
private bool UpdateShortTrailing(ICandleMessage candle, decimal entryPrice)
{
var trailingDistance = GetPriceOffset(TrailingStopPips);
if (trailingDistance <= 0m)
{
_shortTrailingStop = null;
return false;
}
var trailingStep = GetPriceOffset(TrailingStepPips);
var profit = entryPrice - candle.ClosePrice;
if (profit >= trailingDistance + trailingStep)
{
var candidate = candle.ClosePrice + trailingDistance;
var threshold = candle.ClosePrice + (trailingDistance + trailingStep);
if (!_shortTrailingStop.HasValue || _shortTrailingStop.Value > threshold)
_shortTrailingStop = candidate;
}
if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
{
BuyMarket(Math.Abs(Position));
ResetTrailing();
return true;
}
return false;
}
private decimal GetPriceOffset(decimal pips)
{
if (pips <= 0m)
return 0m;
var pip = _pipSize;
if (pip <= 0m)
pip = Security?.PriceStep ?? 1m;
return pip * pips;
}
private decimal CalculatePipSize()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
priceStep = 1m;
var decimals = Security?.Decimals;
var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;
return priceStep * adjust;
}
private void ResetTrailing()
{
_longTrailingStop = null;
_shortTrailingStop = null;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Trade == null) return;
if (Position != 0 && _entryPrice == 0m)
_entryPrice = trade.Trade.Price;
if (Position == 0)
_entryPrice = 0m;
}
private bool AreClose(decimal value1, decimal value2)
{
var decimals = Security?.Decimals ?? 4;
return Math.Round(value1 - value2, decimals) == 0m;
}
}
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.Indicators import ParabolicSar, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class sar_rsi_mts_strategy(Strategy):
def __init__(self):
super(sar_rsi_mts_strategy, self).__init__()
self._sl_pips = self.Param("StopLossPips", 10.0).SetNotNegative().SetDisplay("Stop Loss (pips)", "SL distance", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 40.0).SetNotNegative().SetDisplay("Take Profit (pips)", "TP distance", "Risk")
self._trailing_pips = self.Param("TrailingStopPips", 15.0).SetNotNegative().SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 5.0).SetNotNegative().SetDisplay("Trailing Step (pips)", "Trailing step distance", "Risk")
self._sar_step = self.Param("SarStep", 0.05).SetGreaterThanZero().SetDisplay("SAR Step", "Parabolic SAR acceleration step", "Indicators")
self._sar_max = self.Param("SarMax", 0.5).SetGreaterThanZero().SetDisplay("SAR Maximum", "Parabolic SAR maximum acceleration", "Indicators")
self._rsi_period = self.Param("RsiPeriod", 14).SetGreaterThanZero().SetDisplay("RSI Period", "Lookback period for RSI", "Indicators")
self._rsi_neutral = self.Param("RsiNeutralLevel", 50.0).SetDisplay("RSI Neutral", "Neutral RSI threshold", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Candle type", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(sar_rsi_mts_strategy, self).OnReseted()
self._prev_sar = None
self._prev_rsi = None
self._entry_price = 0
self._long_trailing = None
self._short_trailing = None
def OnStarted2(self, time):
super(sar_rsi_mts_strategy, self).OnStarted2(time)
self._prev_sar = None
self._prev_rsi = None
self._entry_price = 0
self._long_trailing = None
self._short_trailing = None
self._pip_size = 1.0
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
self._pip_size = float(self.Security.PriceStep)
sar = ParabolicSar()
sar.Acceleration = self._sar_step.Value
sar.AccelerationStep = self._sar_step.Value
sar.AccelerationMax = self._sar_max.Value
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(sar, rsi, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, sar)
self.DrawIndicator(area, rsi)
self.DrawOwnTrades(area)
def OnProcess(self, candle, sar_val, rsi_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
# Manage risk first
if self._manage_risk(candle, close):
self._prev_sar = sar_val
self._prev_rsi = rsi_val
return
if self._prev_sar is None or self._prev_rsi is None:
self._prev_sar = sar_val
self._prev_rsi = rsi_val
return
prev_sar = self._prev_sar
prev_rsi = self._prev_rsi
# Buy signal: SAR below price, SAR rising, RSI above neutral and rising
buy_signal = (prev_sar < close
and sar_val > prev_sar
and rsi_val > self._rsi_neutral.Value
and rsi_val > prev_rsi)
if buy_signal:
if self.Position < 0:
self.BuyMarket()
if self.Position <= 0:
self.BuyMarket()
self._entry_price = close
self._long_trailing = None
self._short_trailing = None
else:
# Sell signal: SAR above price, SAR falling, RSI below neutral and falling
sell_signal = (prev_sar > close
and sar_val < prev_sar
and rsi_val < self._rsi_neutral.Value
and rsi_val < prev_rsi)
if sell_signal:
if self.Position > 0:
self.SellMarket()
if self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._long_trailing = None
self._short_trailing = None
self._prev_sar = sar_val
self._prev_rsi = rsi_val
def _manage_risk(self, candle, close):
pip = self._pip_size
if self.Position > 0 and self._entry_price > 0:
# Trailing
trail_dist = self._trailing_pips.Value * pip
trail_step = self._trailing_step_pips.Value * pip
if trail_dist > 0:
profit = close - self._entry_price
if profit >= trail_dist + trail_step:
candidate = close - trail_dist
threshold = close - (trail_dist + trail_step)
if self._long_trailing is None or self._long_trailing < threshold:
self._long_trailing = candidate
if self._long_trailing is not None and float(candle.LowPrice) <= self._long_trailing:
self.SellMarket()
self._reset_trailing()
return True
# Stop loss
sl_dist = self._sl_pips.Value * pip
if sl_dist > 0:
if float(candle.LowPrice) <= self._entry_price - sl_dist:
self.SellMarket()
self._reset_trailing()
return True
# Take profit
tp_dist = self._tp_pips.Value * pip
if tp_dist > 0:
if float(candle.HighPrice) >= self._entry_price + tp_dist:
self.SellMarket()
self._reset_trailing()
return True
elif self.Position < 0 and self._entry_price > 0:
# Trailing
trail_dist = self._trailing_pips.Value * pip
trail_step = self._trailing_step_pips.Value * pip
if trail_dist > 0:
profit = self._entry_price - close
if profit >= trail_dist + trail_step:
candidate = close + trail_dist
threshold = close + (trail_dist + trail_step)
if self._short_trailing is None or self._short_trailing > threshold:
self._short_trailing = candidate
if self._short_trailing is not None and float(candle.HighPrice) >= self._short_trailing:
self.BuyMarket()
self._reset_trailing()
return True
# Stop loss
sl_dist = self._sl_pips.Value * pip
if sl_dist > 0:
if float(candle.HighPrice) >= self._entry_price + sl_dist:
self.BuyMarket()
self._reset_trailing()
return True
# Take profit
tp_dist = self._tp_pips.Value * pip
if tp_dist > 0:
if float(candle.LowPrice) <= self._entry_price - tp_dist:
self.BuyMarket()
self._reset_trailing()
return True
else:
self._reset_trailing()
return False
def _reset_trailing(self):
self._long_trailing = None
self._short_trailing = None
self._entry_price = 0
def CreateClone(self):
return sar_rsi_mts_strategy()