Estrategia SAR Trading v2.0
La Estrategia SAR Trading v2.0 recrea el clásico asesor experto Cronex dentro de la API de alto nivel de StockSharp. Combina una media móvil simple (SMA) con el Parabolic SAR para cronometrar las entradas y luego gestiona la posición con órdenes de protección fijas y un trailing stop basado en pips.
- Indicadores: Media Móvil Simple, Parabolic SAR.
- Marco temporal predeterminado: velas de 15 minutos (configurable a través de
CandleType). - Mercado: cualquier instrumento que proporcione un valor de
PriceStep(pip) significativo.
Lógica de trading
- La estrategia solo evalúa entradas cuando no hay posición abierta.
- Configuración larga: o bien el valor del Parabolic SAR cae por debajo de la SMA o el precio de cierre de
MaShiftbarras atrás está por debajo de la SMA. Esto refleja la regla MQLSAR < MA || Close[shift] < MA. - Configuración corta: o bien el valor del Parabolic SAR sube por encima de la SMA o el cierre de
MaShiftbarras atrás está por encima de la SMA. - Después de enviar una orden de salida, el algoritmo espera hasta que la posición esté plana antes de considerar nuevas señales, coincidiendo con el comportamiento de posición única del EA original.
Gestión de riesgo
StopLossPipsyTakeProfitPipsconvierten pips en distancias de precio absolutas usandoSecurity.PriceStep.TrailingStopPipsmantiene el stop de protección a una distancia de pips fija detrás del precio una vez que la operación está en beneficio.TrailingStepPipsexige un buffer adicional de pips antes de mover el trailing stop de nuevo, emulando la lógica de "paso de trailing" del código MQL.- Si el mercado alcanza los niveles de stop-loss o take-profit, la posición se cierra a mercado.
Parámetros
MaPeriod(predeterminado 18): número de barras usadas por la SMA.MaShift(predeterminado 2): cuántas barras atrás leer el precio de cierre al comparar con la SMA.SarStep(predeterminado 0.02): factor de aceleración del Parabolic SAR.SarMaxStep(predeterminado 0.2): factor máximo de aceleración del Parabolic SAR.StopLossPips(predeterminado 50): distancia del stop-loss fijo en pips.TakeProfitPips(predeterminado 50): distancia del take-profit fijo en pips.TrailingStopPips(predeterminado 15): distancia del trailing stop en pips.TrailingStepPips(predeterminado 5): ganancia adicional en pips requerida antes de que el trailing stop se mueva de nuevo.CandleType: suscripción de vela usada para los cálculos.
Notas adicionales
- La estrategia mantiene un historial interno de cierres para reproducir la llamada
iClose(shift)usada en la versión MQL. - Se basa únicamente en velas terminadas para las decisiones, asegurando consistencia con el asesor experto original.
- El volumen se toma de la propiedad
Volumede la estrategia; por defecto cada señal envía una orden de mercado de un lote.
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()