Открыть на GitHub

Стратегия Parabolic SAR First Dot

Обзор

Parabolic SAR First Dot Strategy — это конвертация MetaTrader-советника pSAR_bug_4 из папки MQL/9954 на высокоуровневый API StockSharp. Система реагирует на самый первый разворот Parabolic SAR на противоположную сторону цены. Когда точки SAR перемещаются под цену закрытия, открывается длинная позиция; если точки оказываются над закрытием, открывается короткая сделка. Каждая позиция защищена фиксированными стоп-лоссом и тейк-профитом, выраженными в «пунктах» Parabolic SAR по аналогии с оригиналом.

Логика торговли

  1. Подготовка данных и индикатора. Стратегия подписывается на настраиваемый тип свечей (по умолчанию 15-минутные) и привязывает индикатор Parabolic SAR с заданными коэффициентами ускорения.
  2. Отслеживание состояния. На первой завершённой свече фиксируется, находится ли SAR выше или ниже закрытия. На последующих свечах новое положение сравнивается с предыдущим.
  3. Правила входа.
    • Покупка: SAR переходит сверху цены закрытия вниз. Любая открытая короткая позиция закрывается, затем отправляется рыночная заявка на покупку с заданным объёмом.
    • Продажа: SAR переходит снизу вверх. Любая длинная позиция закрывается, после чего открывается короткая позиция.
  4. Защитные уровни. После входа рассчитываются уровни стоп-лосса и тейк-профита на основе цены закрытия свечи. Расстояние берётся как StopLossPoints или TakeProfitPoints, умноженные на шаг цены инструмента (PriceStep). Если параметр UseStopMultiplier активен (поведение MetaTrader по умолчанию), расстояние дополнительно умножается на 10 для учёта дробных пунктов.
  5. Правила выхода. На каждой завершённой свече стратегия проверяет максимум и минимум. При пробое стоп-лосса или тейк-профита позиция закрывается рыночной заявкой. При появлении противоположного сигнала SAR выполняется разворот — объём ордера выбирается так, чтобы закрыть текущую позицию и открыть новую в противоположную сторону.

Управление рисками

  • Для каждой новой позиции пересчитываются уровни защитных приказов.
  • Если инструмент не предоставляет шаг цены, используется консервативное значение 0.0001, чтобы избежать нулевых расстояний.
  • Перед любым действием вызывается IsFormedAndOnlineAndAllowTrading(), что гарантирует готовность подписок и разрешение на торговлю.

Параметры

Название Значение по умолчанию Описание
TradeVolume 0.1 Объём заявки для новых позиций, синхронизируется со свойством Strategy.Volume.
StopLossPoints 90 Дистанция стоп-лосса в пунктах Parabolic SAR, переводится в цену через PriceStep (и умножается на 10 при активном UseStopMultiplier).
TakeProfitPoints 20 Дистанция тейк-профита в пунктах Parabolic SAR.
UseStopMultiplier true При включении умножает расстояние до стопа и тейка на 10, воспроизводя переключатель StopMult оригинального советника.
SarAccelerationStep 0.02 Начальный коэффициент ускорения для Parabolic SAR.
SarAccelerationMax 0.2 Максимальный коэффициент ускорения индикатора Parabolic SAR.
CandleType 15-минутный таймфрейм Тип свечей, используемый для расчётов и сигналов.

Особенности конвертации

  • В MetaTrader стоп-лосс и тейк-профит выставлялись брокеру. В StockSharp они воспроизводятся за счёт мониторинга максимумов и минимумов свечей и отправки рыночных заявок при достижении порога.
  • Перемножение расстояний на 10 при активном StopMult сохранено параметром UseStopMultiplier, что важно для брокеров с дробными пунктами.
  • Используется только высокоуровневый API (SubscribeCandles, Bind, BuyMarket, SellMarket), как того требуют правила проекта. Python-версия специально не создавалась согласно задаче.
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 first dot reversal strategy.
/// Opens a position when Parabolic SAR flips relative to the close and protects it with classic stops.
/// </summary>
public class ParabolicSarFirstDotStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _useStopMultiplier;
	private readonly StrategyParam<decimal> _sarAccelerationStep;
	private readonly StrategyParam<decimal> _sarAccelerationMax;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;
	private bool? _prevIsSarAbovePrice;
	private decimal _priceStep;
	private DateTimeOffset _lastTradeTime;

	/// <summary>
	/// Trading volume in lots.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Stop-loss distance expressed in Parabolic SAR points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in Parabolic SAR points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Multiply stop distances by ten to mirror the MetaTrader implementation.
	/// </summary>
	public bool UseStopMultiplier
	{
		get => _useStopMultiplier.Value;
		set => _useStopMultiplier.Value = value;
	}

	/// <summary>
	/// Initial acceleration factor for Parabolic SAR.
	/// </summary>
	public decimal SarAccelerationStep
	{
		get => _sarAccelerationStep.Value;
		set => _sarAccelerationStep.Value = value;
	}

	/// <summary>
	/// Maximum acceleration factor for Parabolic SAR.
	/// </summary>
	public decimal SarAccelerationMax
	{
		get => _sarAccelerationMax.Value;
		set => _sarAccelerationMax.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ParabolicSarFirstDotStrategy"/>.
	/// </summary>
	public ParabolicSarFirstDotStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Volume", "Order volume in lots", "General")
			.SetGreaterThanZero();

		_stopLossPoints = Param(nameof(StopLossPoints), 90)
			.SetDisplay("Stop-Loss Points", "Stop-loss distance converted through the instrument price step", "Risk Management")
			.SetGreaterThanZero()
			
			.SetOptimize(30, 150, 10);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20)
			.SetDisplay("Take-Profit Points", "Take-profit distance converted through the instrument price step", "Risk Management")
			.SetGreaterThanZero()
			
			.SetOptimize(10, 80, 10);

		_useStopMultiplier = Param(nameof(UseStopMultiplier), true)
			.SetDisplay("Use Stop Multiplier", "Multiply distances by ten to reproduce MetaTrader stop handling", "Risk Management");

		_sarAccelerationStep = Param(nameof(SarAccelerationStep), 0.02m)
			.SetDisplay("SAR Step", "Initial acceleration factor for Parabolic SAR", "Indicator")
			.SetRange(0.01m, 0.05m)
			
			.SetOptimize(0.01m, 0.05m, 0.01m);

		_sarAccelerationMax = Param(nameof(SarAccelerationMax), 0.2m)
			.SetDisplay("SAR Max", "Maximum acceleration factor for Parabolic SAR", "Indicator")
			.SetRange(0.1m, 0.4m)
			
			.SetOptimize(0.1m, 0.4m, 0.05m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for calculations", "General");

		Volume = _tradeVolume.Value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevIsSarAbovePrice = null;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		Volume = _tradeVolume.Value;
		_priceStep = 0;
		_lastTradeTime = default;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_priceStep = GetPriceStep();

		var parabolicSar = new ParabolicSar
		{
			Acceleration = SarAccelerationStep,
			AccelerationMax = SarAccelerationMax
		};

		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)
	{
		// Work only with completed candles to match MetaTrader logic.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait for Parabolic SAR indicator to be ready.

		// Check whether existing positions should be closed by protective levels.
		CheckProtectiveLevels(candle);

		var isSarAbovePrice = sarValue > candle.ClosePrice;

		// Initialize state on the first value.
		if (_prevIsSarAbovePrice == null)
		{
			_prevIsSarAbovePrice = isSarAbovePrice;
			return;
		}

		var sarSwitchedBelow = _prevIsSarAbovePrice.Value && !isSarAbovePrice;
		var sarSwitchedAbove = !_prevIsSarAbovePrice.Value && isSarAbovePrice;

		if (sarSwitchedBelow)
			TryEnterLong(candle, sarValue);
		else if (sarSwitchedAbove)
			TryEnterShort(candle, sarValue);

		_prevIsSarAbovePrice = isSarAbovePrice;
	}

	private void TryEnterLong(ICandleMessage candle, decimal sarValue)
	{
		// Prevent duplicate long entries.
		if (Position > 0m)
			return;

		// Cooldown: at least 2 days between trades to avoid over-trading.
		if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_lastTradeTime = candle.OpenTime;

		var entryPrice = candle.ClosePrice;
		var stopDistance = GetDistance(StopLossPoints);
		var takeDistance = GetDistance(TakeProfitPoints);

		_longStop = entryPrice - stopDistance;
		_longTake = entryPrice + takeDistance;
		_shortStop = null;
		_shortTake = null;

		LogInfo($"Long entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_longStop}, Take={_longTake}");
	}

	private void TryEnterShort(ICandleMessage candle, decimal sarValue)
	{
		// Prevent duplicate short entries.
		if (Position < 0m)
			return;

		// Cooldown: at least 2 days between trades to avoid over-trading.
		if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_lastTradeTime = candle.OpenTime;

		var entryPrice = candle.ClosePrice;
		var stopDistance = GetDistance(StopLossPoints);
		var takeDistance = GetDistance(TakeProfitPoints);

		_shortStop = entryPrice + stopDistance;
		_shortTake = entryPrice - takeDistance;
		_longStop = null;
		_longTake = null;

		LogInfo($"Short entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_shortStop}, Take={_shortTake}");
	}

	private void CheckProtectiveLevels(ICandleMessage candle)
	{
		var position = Position;

		if (position > 0m)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(position));
				LogInfo($"Long stop-loss triggered at {stop}.");
				ResetLongTargets();
			}
			else if (_longTake is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(position));
				LogInfo($"Long take-profit triggered at {take}.");
				ResetLongTargets();
			}
		}
		else if (position < 0m)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(position));
				LogInfo($"Short stop-loss triggered at {stop}.");
				ResetShortTargets();
			}
			else if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(position));
				LogInfo($"Short take-profit triggered at {take}.");
				ResetShortTargets();
			}
		}
	}

	private void ResetLongTargets()
	{
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortTargets()
	{
		_shortStop = null;
		_shortTake = null;
	}

	private decimal GetDistance(int basePoints)
	{
		var multiplier = UseStopMultiplier ? 10 : 1;
		return basePoints * multiplier * _priceStep;
	}

	private decimal GetPriceStep()
	{
		// Use security price step when available, otherwise fall back to a minimal tick.
		var step = Security?.PriceStep ?? 0.0001m;
		return step > 0m ? step : 0.0001m;
	}
}