Auf GitHub ansehen

VR-ZVER v2 Strategie

Die VR-ZVER v2 Strategie ist ein StockSharp-Port des klassischen MetaTrader Expert Advisors. Sie behält die Dreifach-Bestätigungsidee des ursprünglichen Skripts bei: Jeder Trade muss von gleitenden Durchschnitten, dem Stochastik-Oszillator und RSI unterstützt werden. Nur wenn alle aktivierten Filter übereinstimmen, platziert die Strategie eine Marktorder.

Handelslogik

  • Signale werden ausgewertet, wenn eine Kerze schließt. Intrabar-Schwankungen werden nur zur Validierung von Stops oder Zielen verwendet.
  • Drei exponentielle gleitende Durchschnitte (schnell, langsam, sehr langsam) müssen in der gleichen Reihenfolge gestapelt sein, um den Trend zu validieren, wenn der MA-Filter aktiviert ist.
  • Der Stochastik-Filter wartet auf einen %K/%D-Crossover nahe konfigurierbarer oberer und unterer Bänder.
  • Der RSI-Filter erfordert, dass der Oszillator eine neutrale Zone verlässt (unter dem unteren Band für Longs, über dem oberen Band für Shorts).
  • Ein Signal wird nur akzeptiert, wenn jeder aktivierte Filter in dieselbe Richtung abstimmt. Wenn ein Filter nicht zustimmt, wird nicht gehandelt.
  • Die Strategie öffnet jeweils eine Position. Sie hedgt nicht und baut keine Grids; wenn flach, wartet sie auf das nächste ausgerichtete Signal.

Positions-Management

  • Ein Take-Profit und Stop-Loss werden in Pips ausgedrückt. Der anfängliche Stop wird auf zwei Drittel der konfigurierten Distanz gesetzt, was das ursprüngliche EA-Verhalten reproduziert.
  • Ein Breakeven-Trigger (ebenfalls in Pips) verschiebt den Stop auf den Einstiegspreis, sobald der Trade die angegebene Distanz gewonnen hat.
  • Trailing Stops verwenden eine Distanz und einen zusätzlichen Schritt. Der Schritt verhindert, dass der Stop bei jedem kleinen Aufwärtsbewegung aktualisiert wird, und entspricht der MT5-Trailing-Logik.
  • Long- und Short-Trades teilen dieselben Verwaltungsregeln und reagieren symmetrisch auf Hochs/Tiefs der Kerze.

Positions-Sizing

  • FixedVolume größer als null öffnet jede Order mit einer festen Größe.
  • Wenn FixedVolume auf null gesetzt ist, berechnet die Strategie das Volumen aus RiskPercent, dem aktuellen Portfoliowert und der Stop-Distanz. Preisschritt und Schrittpreis werden verwendet, um die Pip-Distanz in monetäres Risiko umzurechnen.
  • Volumen werden gerundet, um die VolumeMin-, VolumeMax- und VolumeStep-Beschränkungen des Instruments zu respektieren. Orders werden übersprungen, wenn die berechnete Größe zu klein ist.

Parameter

Name Beschreibung
CandleType Zeitrahmen für die Signalerzeugung (Standard 15-Minuten-Kerzen).
FixedVolume, RiskPercent Wahl zwischen festem oder risikobasiertem Sizing.
StopLossPips, TakeProfitPips Basis-Schutz-Distanzen in Pips.
TrailingStopPips, TrailingStepPips, BreakevenPips Trade-Management-Schwellenwerte.
AllowLongs, AllowShorts Einzelne Richtungen aktivieren oder deaktivieren.
UseMovingAverageFilter, FastMaPeriod, SlowMaPeriod, VerySlowMaPeriod Dreifacher EMA-Trendfilter.
UseStochastic, StochasticKPeriod, StochasticDPeriod, StochasticSmooth, StochasticUpperLevel, StochasticLowerLevel Stochastik-Bestätigungseinstellungen.
UseRsi, RsiPeriod, RsiUpperLevel, RsiLowerLevel RSI-Bestätigungsband.

Hinweise

  • Die Pip-Konvertierung emuliert den ursprünglichen EA: Fünf- und dreistellige Symbole multiplizieren den Preisschritt mit zehn vor der Berechnung der Pip-Werte.
  • Der StockSharp-Port verwendet nur Marktorders. Die Sperr- und ausstehenden Order-Funktionen der MetaTrader-Version werden absichtlich weggelassen, um die Implementierung konsistent mit der High-Level-API zu halten.
  • Hängen Sie die Strategie an ein Chart an, wenn Sie die EMA-, Stochastik- und RSI-Overlays sehen möchten; sie werden automatisch gezeichnet, wenn ein Chart-Bereich verfügbar ist.
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;
	}
}