Открыть на GitHub

Стратегия 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);
		}
	}
}