Ver en GitHub

Parabolic SAR Estrategia del primer punto

Descripción general

La Parabolic SAR estrategia del primer punto es la StockSharp conversión de alto nivel del MetaTrader asesor experto pSAR_bug_4 de la carpeta MQL/9954. El sistema reacciona al primer punto del Parabolic SAR que aparece en el lado opuesto del precio. Cuando el SAR cae por debajo del cierre, se abre una operación larga; cuando el SAR salta por encima del cierre, se ejecuta una operación corta. Cada posición está protegida con distancias fijas de stop-loss y take-profit expresadas en Parabolic SAR "puntos", al igual que en la versión original MQL.

Lógica de trading

  1. Preparación de datos e indicadores. La estrategia se suscribe a un tipo de vela configurable (velas de 15 minutos de forma predeterminada) y vincula un indicador Parabolic SAR con un paso de aceleración definido por el usuario y una aceleración máxima.
  2. Seguimiento del estado. En la primera vela completa, la estrategia recuerda si SAR está por encima o por debajo del cierre. Las velas posteriores comparan la nueva posición SAR con el estado anterior.
  3. Reglas de entrada.
    • Entrada larga: el SAR cambia desde arriba del cierre hasta debajo del cierre. Cualquier posición corta existente se cierra y se abre en el mercado una nueva posición larga con el volumen configurado.
    • Entrada corta: el SAR cambia de debajo del cierre a encima del cierre. Cualquier posición larga existente se cierra antes de abrir una nueva posición corta.
  4. Órdenes de protección. Inmediatamente después de la entrada, la estrategia almacena los niveles de stop-loss y take-profit calculados a partir del cierre de la vela multiplicando StopLossPoints o TakeProfitPoints por el valor PriceStep. Si UseStopMultiplier está habilitado (comportamiento predeterminado copiado de MetaTrader), la distancia se multiplica por 10 para tener en cuenta los corredores que cotizan con pips fraccionarios.
  5. Reglas de salida. En cada vela terminada, la estrategia compara el máximo y el mínimo con los niveles almacenados de stop-loss y take-profit. Si el máximo o el mínimo superan el nivel, la posición se cierra en el mercado. Cuando llega una señal SAR opuesta, la posición también se invierte enviando una orden de tamaño para nivelar la exposición actual y abrir la nueva operación.

Gestión del riesgo

  • Las distancias de stop-loss y take-profit se recalculan para cada nueva posición.
  • El código realiza un respaldo conservador: cuando el valor no proporciona un incremento de precio, se utiliza un valor de 0.0001 para evitar distancias cero.
  • Todas las decisiones comerciales utilizan el asistente IsFormedAndOnlineAndAllowTrading() para garantizar que la suscripción esté activa y activa.

Parámetros

Nombre Predeterminado Descripción
TradeVolume 0.1 Volumen de pedidos utilizado para nuevas posiciones. El parámetro también actualiza la propiedad base Strategy.Volume.
StopLossPoints 90 Distancia de stop-loss expresada en Parabolic SAR puntos. El valor se multiplica por la seguridad PriceStep (y ​​opcionalmente por 10 cuando UseStopMultiplier es verdadero).
TakeProfitPoints 20 Distancia de obtención de beneficios en Parabolic SAR puntos convertidos a través del paso de precio.
UseStopMultiplier true Si está habilitado, multiplica las distancias de stop-loss y take-profit por 10 para imitar el cambio StopMult del experto MetaTrader.
SarAccelerationStep 0.02 Factor de aceleración inicial suministrado al indicador Parabolic SAR.
SarAccelerationMax 0.2 Factor de aceleración máximo para el indicador Parabolic SAR.
CandleType 15m time-frame Tipo de vela utilizado para los cálculos de indicadores y señales.

Notas sobre la conversión

  • MetaTrader las órdenes de limitación de pérdidas y toma de ganancias eran órdenes de protección del lado del corredor. StockSharp los reproduce monitoreando los máximos y mínimos de las velas y enviando salidas del mercado cuando se cruzan los umbrales.
  • El experto MetaTrader multiplicó las distancias de parada por diez siempre que StopMult fuera cierto para mejorar la compatibilidad con los corredores que cotizan con pips fraccionarios. El parámetro UseStopMultiplier implementa el mismo comportamiento.
  • La conversión utiliza API de alto nivel de StockSharp (SubscribeCandles, Bind, BuyMarket, SellMarket) según lo exigen las pautas del proyecto. Aún no se proporciona ninguna versión adicional de Python que coincida con la solicitud de la tarea.
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;
	}
}