在 GitHub 上查看

SAR Trading v2.0 策略

SAR Trading v2.0 将 Cronex 的经典 MQL5 策略迁移到 StockSharp 高阶 API。策略结合简单移动平均线 (SMA) 与 Parabolic SAR,在出现方向信号时入场,并通过固定止损、止盈以及基于点差的跟踪止损管理持仓。

  • 指标:Simple Moving Average、Parabolic SAR。
  • 默认周期:15 分钟 K 线(可通过 CandleType 调整)。
  • 适用市场:任意提供有效 PriceStep(最小报价步长)的品种。

交易逻辑

  • 只有在仓位为空时才会评估入场信号。
  • 做多: 当 Parabolic SAR 跌破 SMA,或 MaShift 根之前的收盘价低于 SMA 时入场,多头条件对应原始代码的 SAR < MA || Close[shift] < MA 判断。
  • 做空: 当 Parabolic SAR 升至 SMA 之上,或 MaShift 根之前的收盘价高于 SMA 时入场。
  • 发出离场委托后,策略会等待仓位归零,再处理新的信号,从而保持一次仅持有一个方向的仓位,与原 EA 一致。

风险控制

  • StopLossPipsTakeProfitPips 使用 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:用于计算的 K 线类型。

其他说明

  • 策略在内部保存收盘价历史,以复现 MQL 中的 iClose(shift) 调用。
  • 所有决策均基于已完成的 K 线,确保与原始专家顾问的行为一致。
  • 下单数量来源于策略的 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);
		}
	}
}