Exp Sar Tm Plus Strategy
High-level StockSharp port of the Exp_Sar_Tm_Plus expert advisor. The strategy monitors Parabolic SAR reversals on a configurable timeframe and mirrors the original money-management and time-out features while keeping the logic compatible with the StockSharp high-level API.
Trading logic
- Candles are subscribed from the
CandleTypeparameter (default: 4-hour time frame). The Parabolic SAR indicator is calculated with the user-definedSarStepandSarMaximumcoefficients. - For every finished candle the algorithm buffers close prices and SAR values. The
SignalBarparameter selects which closed candle is evaluated (default: the last closed bar) and compares it to the preceding candle to detect a change in SAR direction. - A long position is opened when price crosses above the SAR (previous candle below SAR, selected candle above SAR) and long trading is enabled. Existing short exposure is closed automatically before switching direction.
- A short position is opened when price crosses below the SAR (previous candle above SAR, selected candle below SAR) and short trading is enabled. Existing long exposure is flattened first.
- Positions are closed when the SAR moves against them (
AllowLongExit/AllowShortExit), when optional stop-loss / take-profit levels are breached, or when the maximum holding time (UseTimeExit+HoldingMinutes) expires. - Stop-loss and take-profit levels are recalculated on every entry using the instrument
PriceStep. Both levels are optional and ignored when the corresponding value is zero.
Parameters
MoneyManagement– fraction of the baseVolumethat will be traded on every entry. Values ≤ 0 fall back to the plainVolumevalue. Normalized to the instrumentVolumeStep.ManagementMode– enumeration preserved from the original expert. All modes currently behave likeLot(fixed volume) inside this port.StopLossPoints/TakeProfitPoints– distance in price steps used to set protective levels around the entry price. Set to zero to disable.DeviationPoints– original slippage setting. It is kept for completeness, but the high-level API executes market orders without using this value.AllowLongEntry,AllowShortEntry– toggles for opening long/short positions.AllowLongExit,AllowShortExit– toggles for closing positions when price crosses the SAR in the opposite direction.UseTimeExit– enables position liquidation afterHoldingMinutesminutes in the market.HoldingMinutes– duration for the time-based exit window.CandleType– candle data type for SAR analysis.SarStep,SarMaximum– Parabolic SAR configuration.SignalBar– number of closed candles to shift the signal evaluation (0 = current finished candle, 1 = previous, etc.).
Risk management and notes
- The strategy invokes
StartProtection()on start, enabling StockSharp built-in protection services. - Time-based exits rely on the candle
CloseTime(fallback toOpenTimeif it is not available) to measure the holding period accurately. - Only one net position is maintained at any time. Position reversals automatically close the opposite side before entering a new trade.
- The implementation keeps the parameter set of the original MQL5 expert. Some options (like non-
Lotmoney management modes or orderDeviationPoints) are placeholders because the high-level API abstracts broker-side mechanics.
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>
/// Port of the Exp_Sar_Tm_Plus MQL5 expert advisor.
/// The strategy monitors Parabolic SAR swings on a configurable timeframe and
/// mirrors the original entry/exit automation together with optional time-based protection.
/// </summary>
public class ExpSarTmPlusStrategy : Strategy
{
private readonly StrategyParam<decimal> _moneyManagement;
private readonly StrategyParam<MoneyManagementModes> _moneyManagementMode;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _deviationPoints;
private readonly StrategyParam<bool> _allowLongEntry;
private readonly StrategyParam<bool> _allowShortEntry;
private readonly StrategyParam<bool> _allowLongExit;
private readonly StrategyParam<bool> _allowShortExit;
private readonly StrategyParam<bool> _useTimeExit;
private readonly StrategyParam<int> _holdingMinutes;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _sarStep;
private readonly StrategyParam<decimal> _sarMaximum;
private readonly StrategyParam<int> _signalBar;
private decimal?[] _closeBuffer = Array.Empty<decimal?>();
private decimal?[] _sarBuffer = Array.Empty<decimal?>();
private int _bufferIndex;
private int _bufferCount;
private decimal? _stopPrice;
private decimal? _takePrice;
private DateTimeOffset? _positionEntryTime;
/// <summary>
/// Money management modes replicated from the original expert.
/// </summary>
public enum MoneyManagementModes
{
FreeMargin,
Balance,
LossFreeMargin,
LossBalance,
Lot,
}
/// <summary>
/// Portion of the base volume that will be traded.
/// </summary>
public decimal MoneyManagement
{
get => _moneyManagement.Value;
set => _moneyManagement.Value = value;
}
/// <summary>
/// Interpretation mode for the money management value.
/// </summary>
public MoneyManagementModes ManagementMode
{
get => _moneyManagementMode.Value;
set => _moneyManagementMode.Value = value;
}
/// <summary>
/// Stop loss distance expressed in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Maximum slippage accepted when executing market orders.
/// </summary>
public int DeviationPoints
{
get => _deviationPoints.Value;
set => _deviationPoints.Value = value;
}
/// <summary>
/// Enables long entries when Parabolic SAR flips above price.
/// </summary>
public bool AllowLongEntry
{
get => _allowLongEntry.Value;
set => _allowLongEntry.Value = value;
}
/// <summary>
/// Enables short entries when Parabolic SAR flips below price.
/// </summary>
public bool AllowShortEntry
{
get => _allowShortEntry.Value;
set => _allowShortEntry.Value = value;
}
/// <summary>
/// Allows closing long positions when price falls under the SAR value.
/// </summary>
public bool AllowLongExit
{
get => _allowLongExit.Value;
set => _allowLongExit.Value = value;
}
/// <summary>
/// Allows closing short positions when price rises above the SAR value.
/// </summary>
public bool AllowShortExit
{
get => _allowShortExit.Value;
set => _allowShortExit.Value = value;
}
/// <summary>
/// Enables time-based liquidation of open positions.
/// </summary>
public bool UseTimeExit
{
get => _useTimeExit.Value;
set => _useTimeExit.Value = value;
}
/// <summary>
/// Maximum holding time in minutes before a position is closed.
/// </summary>
public int HoldingMinutes
{
get => _holdingMinutes.Value;
set => _holdingMinutes.Value = value;
}
/// <summary>
/// Candle type used to evaluate the Parabolic SAR signals.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Acceleration step for Parabolic SAR.
/// </summary>
public decimal SarStep
{
get => _sarStep.Value;
set => _sarStep.Value = value;
}
/// <summary>
/// Maximum acceleration for Parabolic SAR.
/// </summary>
public decimal SarMaximum
{
get => _sarMaximum.Value;
set => _sarMaximum.Value = value;
}
/// <summary>
/// Number of closed candles used as signal offset.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = value;
}
/// <summary>
/// Initialize parameters with defaults aligned to the MQL5 implementation.
/// </summary>
public ExpSarTmPlusStrategy()
{
_moneyManagement = Param(nameof(MoneyManagement), 0.1m)
.SetDisplay("Money Management", "Portion of the base volume used per entry", "Risk")
.SetOptimize(0.05m, 1m, 0.05m);
_moneyManagementMode = Param(nameof(ManagementMode), MoneyManagementModes.Lot)
.SetDisplay("Money Management Mode", "Mode used to interpret the money management value", "Risk");
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetDisplay("Stop Loss (points)", "Stop loss distance measured in price steps", "Risk")
.SetOptimize(100, 3000, 100)
.SetNotNegative();
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetDisplay("Take Profit (points)", "Take profit distance measured in price steps", "Risk")
.SetOptimize(100, 5000, 100)
.SetNotNegative();
_deviationPoints = Param(nameof(DeviationPoints), 10)
.SetDisplay("Execution Deviation", "Maximum allowed deviation in points", "Orders")
.SetNotNegative();
_allowLongEntry = Param(nameof(AllowLongEntry), true)
.SetDisplay("Enable Long Entries", "Allow opening long positions", "Execution");
_allowShortEntry = Param(nameof(AllowShortEntry), true)
.SetDisplay("Enable Short Entries", "Allow opening short positions", "Execution");
_allowLongExit = Param(nameof(AllowLongExit), true)
.SetDisplay("Enable Long Exits", "Allow closing long positions on SAR cross", "Execution");
_allowShortExit = Param(nameof(AllowShortExit), true)
.SetDisplay("Enable Short Exits", "Allow closing short positions on SAR cross", "Execution");
_useTimeExit = Param(nameof(UseTimeExit), true)
.SetDisplay("Enable Time Exit", "Close positions after the holding period", "Risk");
_holdingMinutes = Param(nameof(HoldingMinutes), 240)
.SetDisplay("Holding Minutes", "Maximum position holding time in minutes", "Risk")
.SetOptimize(60, 720, 60)
.SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for Parabolic SAR", "Data");
_sarStep = Param(nameof(SarStep), 0.02m)
.SetDisplay("SAR Step", "Acceleration step for Parabolic SAR", "Indicators")
.SetOptimize(0.01m, 0.1m, 0.01m)
.SetGreaterThanZero();
_sarMaximum = Param(nameof(SarMaximum), 0.2m)
.SetDisplay("SAR Maximum", "Maximum acceleration for Parabolic SAR", "Indicators")
.SetOptimize(0.1m, 1m, 0.1m)
.SetGreaterThanZero();
_signalBar = Param(nameof(SignalBar), 1)
.SetDisplay("Signal Bar Offset", "Number of closed candles used for signal confirmation", "Data")
.SetNotNegative();
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
InitializeBuffers();
ResetRiskLevels();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
InitializeBuffers();
var parabolicSar = new ParabolicSar
{
Acceleration = SarStep,
AccelerationMax = SarMaximum,
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(parabolicSar, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, parabolicSar);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal sarValue)
{
if (candle.State != CandleStates.Finished)
return;
EnsureBufferSize();
UpdateBuffers(candle.ClosePrice, sarValue);
if (_bufferCount <= Math.Max(0, SignalBar) + 1)
return;
var (currentClose, currentSar, previousClose, previousSar) = GetSignalValues();
if (currentClose is null || currentSar is null || previousClose is null || previousSar is null)
return;
var isPriceAboveCurrentSar = currentClose.Value > currentSar.Value;
var wasPriceAbovePreviousSar = previousClose.Value > previousSar.Value;
HandleExits(candle, isPriceAboveCurrentSar);
var crossedUp = !wasPriceAbovePreviousSar && isPriceAboveCurrentSar;
var crossedDown = wasPriceAbovePreviousSar && !isPriceAboveCurrentSar;
if (crossedUp && AllowLongEntry && Position <= 0)
{
EnterLong(candle);
}
else if (crossedDown && AllowShortEntry && Position >= 0)
{
EnterShort(candle);
}
}
private void InitializeBuffers()
{
var size = Math.Max(2, Math.Max(0, SignalBar) + 2);
_closeBuffer = new decimal?[size];
_sarBuffer = new decimal?[size];
_bufferIndex = 0;
_bufferCount = 0;
}
private void EnsureBufferSize()
{
var size = Math.Max(2, Math.Max(0, SignalBar) + 2);
if (_closeBuffer.Length == size)
return;
_closeBuffer = new decimal?[size];
_sarBuffer = new decimal?[size];
_bufferIndex = 0;
_bufferCount = 0;
}
private void UpdateBuffers(decimal close, decimal sar)
{
var size = _closeBuffer.Length;
if (size == 0)
return;
_closeBuffer[_bufferIndex] = close;
_sarBuffer[_bufferIndex] = sar;
_bufferIndex = (_bufferIndex + 1) % size;
if (_bufferCount < size)
_bufferCount++;
}
private (decimal? currentClose, decimal? currentSar, decimal? previousClose, decimal? previousSar) GetSignalValues()
{
var size = _closeBuffer.Length;
if (size == 0)
return (null, null, null, null);
var signalOffset = Math.Max(0, SignalBar);
var currentIndex = (_bufferIndex - 1 - signalOffset + size) % size;
var previousIndex = (_bufferIndex - 2 - signalOffset + size) % size;
return (
_closeBuffer[currentIndex],
_sarBuffer[currentIndex],
_closeBuffer[previousIndex],
_sarBuffer[previousIndex]);
}
private void HandleExits(ICandleMessage candle, bool isPriceAboveCurrentSar)
{
if (Position > 0)
{
if (ShouldExitByTime(candle))
{
CloseLong();
return;
}
if (AllowLongExit && !isPriceAboveCurrentSar)
{
CloseLong();
return;
}
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
CloseLong();
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
CloseLong();
}
}
else if (Position < 0)
{
if (ShouldExitByTime(candle))
{
CloseShort();
return;
}
if (AllowShortExit && isPriceAboveCurrentSar)
{
CloseShort();
return;
}
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
CloseShort();
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
CloseShort();
}
}
}
private bool ShouldExitByTime(ICandleMessage candle)
{
var positionEntryTime = _positionEntryTime;
if (!UseTimeExit || !positionEntryTime.HasValue)
return false;
var holdingPeriod = TimeSpan.FromMinutes(Math.Max(0, HoldingMinutes));
if (holdingPeriod <= TimeSpan.Zero)
return false;
var closeTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
return closeTime - positionEntryTime.Value >= holdingPeriod;
}
private void EnterLong(ICandleMessage candle)
{
ResetRiskLevels();
var volume = GetOrderVolume() + Math.Abs(Math.Min(0m, Position));
if (volume <= 0)
return;
BuyMarket(volume);
_positionEntryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
var priceStep = Security?.PriceStep ?? 1m;
if (priceStep <= 0)
priceStep = 1m;
_stopPrice = StopLossPoints > 0 ? candle.ClosePrice - priceStep * StopLossPoints : null;
_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice + priceStep * TakeProfitPoints : null;
}
private void EnterShort(ICandleMessage candle)
{
ResetRiskLevels();
var volume = GetOrderVolume() + Math.Abs(Math.Max(0m, Position));
if (volume <= 0)
return;
SellMarket(volume);
_positionEntryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
var priceStep = Security?.PriceStep ?? 1m;
if (priceStep <= 0)
priceStep = 1m;
_stopPrice = StopLossPoints > 0 ? candle.ClosePrice + priceStep * StopLossPoints : null;
_takePrice = TakeProfitPoints > 0 ? candle.ClosePrice - priceStep * TakeProfitPoints : null;
}
private void CloseLong()
{
var volume = Math.Abs(Position);
if (volume <= 0)
return;
SellMarket(volume);
ResetRiskLevels();
}
private void CloseShort()
{
var volume = Math.Abs(Position);
if (volume <= 0)
return;
BuyMarket(volume);
ResetRiskLevels();
}
private decimal GetOrderVolume()
{
var step = 1m;
if (step <= 0)
step = 1m;
var baseVolume = Volume * MoneyManagement;
if (baseVolume <= 0)
baseVolume = Volume;
var normalized = Math.Round(baseVolume / step) * step;
if (normalized <= 0)
normalized = step;
return normalized;
}
private void ResetRiskLevels()
{
_stopPrice = null;
_takePrice = null;
_positionEntryTime = null;
}
}
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
from StockSharp.Algo.Strategies import Strategy
class exp_sar_tm_plus_strategy(Strategy):
"""
Parabolic SAR strategy with time-based exit.
Enters on SAR crossover, exits on reverse cross, SL/TP, or time limit.
"""
def __init__(self):
super(exp_sar_tm_plus_strategy, self).__init__()
self._stop_loss_points = self.Param("StopLossPoints", 1000) \
.SetDisplay("Stop Loss (points)", "Stop distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 2000) \
.SetDisplay("Take Profit (points)", "Take profit in price steps", "Risk")
self._use_time_exit = self.Param("UseTimeExit", True) \
.SetDisplay("Enable Time Exit", "Close after holding period", "Risk")
self._holding_minutes = self.Param("HoldingMinutes", 240) \
.SetDisplay("Holding Minutes", "Max position holding time", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe for SAR", "Data")
self._sar_step = self.Param("SarStep", 0.02) \
.SetDisplay("SAR Step", "Acceleration step", "Indicators")
self._sar_max = self.Param("SarMaximum", 0.2) \
.SetDisplay("SAR Maximum", "Maximum acceleration", "Indicators")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar Offset", "Bars for signal confirmation", "Data")
self._close_buffer = []
self._sar_buffer = []
self._buffer_index = 0
self._buffer_count = 0
self._stop_price = None
self._take_price = None
self._entry_time = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(exp_sar_tm_plus_strategy, self).OnReseted()
self._init_buffers()
self._reset_risk()
def OnStarted2(self, time):
super(exp_sar_tm_plus_strategy, self).OnStarted2(time)
self._init_buffers()
sar = ParabolicSar()
sar.Acceleration = self._sar_step.Value
sar.AccelerationMax = self._sar_max.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sar, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sar)
self.DrawOwnTrades(area)
def _process_candle(self, candle, sar_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
sar_val = float(sar_val)
self._update_buffers(close, sar_val)
sig_bar = max(0, self._signal_bar.Value)
if self._buffer_count <= sig_bar + 1:
return
vals = self._get_signal_values(sig_bar)
if vals[0] is None or vals[1] is None or vals[2] is None or vals[3] is None:
return
cur_close, cur_sar, prev_close, prev_sar = vals
is_above = cur_close > cur_sar
was_above = prev_close > prev_sar
self._handle_exits(candle, is_above)
crossed_up = not was_above and is_above
crossed_down = was_above and not is_above
if crossed_up and self.Position <= 0:
self._enter_long(candle)
elif crossed_down and self.Position >= 0:
self._enter_short(candle)
def _init_buffers(self):
size = max(2, max(0, self._signal_bar.Value) + 2)
self._close_buffer = [None] * size
self._sar_buffer = [None] * size
self._buffer_index = 0
self._buffer_count = 0
def _update_buffers(self, close, sar):
size = len(self._close_buffer)
if size == 0:
return
self._close_buffer[self._buffer_index] = close
self._sar_buffer[self._buffer_index] = sar
self._buffer_index = (self._buffer_index + 1) % size
if self._buffer_count < size:
self._buffer_count += 1
def _get_signal_values(self, sig_offset):
size = len(self._close_buffer)
if size == 0:
return (None, None, None, None)
cur_idx = (self._buffer_index - 1 - sig_offset + size) % size
prev_idx = (self._buffer_index - 2 - sig_offset + size) % size
return (self._close_buffer[cur_idx], self._sar_buffer[cur_idx],
self._close_buffer[prev_idx], self._sar_buffer[prev_idx])
def _handle_exits(self, candle, is_above):
low = float(candle.LowPrice)
high = float(candle.HighPrice)
if self.Position > 0:
if self._should_exit_by_time(candle):
self.SellMarket()
self._reset_risk()
return
if not is_above:
self.SellMarket()
self._reset_risk()
return
if self._stop_price is not None and low <= self._stop_price:
self.SellMarket()
self._reset_risk()
return
if self._take_price is not None and high >= self._take_price:
self.SellMarket()
self._reset_risk()
elif self.Position < 0:
if self._should_exit_by_time(candle):
self.BuyMarket()
self._reset_risk()
return
if is_above:
self.BuyMarket()
self._reset_risk()
return
if self._stop_price is not None and high >= self._stop_price:
self.BuyMarket()
self._reset_risk()
return
if self._take_price is not None and low <= self._take_price:
self.BuyMarket()
self._reset_risk()
def _should_exit_by_time(self, candle):
if not self._use_time_exit.Value or self._entry_time is None:
return False
mins = max(0, self._holding_minutes.Value)
if mins <= 0:
return False
close_time = candle.CloseTime if candle.CloseTime != candle.CloseTime.__class__() else candle.OpenTime
return (close_time - self._entry_time).TotalMinutes >= mins
def _enter_long(self, candle):
self._reset_risk()
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
close_time = candle.CloseTime if candle.CloseTime != candle.CloseTime.__class__() else candle.OpenTime
self._entry_time = close_time
ps = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps <= 0:
ps = 1.0
close = float(candle.ClosePrice)
sl = self._stop_loss_points.Value
tp = self._take_profit_points.Value
self._stop_price = close - ps * sl if sl > 0 else None
self._take_price = close + ps * tp if tp > 0 else None
def _enter_short(self, candle):
self._reset_risk()
if self.Position > 0:
self.SellMarket()
self.SellMarket()
close_time = candle.CloseTime if candle.CloseTime != candle.CloseTime.__class__() else candle.OpenTime
self._entry_time = close_time
ps = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps <= 0:
ps = 1.0
close = float(candle.ClosePrice)
sl = self._stop_loss_points.Value
tp = self._take_profit_points.Value
self._stop_price = close + ps * sl if sl > 0 else None
self._take_price = close - ps * tp if tp > 0 else None
def _reset_risk(self):
self._stop_price = None
self._take_price = None
self._entry_time = None
def CreateClone(self):
return exp_sar_tm_plus_strategy()