Стратегия SAR Trading v2.0
SAR Trading v2.0 — адаптация классического советника Cronex для платформы StockSharp. Стратегия использует простую скользящую среднюю (SMA) и Parabolic SAR для определения направления сделки, а также управляет позицией с помощью фиксированных стопов и трейлинг-стопа, измеряемого в пунктах.
- Индикаторы: Simple Moving Average, Parabolic SAR.
- Таймфрейм по умолчанию: 15-минутные свечи (можно изменить через параметр
CandleType). - Рынок: любой инструмент, у которого задан корректный
PriceStep.
Логика торговли
- Входы рассматриваются только при отсутствии открытой позиции.
- Лонг: либо значение Parabolic SAR находится ниже SMA, либо цена закрытия
MaShiftсвечей назад ниже SMA. Это повторяет условиеSAR < MA || Close[shift] < MAиз оригинального кода. - Шорт: либо значение Parabolic SAR выше SMA, либо цена закрытия
MaShiftсвечей назад выше SMA. - После отправки приказа на выход стратегия ждёт, пока позиция полностью закроется, прежде чем анализировать новые сигналы, что обеспечивает работу только с одной позицией, как и в MQL-версии.
Управление рисками
StopLossPipsиTakeProfitPipsпреобразуют пункты в абсолютное расстояние по цене черезSecurity.PriceStep.TrailingStopPipsудерживает защитный стоп на фиксированном расстоянии в пунктах после выхода позиции в прибыль.TrailingStepPipsтребует дополнительного движения цены прежде, чем подтянуть трейлинг-стоп, полностью повторяя логику «шага» из MQL.- При достижении уровней стоп-лосса или тейк-профита позиция закрывается рыночной заявкой.
Параметры
MaPeriod(значение по умолчанию 18) — период SMA.MaShift(по умолчанию 2) — сколько свечей назад брать цену закрытия для сравнения со SMA.SarStep(по умолчанию 0.02) — коэффициент ускорения Parabolic SAR.SarMaxStep(по умолчанию 0.2) — максимальный коэффициент ускорения Parabolic SAR.StopLossPips(по умолчанию 50) — фиксированный стоп-лосс в пунктах.TakeProfitPips(по умолчанию 50) — фиксированный тейк-профит в пунктах.TrailingStopPips(по умолчанию 15) — расстояние трейлинг-стопа в пунктах.TrailingStepPips(по умолчанию 5) — дополнительное изменение цены для переноса трейлинг-стопа.CandleType— тип свечей, используемых в расчётах.
Дополнительные замечания
- Стратегия хранит внутреннюю историю закрытий, чтобы воспроизвести вызов
iClose(shift)из MQL. - Все решения принимаются только по завершённым свечам, что обеспечивает совпадение с поведением оригинального советника.
- Объём сделки берётся из свойства
Volume; по умолчанию каждое срабатывание открывает позицию объёмом 1.
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 shifted SMA trend strategy inspired by the original MQL5 version.
/// Opens long positions when SAR or the shifted close confirm bullish alignment.
/// Opens short positions when SAR or the shifted close confirm bearish alignment.
/// Includes configurable fixed stops, take profit and trailing stop with step filter.
/// </summary>
public class SarTradingV20Strategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _maShift;
private readonly StrategyParam<decimal> _sarStep;
private readonly StrategyParam<decimal> _sarMaxStep;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _ma = null!;
private ParabolicSar _parabolicSar = null!;
private readonly List<decimal> _closeHistory = new();
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takeProfitPrice;
private decimal _pipSize;
private bool _exitPending;
/// <summary>
/// SMA length.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Number of bars to shift the close comparison.
/// </summary>
public int MaShift
{
get => _maShift.Value;
set => _maShift.Value = value;
}
/// <summary>
/// Parabolic SAR acceleration factor.
/// </summary>
public decimal SarStep
{
get => _sarStep.Value;
set => _sarStep.Value = value;
}
/// <summary>
/// Parabolic SAR maximum acceleration factor.
/// </summary>
public decimal SarMaxStep
{
get => _sarMaxStep.Value;
set => _sarMaxStep.Value = value;
}
/// <summary>
/// Stop-loss size in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit size in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum advance before the trailing stop moves.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize parameters for the strategy.
/// </summary>
public SarTradingV20Strategy()
{
_maPeriod = Param(nameof(MaPeriod), 18)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Number of bars for the simple moving average.", "Indicators")
;
_maShift = Param(nameof(MaShift), 2)
.SetNotNegative()
.SetDisplay("MA Shift", "Bars to shift the close comparison against the SMA.", "Indicators");
_sarStep = Param(nameof(SarStep), 0.02m)
.SetGreaterThanZero()
.SetDisplay("SAR Step", "Acceleration factor for Parabolic SAR.", "Indicators")
;
_sarMaxStep = Param(nameof(SarMaxStep), 0.2m)
.SetGreaterThanZero()
.SetDisplay("SAR Max", "Maximum acceleration factor for Parabolic SAR.", "Indicators")
;
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Fixed stop-loss distance expressed in pips.", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Fixed take-profit distance expressed in pips.", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 15)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips.", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Additional profit before trailing stop moves.", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Candle type subscribed for processing.", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ma = null!;
_parabolicSar = null!;
_closeHistory.Clear();
ResetPositionState();
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = Security?.PriceStep ?? 0m;
if (_pipSize <= 0m)
{
// Fallback to a default pip size when the security does not provide one.
_pipSize = 0.0001m;
}
_ma = new SimpleMovingAverage { Length = MaPeriod };
_parabolicSar = new ParabolicSar
{
Acceleration = SarStep,
AccelerationMax = SarMaxStep
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ma, _parabolicSar, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ma);
DrawIndicator(area, _parabolicSar);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal sarValue)
{
// Only react on completed candles to mirror the original expert behavior.
if (candle.State != CandleStates.Finished)
return;
// Keep a sliding window of closes for shifted comparisons.
UpdateCloseHistory(candle.ClosePrice);
// Clear pending state when the previous exit was filled.
if (_exitPending && Position == 0)
{
ResetPositionState();
}
// Manage the active position before looking for new entries.
if (Position != 0)
{
ManageExistingPosition(candle);
return;
}
if (!_ma.IsFormed || !_parabolicSar.IsFormed)
return;
if (_closeHistory.Count <= MaShift)
return;
var shiftedClose = _closeHistory[_closeHistory.Count - 1 - MaShift];
var sarBelowMa = sarValue < maValue;
var sarAboveMa = sarValue > maValue;
var closeBelowMa = shiftedClose < maValue;
var closeAboveMa = shiftedClose > maValue;
if (sarBelowMa || closeBelowMa)
{
OpenLong(candle.ClosePrice);
}
else if (sarAboveMa || closeAboveMa)
{
OpenShort(candle.ClosePrice);
}
}
private void ManageExistingPosition(ICandleMessage candle)
{
if (_exitPending)
return;
if (_entryPrice == null)
return;
if (Position > 0)
{
UpdateTrailingForLong(candle);
TryExitLong(candle);
}
else if (Position < 0)
{
UpdateTrailingForShort(candle);
TryExitShort(candle);
}
}
private void UpdateTrailingForLong(ICandleMessage candle)
{
if (TrailingStopPips <= 0 || _entryPrice == null)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var profit = candle.ClosePrice - _entryPrice.Value;
// Move the stop only after price advanced by trailing distance plus the configured step.
if (profit <= trailingDistance + trailingStep)
return;
var candidate = candle.ClosePrice - trailingDistance;
var minIncrease = TrailingStepPips > 0 ? trailingStep : 0m;
if (_stopPrice == null || candidate > _stopPrice.Value + minIncrease)
{
_stopPrice = candidate;
// trailing stop updated
}
}
private void UpdateTrailingForShort(ICandleMessage candle)
{
if (TrailingStopPips <= 0 || _entryPrice == null)
return;
var trailingDistance = TrailingStopPips * _pipSize;
var trailingStep = TrailingStepPips * _pipSize;
var profit = _entryPrice.Value - candle.ClosePrice;
// Move the stop only after price advanced by trailing distance plus the configured step.
if (profit <= trailingDistance + trailingStep)
return;
var candidate = candle.ClosePrice + trailingDistance;
var minDecrease = TrailingStepPips > 0 ? trailingStep : 0m;
if (_stopPrice == null || candidate < _stopPrice.Value - minDecrease)
{
_stopPrice = candidate;
// trailing stop updated
}
}
private void TryExitLong(ICandleMessage candle)
{
var position = Math.Abs(Position);
if (position <= 0)
return;
if (_stopPrice != null && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(position);
_exitPending = true;
// exit long via stop
return;
}
if (_takeProfitPrice != null && candle.HighPrice >= _takeProfitPrice.Value)
{
SellMarket(position);
_exitPending = true;
// exit long via take profit
}
}
private void TryExitShort(ICandleMessage candle)
{
var position = Math.Abs(Position);
if (position <= 0)
return;
if (_stopPrice != null && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(position);
_exitPending = true;
// exit short via stop
return;
}
if (_takeProfitPrice != null && candle.LowPrice <= _takeProfitPrice.Value)
{
BuyMarket(position);
_exitPending = true;
// exit short via take profit
}
}
private void OpenLong(decimal price)
{
var volume = Volume;
if (volume <= 0)
return;
BuyMarket(volume);
InitializePositionState(price, true);
}
private void OpenShort(decimal price)
{
var volume = Volume;
if (volume <= 0)
return;
SellMarket(volume);
InitializePositionState(price, false);
}
private void InitializePositionState(decimal entryPrice, bool isLong)
{
_entryPrice = entryPrice;
_exitPending = false;
var pip = _pipSize > 0m ? _pipSize : 0.0001m;
_stopPrice = StopLossPips > 0
? isLong
? entryPrice - StopLossPips * pip
: entryPrice + StopLossPips * pip
: null;
_takeProfitPrice = TakeProfitPips > 0
? isLong
? entryPrice + TakeProfitPips * pip
: entryPrice - TakeProfitPips * pip
: null;
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
_takeProfitPrice = null;
_exitPending = false;
}
private void UpdateCloseHistory(decimal closePrice)
{
_closeHistory.Add(closePrice);
var maxCount = Math.Max(MaShift + 1, 1);
if (_closeHistory.Count > maxCount)
{
_closeHistory.RemoveRange(0, _closeHistory.Count - maxCount);
}
}
}
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 SimpleMovingAverage, ParabolicSar
from StockSharp.Algo.Strategies import Strategy
class sar_trading_v20_strategy(Strategy):
def __init__(self):
super(sar_trading_v20_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 18)
self._ma_shift = self.Param("MaShift", 2)
self._sar_step = self.Param("SarStep", 0.02)
self._sar_max_step = self.Param("SarMaxStep", 0.2)
self._stop_loss_pips = self.Param("StopLossPips", 50)
self._take_profit_pips = self.Param("TakeProfitPips", 50)
self._trailing_stop_pips = self.Param("TrailingStopPips", 15)
self._trailing_step_pips = self.Param("TrailingStepPips", 5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._ma = None
self._sar = None
self._close_history = []
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._pip_size = 0.0
self._exit_pending = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def MaPeriod(self):
return self._ma_period.Value
@property
def MaShift(self):
return self._ma_shift.Value
@property
def SarStep(self):
return self._sar_step.Value
@property
def SarMaxStep(self):
return self._sar_max_step.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def TrailingStopPips(self):
return self._trailing_stop_pips.Value
@property
def TrailingStepPips(self):
return self._trailing_step_pips.Value
def OnStarted2(self, time):
super(sar_trading_v20_strategy, self).OnStarted2(time)
sec = self.Security
ps = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 0.0
self._pip_size = ps if ps > 0 else 0.0001
self._ma = SimpleMovingAverage()
self._ma.Length = self.MaPeriod
self._sar = ParabolicSar()
self._sar.Acceleration = self.SarStep
self._sar.AccelerationMax = self.SarMaxStep
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._ma, self._sar, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ma)
self.DrawIndicator(area, self._sar)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ma_value, sar_value):
if candle.State != CandleStates.Finished:
return
self._update_close_history(float(candle.ClosePrice))
if self._exit_pending and self.Position == 0:
self._reset_position_state()
if self.Position != 0:
self._manage_existing_position(candle)
return
if not self._ma.IsFormed or not self._sar.IsFormed:
return
if len(self._close_history) <= self.MaShift:
return
shifted_close = self._close_history[len(self._close_history) - 1 - self.MaShift]
ma_v = float(ma_value)
sar_v = float(sar_value)
sar_below_ma = sar_v < ma_v
sar_above_ma = sar_v > ma_v
close_below_ma = shifted_close < ma_v
close_above_ma = shifted_close > ma_v
if sar_below_ma or close_below_ma:
self._open_long(float(candle.ClosePrice))
elif sar_above_ma or close_above_ma:
self._open_short(float(candle.ClosePrice))
def _manage_existing_position(self, candle):
if self._exit_pending:
return
if self._entry_price is None:
return
if self.Position > 0:
self._update_trailing_for_long(candle)
self._try_exit_long(candle)
elif self.Position < 0:
self._update_trailing_for_short(candle)
self._try_exit_short(candle)
def _update_trailing_for_long(self, candle):
if self.TrailingStopPips <= 0 or self._entry_price is None:
return
trail_dist = self.TrailingStopPips * self._pip_size
trail_step = self.TrailingStepPips * self._pip_size
profit = float(candle.ClosePrice) - self._entry_price
if profit <= trail_dist + trail_step:
return
candidate = float(candle.ClosePrice) - trail_dist
min_inc = trail_step if self.TrailingStepPips > 0 else 0
if self._stop_price is None or candidate > self._stop_price + min_inc:
self._stop_price = candidate
def _update_trailing_for_short(self, candle):
if self.TrailingStopPips <= 0 or self._entry_price is None:
return
trail_dist = self.TrailingStopPips * self._pip_size
trail_step = self.TrailingStepPips * self._pip_size
profit = self._entry_price - float(candle.ClosePrice)
if profit <= trail_dist + trail_step:
return
candidate = float(candle.ClosePrice) + trail_dist
min_dec = trail_step if self.TrailingStepPips > 0 else 0
if self._stop_price is None or candidate < self._stop_price - min_dec:
self._stop_price = candidate
def _try_exit_long(self, candle):
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket()
self._exit_pending = True
return
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket()
self._exit_pending = True
def _try_exit_short(self, candle):
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket()
self._exit_pending = True
return
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket()
self._exit_pending = True
def _open_long(self, price):
self.BuyMarket()
self._init_position_state(price, True)
def _open_short(self, price):
self.SellMarket()
self._init_position_state(price, False)
def _init_position_state(self, entry_price, is_long):
self._entry_price = entry_price
self._exit_pending = False
pip = self._pip_size if self._pip_size > 0 else 0.0001
if self.StopLossPips > 0:
self._stop_price = entry_price - self.StopLossPips * pip if is_long else entry_price + self.StopLossPips * pip
else:
self._stop_price = None
if self.TakeProfitPips > 0:
self._take_profit_price = entry_price + self.TakeProfitPips * pip if is_long else entry_price - self.TakeProfitPips * pip
else:
self._take_profit_price = None
def _reset_position_state(self):
self._entry_price = None
self._stop_price = None
self._take_profit_price = None
self._exit_pending = False
def _update_close_history(self, close_price):
self._close_history.append(close_price)
max_count = max(self.MaShift + 1, 1)
if len(self._close_history) > max_count:
self._close_history = self._close_history[-max_count:]
def OnReseted(self):
super(sar_trading_v20_strategy, self).OnReseted()
self._ma = None
self._sar = None
self._close_history = []
self._reset_position_state()
self._pip_size = 0.0
def CreateClone(self):
return sar_trading_v20_strategy()