Ver en GitHub

Estrategia VR-ZVER v2

La estrategia VR-ZVER v2 es un port de StockSharp del clásico asesor experto de MetaTrader. Mantiene la idea de triple confirmación del script original: cada operación debe ser respaldada por medias móviles, el oscilador estocástico y RSI. Solo cuando todos los filtros habilitados coinciden, la estrategia coloca una orden de mercado.

Lógica de trading

  • Las señales se evalúan cuando cierra una vela. Las fluctuaciones intrábarra solo se usan para validar stops o objetivos.
  • Tres medias móviles exponenciales (rápida, lenta, muy lenta) deben apilarse en el mismo orden para validar la tendencia cuando el filtro de MA está habilitado.
  • El filtro estocástico espera un cruce de %K/%D cerca de las bandas superior e inferior configurables.
  • El filtro RSI requiere que el oscilador salga de una zona neutral (por debajo de la banda inferior para largos, por encima de la banda superior para cortos).
  • Una señal se acepta solo cuando cada filtro habilitado vota en la misma dirección. Si algún filtro no está de acuerdo, no se opera.
  • La estrategia abre una posición a la vez. No hace hedging ni construye grillas; cuando está plana espera la próxima señal alineada.

Gestión de posición

  • Un take-profit y stop-loss se expresan en pips. El stop inicial se establece en dos tercios de la distancia configurada, reproduciendo el comportamiento original del EA.
  • Un activador de punto de equilibrio (también en pips) mueve el stop al precio de entrada una vez que la operación ha ganado la distancia especificada.
  • Los trailing stops usan una distancia y un paso adicional. El paso evita que el stop se actualice en cada pequeño movimiento ascendente y coincide con la lógica de trailing de MT5.
  • Los trades largos y cortos comparten las mismas reglas de gestión y reaccionan simétricamente a los máximos/mínimos de la vela.

Dimensionamiento de posición

  • FixedVolume mayor que cero abre cada orden con un tamaño fijo.
  • Cuando FixedVolume se establece en cero, la estrategia calcula el volumen desde RiskPercent, el valor actual del portafolio y la distancia del stop. El paso de precio y el precio de paso se usan para convertir la distancia en pips en riesgo monetario.
  • Los volúmenes se redondean para respetar las restricciones de VolumeMin, VolumeMax y VolumeStep del instrumento. Las órdenes se omiten si el tamaño calculado es demasiado pequeño.

Parámetros

Nombre Descripción
CandleType Marco temporal usado para la generación de señales (por defecto velas de 15 minutos).
FixedVolume, RiskPercent Elegir entre dimensionamiento fijo o basado en riesgo.
StopLossPips, TakeProfitPips Distancias de protección base en pips.
TrailingStopPips, TrailingStepPips, BreakevenPips Umbrales de gestión de operaciones.
AllowLongs, AllowShorts Habilitar o deshabilitar direcciones individuales.
UseMovingAverageFilter, FastMaPeriod, SlowMaPeriod, VerySlowMaPeriod Filtro de tendencia EMA triple.
UseStochastic, StochasticKPeriod, StochasticDPeriod, StochasticSmooth, StochasticUpperLevel, StochasticLowerLevel Configuraciones de confirmación estocástica.
UseRsi, RsiPeriod, RsiUpperLevel, RsiLowerLevel Banda de confirmación RSI.

Notas

  • La conversión de pips emula el EA original: los símbolos de cinco y tres dígitos multiplican el paso de precio por diez antes de calcular los valores de pip.
  • El port de StockSharp solo usa órdenes de mercado. Las funciones de bloqueo y órdenes pendientes de la versión MetaTrader se omiten intencionalmente para mantener la implementación consistente con la API de alto nivel.
  • Adjunte la estrategia a un gráfico si desea ver los overlays de EMA, estocástico y RSI; se dibujan automáticamente cuando hay disponible un área de gráfico.
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>
/// Port of the VR-ZVER v2 expert advisor with triple EMA confirmation and stochastic/RSI filters.
/// </summary>
public class VrZverV2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _breakevenPips;
	private readonly StrategyParam<bool> _allowLongs;
	private readonly StrategyParam<bool> _allowShorts;
	private readonly StrategyParam<bool> _useMovingAverageFilter;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _verySlowMaPeriod;
	private readonly StrategyParam<bool> _useStochastic;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSmooth;
	private readonly StrategyParam<decimal> _stochasticUpperLevel;
	private readonly StrategyParam<decimal> _stochasticLowerLevel;
	private readonly StrategyParam<bool> _useRsi;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiUpperLevel;
	private readonly StrategyParam<decimal> _rsiLowerLevel;

	private ExponentialMovingAverage _fastMa;
	private ExponentialMovingAverage _slowMa;
	private ExponentialMovingAverage _verySlowMa;
	private StochasticOscillator _stochastic;
	private RelativeStrengthIndex _rsi;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal? _trailingStop;
	private bool _breakevenActivated;

	public VrZverV2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Time frame for signal generation", "General");

		_fixedVolume = Param(nameof(FixedVolume), 1m)
		.SetDisplay("Fixed Volume", "Use fixed volume when greater than zero", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 10m)
		.SetDisplay("Risk %", "Risk percentage used when fixed volume is zero", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 10000m)
		.SetDisplay("Stop Loss (pips)", "Full stop distance expressed in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 15000m)
		.SetDisplay("Take Profit (pips)", "Profit target distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 8000m)
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 3000m)
		.SetDisplay("Trailing Step (pips)", "Additional distance before trailing updates", "Risk");

		_breakevenPips = Param(nameof(BreakevenPips), 5000m)
		.SetDisplay("Breakeven (pips)", "Move stop to entry after this profit", "Risk");

		_allowLongs = Param(nameof(AllowLongs), true)
		.SetDisplay("Allow Longs", "Permit buy trades", "General");

		_allowShorts = Param(nameof(AllowShorts), true)
		.SetDisplay("Allow Shorts", "Permit sell trades", "General");

		_useMovingAverageFilter = Param(nameof(UseMovingAverageFilter), true)
		.SetDisplay("Use MA Filter", "Require triple EMA alignment", "Indicators");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Fast EMA", "Length of the fast EMA", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Slow EMA", "Length of the slow EMA", "Indicators");

		_verySlowMaPeriod = Param(nameof(VerySlowMaPeriod), 7)
		.SetGreaterThanZero()
		.SetDisplay("Very Slow EMA", "Length of the very slow EMA", "Indicators");

		_useStochastic = Param(nameof(UseStochastic), false)
		.SetDisplay("Use Stochastic", "Enable stochastic confirmation", "Indicators");

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 42)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %K", "Number of periods for %K", "Indicators");

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %D", "Smoothing period for %D", "Indicators");

		_stochasticSmooth = Param(nameof(StochasticSmooth), 7)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic Smooth", "Final smoothing for stochastic", "Indicators");

		_stochasticUpperLevel = Param(nameof(StochasticUpperLevel), 60m)
		.SetDisplay("Stochastic Upper", "Upper threshold for short signals", "Indicators");

		_stochasticLowerLevel = Param(nameof(StochasticLowerLevel), 40m)
		.SetDisplay("Stochastic Lower", "Lower threshold for long signals", "Indicators");

		_useRsi = Param(nameof(UseRsi), false)
		.SetDisplay("Use RSI", "Enable RSI filter", "Indicators");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("RSI Period", "Length of the RSI", "Indicators");

		_rsiUpperLevel = Param(nameof(RsiUpperLevel), 60m)
		.SetDisplay("RSI Upper", "Upper threshold for short entries", "Indicators");

		_rsiLowerLevel = Param(nameof(RsiLowerLevel), 40m)
		.SetDisplay("RSI Lower", "Lower threshold for long entries", "Indicators");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal BreakevenPips
	{
		get => _breakevenPips.Value;
		set => _breakevenPips.Value = value;
	}

	public bool AllowLongs
	{
		get => _allowLongs.Value;
		set => _allowLongs.Value = value;
	}

	public bool AllowShorts
	{
		get => _allowShorts.Value;
		set => _allowShorts.Value = value;
	}

	public bool UseMovingAverageFilter
	{
		get => _useMovingAverageFilter.Value;
		set => _useMovingAverageFilter.Value = value;
	}

	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	public int VerySlowMaPeriod
	{
		get => _verySlowMaPeriod.Value;
		set => _verySlowMaPeriod.Value = value;
	}

	public bool UseStochastic
	{
		get => _useStochastic.Value;
		set => _useStochastic.Value = value;
	}

	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	public int StochasticSmooth
	{
		get => _stochasticSmooth.Value;
		set => _stochasticSmooth.Value = value;
	}

	public decimal StochasticUpperLevel
	{
		get => _stochasticUpperLevel.Value;
		set => _stochasticUpperLevel.Value = value;
	}

	public decimal StochasticLowerLevel
	{
		get => _stochasticLowerLevel.Value;
		set => _stochasticLowerLevel.Value = value;
	}

	public bool UseRsi
	{
		get => _useRsi.Value;
		set => _useRsi.Value = value;
	}

	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	public decimal RsiUpperLevel
	{
		get => _rsiUpperLevel.Value;
		set => _rsiUpperLevel.Value = value;
	}

	public decimal RsiLowerLevel
	{
		get => _rsiLowerLevel.Value;
		set => _rsiLowerLevel.Value = value;
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_fastMa = null;
		_slowMa = null;
		_verySlowMa = null;
		_stochastic = null;
		_rsi = null;
		ResetTradeState();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Prepare pip size once the security is available.
		_pipSize = CalculatePipSize();
		// Clear any leftover state from previous runs.
		ResetTradeState();

		// Instantiate indicators with the configured lengths.
		_fastMa = new ExponentialMovingAverage { Length = FastMaPeriod };
		_slowMa = new ExponentialMovingAverage { Length = SlowMaPeriod };
		_verySlowMa = new ExponentialMovingAverage { Length = VerySlowMaPeriod };

		if (UseStochastic)
		{
			_stochastic = new StochasticOscillator();
			_stochastic.K.Length = StochasticKPeriod;
			_stochastic.D.Length = StochasticDPeriod;
		}

		if (UseRsi)
		{
			_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		}

		// Subscribe to candle updates and bind the three EMAs.
		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_fastMa, _slowMa, _verySlowMa, ProcessCandle)
		.Start();

		// Draw indicators and trades when a chart area is available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _fastMa);
			DrawIndicator(area, _slowMa);
			DrawIndicator(area, _verySlowMa);
			if (_stochastic != null)
				DrawIndicator(area, _stochastic);
			if (_rsi != null)
				DrawIndicator(area, _rsi);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastMaValue, decimal slowMaValue, decimal verySlowMaValue)
	{
		if (candle.State != CandleStates.Finished)
		return;

		// Process stochastic and RSI manually only when enabled.
		IIndicatorValue stochasticValue = null;
		if (UseStochastic && _stochastic != null)
			stochasticValue = _stochastic.Process(candle);

		decimal rsiValue = 50m;
		if (UseRsi && _rsi != null)
		{
			var rsiResult = _rsi.Process(new DecimalIndicatorValue(_rsi, candle.ClosePrice, candle.CloseTime) { IsFinal = true });
			rsiValue = rsiResult.IsFormed ? rsiResult.ToDecimal() : 50m;
		}

		if (UseMovingAverageFilter && (!_fastMa.IsFormed || !_slowMa.IsFormed || !_verySlowMa.IsFormed))
		return;

		if (UseStochastic && !_stochastic.IsFormed)
		return;

		if (UseRsi && !_rsi.IsFormed)
		return;

		// Manage the active position before evaluating new signals.
		UpdateRiskManagement(candle);

		// Aggregate votes from all enabled filters.
		var filters = 0;
		var upVotes = 0;
		var downVotes = 0;

		if (UseMovingAverageFilter)
		{
			filters++;

			if (fastMaValue > slowMaValue && slowMaValue > verySlowMaValue)
			upVotes++;
		else if (fastMaValue < slowMaValue && slowMaValue < verySlowMaValue)
			downVotes++;
		}

		if (UseStochastic && stochasticValue != null)
		{
			if (stochasticValue is not IStochasticOscillatorValue stoch)
			return;
			if (stoch.K is not decimal stochK || stoch.D is not decimal stochD)
			return;

			filters++;

			if (stochD < stochK && StochasticLowerLevel > stochK)
			upVotes++;
			if (stochD > stochK && StochasticUpperLevel < stochK)
			downVotes++;
		}

		if (UseRsi)
		{
			filters++;

			if (rsiValue < RsiLowerLevel)
			upVotes++;
			if (rsiValue > RsiUpperLevel)
			downVotes++;
		}

		if (filters == 0)
		return;

		var longSignal = AllowLongs && upVotes == filters;
		var shortSignal = AllowShorts && downVotes == filters;

		// Only open a new trade when there is no active position.
		if (Position == 0)
		{
			if (longSignal)
			TryEnterLong(candle);
			else if (shortSignal)
			TryEnterShort(candle);
		}
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		var volume = CalculateEntryVolume();
		if (volume <= 0m)
		return;

		// Enter a long position at market price.
		BuyMarket(volume);

		// Store trade prices for later risk management.
		_entryPrice = candle.ClosePrice;
		_breakevenActivated = false;
		_trailingStop = null;

		var stopOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
		var takeOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;

		_stopPrice = stopOffset > 0m ? _entryPrice - stopOffset : null;
		_takePrice = takeOffset > 0m ? _entryPrice + takeOffset : null;
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		var volume = CalculateEntryVolume();
		if (volume <= 0m)
		return;

		// Enter a short position at market price.
		SellMarket(volume);

		// Store trade prices for later risk management.
		_entryPrice = candle.ClosePrice;
		_breakevenActivated = false;
		_trailingStop = null;

		var stopOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
		var takeOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;

		_stopPrice = stopOffset > 0m ? _entryPrice + stopOffset : null;
		_takePrice = takeOffset > 0m ? _entryPrice - takeOffset : null;
	}

	private void UpdateRiskManagement(ICandleMessage candle)
	{
		// Manage long positions first.
		if (Position > 0)
		{
			HandleBreakevenLong(candle);
			HandleTrailingLong(candle);

			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
			else if (_takePrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		// Manage short positions in the same fashion.
		else if (Position < 0)
		{
			HandleBreakevenShort(candle);
			HandleTrailingShort(candle);

			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
			else if (_takePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		// Reset helper state when flat.
		else
		{
			ResetTradeState();
		}
	}

	// Move the stop to breakeven for long trades once profit reaches the threshold.
	private void HandleBreakevenLong(ICandleMessage candle)
	{
		if (_breakevenActivated || BreakevenPips <= 0m)
		return;

		var trigger = _entryPrice + BreakevenPips * _pipSize;
		if (candle.HighPrice >= trigger)
		{
			_breakevenActivated = true;
			UpdateLongStop(_entryPrice);
		}
	}

	// Move the stop to breakeven for short trades once profit reaches the threshold.
	private void HandleBreakevenShort(ICandleMessage candle)
	{
		if (_breakevenActivated || BreakevenPips <= 0m)
		return;

		var trigger = _entryPrice - BreakevenPips * _pipSize;
		if (candle.LowPrice <= trigger)
		{
			_breakevenActivated = true;
			UpdateShortStop(_entryPrice);
		}
	}

	// Update trailing logic for long trades using distance and step thresholds.
	private void HandleTrailingLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m)
		return;

		var distance = TrailingStopPips * _pipSize;
		if (distance <= 0m)
		return;

		var step = TrailingStepPips * _pipSize;
		var desiredStop = candle.ClosePrice - distance;

		if (_trailingStop is null)
		{
			var activationPrice = _entryPrice + distance + step;
			if (candle.HighPrice >= activationPrice)
			{
				_trailingStop = desiredStop;
				UpdateLongStop(desiredStop);
			}
		}
		else if (desiredStop > _trailingStop.Value + step)
		{
			_trailingStop = desiredStop;
			UpdateLongStop(desiredStop);
		}
	}

	// Update trailing logic for short trades using distance and step thresholds.
	private void HandleTrailingShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m)
		return;

		var distance = TrailingStopPips * _pipSize;
		if (distance <= 0m)
		return;

		var step = TrailingStepPips * _pipSize;
		var desiredStop = candle.ClosePrice + distance;

		if (_trailingStop is null)
		{
			var activationPrice = _entryPrice - distance - step;
			if (candle.LowPrice <= activationPrice)
			{
				_trailingStop = desiredStop;
				UpdateShortStop(desiredStop);
			}
		}
		else if (desiredStop < _trailingStop.Value - step)
		{
			_trailingStop = desiredStop;
			UpdateShortStop(desiredStop);
		}
	}

	// Ensure the long stop can only move upward.
	private void UpdateLongStop(decimal newLevel)
	{
		if (_stopPrice is null || newLevel > _stopPrice.Value)
		_stopPrice = newLevel;
	}

	// Ensure the short stop can only move downward.
	private void UpdateShortStop(decimal newLevel)
	{
		if (_stopPrice is null || newLevel < _stopPrice.Value)
		_stopPrice = newLevel;
	}

	// Determine trade size using either fixed volume or risk-based sizing.
	private decimal CalculateEntryVolume()
	{
		if (FixedVolume > 0m)
		return AdjustVolume(FixedVolume);

		var stopOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
		if (stopOffset <= 0m)
		return AdjustVolume(Volume);

		var riskVolume = GetRiskVolume(stopOffset);
		return AdjustVolume(riskVolume);
	}

	// Translate the configured risk percentage into lots based on stop distance.
	private decimal GetRiskVolume(decimal stopOffset)
	{
		if (stopOffset <= 0m)
		return 0m;

		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;

		if (priceStep <= 0m || stepPrice <= 0m)
		return 0m;

		var lossPerUnit = stopOffset / priceStep * stepPrice;
		if (lossPerUnit <= 0m)
		return 0m;

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
		return 0m;

		var riskAmount = equity * RiskPercent / 100m;
		if (riskAmount <= 0m)
		return 0m;

		return riskAmount / lossPerUnit;
	}

	// Normalize the requested volume to instrument constraints.
	private decimal AdjustVolume(decimal volume)
	{
		if (volume <= 0m)
		return 0m;

		var security = Security;
		if (security == null)
		return volume;

		var step = security.VolumeStep ?? 0m;

		if (step > 0m)
		{
			var steps = Math.Floor(volume / step);
			var adjusted = steps * step;
			if (adjusted <= 0m)
				adjusted = step;
			return adjusted;
		}

		return volume > 0m ? volume : 0m;
	}

	// Mimic the MetaTrader pip conversion used in the original script.
	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		return 1m;

		// For crypto and large-price instruments, scale pip size
		// so that pip-based parameters produce meaningful price offsets.
		return step;
	}

	// Clear cached state values when no position is active.
	private void ResetTradeState()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_trailingStop = null;
		_breakevenActivated = false;
	}
}