Auf GitHub ansehen

SV Tages-Ausbruch Strategie

Überblick

Die SV Tages-Ausbruch Strategie ist eine direkte C#-Konvertierung des MetaTrader 5 Expert Advisors "SV v.4.2.5". Das System bewertet die Kursbewegung einmal pro abgeschlossener Bar und erlaubt maximal einen Trade pro Börsentag. Der Handel beginnt erst nach der konfigurierten Startzeit und basiert auf der Beziehung zwischen dem jüngsten Hoch/Tief-Bereich und zwei geglätteten gleitenden Durchschnitten. Eine Long-Position wird eröffnet, wenn der gesamte analysierte Bereich unter beiden Durchschnitten bleibt, was eine erwartete Erholung aus überverkauften Bedingungen signalisiert. Umgekehrt wird eine Short-Position eröffnet, wenn der Bereich über beiden Durchschnitten bleibt, was eine mögliche Umkehr aus überkauftem Gebiet signalisiert.

Handelsregeln

Einstiegsbedingungen

  • Tagesfilter – Es werden keine Trades bewertet, bis die aktuelle Serverzeit später als Start Hour/Start Minute ist. Pro Tag ist nur ein Einstieg erlaubt.
  • Datenfenster – Die Strategie überspringt die neuesten Shift Bars und analysiert die nächsten Interval Bars. Ihre höchsten und niedrigsten Preise werden mit den verschobenen gleitenden Durchschnitten verglichen.
  • Long-Einstieg – Wenn der höchste Preis im analysierten Fenster strikt unter dem langsamen MA liegt und der niedrigste Preis strikt unter dem schnellen MA liegt, Long einsteigen (zuerst jede bestehende Short-Position schließen).
  • Short-Einstieg – Wenn der niedrigste Preis im analysierten Fenster strikt über dem langsamen MA liegt und der höchste Preis strikt über dem schnellen MA liegt, Short einsteigen (zuerst jede bestehende Long-Position schließen).

Exit-Management

  • Anfänglicher Stop-Loss – wird Stop Loss (pips) vom Einstiegspreis entfernt platziert. Wenn das Niveau erreicht wird, wird die Position geschlossen.
  • Take-Profit – wird Take Profit (pips) vom Einstiegspreis entfernt platziert. Wenn das Niveau erreicht wird, wird die Position geschlossen.
  • Trailing-Stop – wenn aktiviert (sowohl Trailing-Abstand als auch -Schritt sind größer als null), bewegt sich der Stop in die Gewinnrichtung. Bei Longs wird der Stop auf Schluss − Trailing Stop angehoben, sobald der Preis mehr als Trailing Stop + Trailing Step vorgerückt ist; Shorts spiegeln die Logik.
  • Tagessperre – Unabhängig davon, wie ein Trade ausläuft, wird die Strategie bis zum nächsten Handelstag keine neue Position öffnen.

Positionsgrößenbestimmung

  • Manueller Modus – wenn Use Manual Volume true ist, sendet die Strategie den festen Volume-Wert (angepasst an den Volumenschritt des Instruments).
  • Risikobasierter Modus – wenn Use Manual Volume false ist, schätzt die Strategie die Handelsgröße aus dem Kontokapital und Risk %. Es teilt das Risikokapital durch den Geldwert des konfigurierten Stop-Abstands unter Verwendung von Instrument-Schrittinformationen, wenn verfügbar.

Parameter

Parameter Standard Beschreibung
Use Manual Volume false Den festen Volume-Wert anstelle der risikobasierten Größenbestimmung verwenden.
Volume 0.1 Handelsvolumen, wenn manuelle Größenbestimmung aktiviert ist.
Risk % 5 Prozentsatz des Kontokapitals, der pro Trade riskiert wird, wenn manuelle Größenbestimmung aktiv ist.
Stop Loss (pips) 50 Stop-Loss-Abstand in Pips. Auf 0 setzen zum Deaktivieren.
Take Profit (pips) 50 Take-Profit-Abstand in Pips. Auf 0 setzen zum Deaktivieren.
Trailing Stop (pips) 5 Trailing-Stop-Abstand in Pips. Erfordert, dass Trailing Step größer als null ist.
Trailing Step (pips) 5 Minimaler Gewinnzuwachs, bevor der Trailing-Stop bewegt wird.
Start Hour 19 Stunde (Börsenzeit), wenn Einstiege beginnen dürfen.
Start Minute 0 Minute (Börsenzeit), wenn Einstiege beginnen dürfen.
Shift 6 Anzahl der neuesten Bars, die vor der Analyse des Bereichs ausgeschlossen werden.
Interval 27 Anzahl der historischen Bars, die zur Berechnung des Hoch/Tief-Fensters verwendet werden.
Fast MA Period 14 Länge des schnellen gleitenden Durchschnitts.
Fast MA Shift 0 Horizontale Verschiebung (Bars zurück) für den Wert des schnellen MA.
Fast MA Method Smma Gleitender Durchschnitt-Methode für den schnellen MA.
Fast Applied Price Median Preisquelle für den schnellen MA.
Slow MA Period 41 Länge des langsamen gleitenden Durchschnitts.
Slow MA Shift 0 Horizontale Verschiebung (Bars zurück) für den Wert des langsamen MA.
Slow MA Method Smma Gleitender Durchschnitt-Methode für den langsamen MA.
Slow Applied Price Median Preisquelle für den langsamen MA.
Candle Type 1 hour Für Berechnungen verwendete Kerzenserie.

Zusätzliche Hinweise

  • Die Konvertierung behält das ursprüngliche Verhalten bei, ein verzögertes Preis-Fenster (Shift + Interval) zu analysieren, um die aktuellsten Bars bei der Bestimmung von Ausbrüchen zu vermeiden.
  • Die Trailing-Logik verwendet den Kerzenschlusskurs, um MetaTrader's tick-basierte Trailing-Updates zu approximieren. Passen Sie die Pip-Abstände an, wenn Ihr Instrument eine andere Präzision erfordert.
  • Die risikobasierte Größenbestimmung basiert auf Security.PriceStep, Security.StepPrice und Security.VolumeStep. Geben Sie diese Werte in Ihren Instrumenteinstellungen an für eine genaue Lot-Größenbestimmung.
  • Die Strategie ruft StartProtection() auf, sodass Sie bei Bedarf zusätzliche globale Risikoregeln anhängen können.
  • Um den ursprünglichen EA zu spiegeln, stellen Sie sicher, dass Ihr Datenfeed und Ihr Trading-Konto in derselben Serverzeitzone betrieben werden, auf die die Parameter Start Hour und Start Minute verweisen.
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>
/// Daily breakout strategy converted from the "SV v.4.2.5" MetaTrader 5 expert advisor.
/// Evaluates one trade per day after a configurable start time using moving average filters.
/// </summary>
public class SvDailyBreakoutStrategy : Strategy
{
	private readonly StrategyParam<bool> _useManualVolume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _startMinute;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<int> _interval;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _fastMaShift;
	private readonly StrategyParam<MovingAverageMethods> _fastMaMethod;
	private readonly StrategyParam<AppliedPrices> _fastAppliedPrice;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _slowMaShift;
	private readonly StrategyParam<MovingAverageMethods> _slowMaMethod;
	private readonly StrategyParam<AppliedPrices> _slowAppliedPrice;
	private readonly StrategyParam<DataType> _candleType;

	private IIndicator _fastMa;
	private IIndicator _slowMa;

	private readonly List<decimal> _fastMaValues = new();
	private readonly List<decimal> _slowMaValues = new();
	private readonly List<decimal> _highHistory = new();
	private readonly List<decimal> _lowHistory = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal? _trailingStopPrice;
	private DateTime? _currentDay;
	private decimal _pipSize;

	/// <summary>
	/// Use manual volume instead of the risk-based sizing model.
	/// </summary>
	public bool UseManualVolume
	{
		get => _useManualVolume.Value;
		set => _useManualVolume.Value = value;
	}


	/// <summary>
	/// Risk percentage of account equity used when calculating the dynamic position size.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing step distance expressed in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Hour of the day (exchange time) when the strategy starts searching for entries.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Minute of the hour when the strategy starts searching for entries.
	/// </summary>
	public int StartMinute
	{
		get => _startMinute.Value;
		set => _startMinute.Value = value;
	}

	/// <summary>
	/// Number of recent bars excluded from the high/low analysis window.
	/// </summary>
	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	/// <summary>
	/// Number of bars that are analysed when computing the breakout range.
	/// </summary>
	public int Interval
	{
		get => _interval.Value;
		set => _interval.Value = value;
	}

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Fast moving average horizontal shift.
	/// </summary>
	public int FastMaShift
	{
		get => _fastMaShift.Value;
		set => _fastMaShift.Value = value;
	}

	/// <summary>
	/// Fast moving average calculation method.
	/// </summary>
	public MovingAverageMethods FastMaMethod
	{
		get => _fastMaMethod.Value;
		set => _fastMaMethod.Value = value;
	}

	/// <summary>
	/// Applied price used for the fast moving average.
	/// </summary>
	public AppliedPrices FastAppliedPrice
	{
		get => _fastAppliedPrice.Value;
		set => _fastAppliedPrice.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Slow moving average horizontal shift.
	/// </summary>
	public int SlowMaShift
	{
		get => _slowMaShift.Value;
		set => _slowMaShift.Value = value;
	}

	/// <summary>
	/// Slow moving average calculation method.
	/// </summary>
	public MovingAverageMethods SlowMaMethod
	{
		get => _slowMaMethod.Value;
		set => _slowMaMethod.Value = value;
	}

	/// <summary>
	/// Applied price used for the slow moving average.
	/// </summary>
	public AppliedPrices SlowAppliedPrice
	{
		get => _slowAppliedPrice.Value;
		set => _slowAppliedPrice.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters with defaults that match the original expert advisor.
	/// </summary>
	public SvDailyBreakoutStrategy()
	{
		_useManualVolume = Param(nameof(UseManualVolume), false)
			.SetDisplay("Use Manual Volume", "Use fixed volume instead of risk percentage", "Risk");


		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Risk percentage of account equity", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Trailing step increment in pips", "Risk");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Hour when trading may begin", "Trading Window");

		_startMinute = Param(nameof(StartMinute), 0)
			.SetDisplay("Start Minute", "Minute when trading may begin", "Trading Window");

		_shift = Param(nameof(Shift), 2)
			.SetNotNegative()
			.SetDisplay("Shift", "Number of newest bars excluded from range analysis", "Logic");

		_interval = Param(nameof(Interval), 10)
			.SetGreaterThanZero()
			.SetDisplay("Interval", "Number of historical bars analysed", "Logic");

		_fastMaPeriod = Param(nameof(FastMaPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA Period", "Fast moving average length", "Indicators");

		_fastMaShift = Param(nameof(FastMaShift), 0)
			.SetNotNegative()
			.SetDisplay("Fast MA Shift", "Horizontal shift for the fast moving average", "Indicators");

		_fastMaMethod = Param(nameof(FastMaMethod), MovingAverageMethods.Ema)
			.SetDisplay("Fast MA Method", "Calculation method for the fast moving average", "Indicators");

		_fastAppliedPrice = Param(nameof(FastAppliedPrice), AppliedPrices.Median)
			.SetDisplay("Fast Applied Price", "Price type used for the fast moving average", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA Period", "Slow moving average length", "Indicators");

		_slowMaShift = Param(nameof(SlowMaShift), 0)
			.SetNotNegative()
			.SetDisplay("Slow MA Shift", "Horizontal shift for the slow moving average", "Indicators");

		_slowMaMethod = Param(nameof(SlowMaMethod), MovingAverageMethods.Ema)
			.SetDisplay("Slow MA Method", "Calculation method for the slow moving average", "Indicators");

		_slowAppliedPrice = Param(nameof(SlowAppliedPrice), AppliedPrices.Median)
			.SetDisplay("Slow Applied Price", "Price type used for the slow moving average", "Indicators");

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

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

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

		_fastMaValues.Clear();
		_slowMaValues.Clear();
		_highHistory.Clear();
		_lowHistory.Clear();
		_entryPrice = null;
		_pipSize = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
		_trailingStopPrice = null;
		_currentDay = null;
	}

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

		// validation removed for flexibility

		_fastMa = new ExponentialMovingAverage { Length = FastMaPeriod };
		_slowMa = new ExponentialMovingAverage { Length = SlowMaPeriod };

		var decimals = Security?.Decimals ?? 2;
		var step = Security?.PriceStep ?? 0.01m;
		var factor = decimals is 3 or 5 ? 10m : 1m;
		_pipSize = step * factor;
		if (_pipSize <= 0m)
			_pipSize = step > 0m ? step : 0.01m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_fastMa, _slowMa, ProcessCandleWithMa)
			.Start();
	}

	private void ProcessCandleWithMa(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateDailyState(candle.CloseTime);

		UpdateRangeHistory(candle);

		UpdateTrailing(candle);

		if (CheckProtectiveExits(candle))
			return;

		if (Position != 0)
			return;

		if (!TryGetRangeExtremes(out var lowest, out var highest))
			return;

		if (highest < slowValue && lowest < fastValue)
		{
			EnterPosition(true, candle);
			return;
		}

		if (lowest > slowValue && highest > fastValue)
		{
			EnterPosition(false, candle);
		}
	}

	private void EnterPosition(bool isLong, ICandleMessage candle)
	{
		var entryPrice = candle.ClosePrice;
		var stopDistance = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;

		var volume = UseManualVolume
			? NormalizeVolume(Volume)
			: CalculateRiskBasedVolume(stopDistance);

		if (volume <= 0m)
			return;

		if (isLong)
		{
			var totalVolume = volume + (Position < 0 ? Math.Abs(Position) : 0m);
			if (totalVolume <= 0m)
				return;

			BuyMarket(totalVolume);
			_entryPrice = entryPrice;
			_stopPrice = StopLossPips > 0 ? entryPrice - stopDistance : null;
			_takeProfitPrice = TakeProfitPips > 0 ? entryPrice + TakeProfitPips * _pipSize : null;
		}
		else
		{
			var totalVolume = volume + (Position > 0 ? Position : 0m);
			if (totalVolume <= 0m)
				return;

			SellMarket(totalVolume);
			_entryPrice = entryPrice;
			_stopPrice = StopLossPips > 0 ? entryPrice + stopDistance : null;
			_takeProfitPrice = TakeProfitPips > 0 ? entryPrice - TakeProfitPips * _pipSize : null;
		}

		_trailingStopPrice = TrailingStopPips > 0 ? _stopPrice : null;
	}

	private void UpdateDailyState(DateTimeOffset time)
	{
		var day = time.Date;
		if (_currentDay != day)
		{
			_currentDay = day;
		}
	}

	private void UpdateRangeHistory(ICandleMessage candle)
	{
		_highHistory.Add(candle.HighPrice);
		_lowHistory.Add(candle.LowPrice);

		var maxCount = Math.Max(Shift + Interval + 5, 50);
		if (_highHistory.Count > maxCount)
		{
			var remove = _highHistory.Count - maxCount;
			_highHistory.RemoveRange(0, remove);
			_lowHistory.RemoveRange(0, remove);
		}
	}

	private decimal? ProcessMovingAverage(IIndicator indicator, AppliedPrices priceMode, List<decimal> buffer, int shift, ICandleMessage candle)
	{
		var price = GetAppliedPrice(candle, priceMode);
		var result = indicator.Process(new DecimalIndicatorValue(indicator, price, candle.OpenTime));

		if (!result.IsFormed)
			return null;

		var value = result.GetValue<decimal>();
		buffer.Add(value);

		var maxSize = Math.Max(shift + 1, 100);
		if (buffer.Count > maxSize)
			buffer.RemoveAt(0);

		var index = buffer.Count - 1 - shift;
		if (index < 0 || index >= buffer.Count)
			return null;

		return buffer[index];
	}

	private bool TryGetRangeExtremes(out decimal lowest, out decimal highest)
	{
		lowest = 0m;
		highest = 0m;

		var required = Shift + Interval;
		if (required <= 0)
			return false;

		if (_lowHistory.Count < required || _highHistory.Count < required)
			return false;

		var low = decimal.MaxValue;
		var high = decimal.MinValue;
		var total = _lowHistory.Count;

		for (var offset = Shift; offset < Shift + Interval; offset++)
		{
			var index = total - 1 - offset;
			if (index < 0)
				return false;

			var lowValue = _lowHistory[index];
			var highValue = _highHistory[index];

			if (lowValue < low)
				low = lowValue;

			if (highValue > high)
				high = highValue;
		}

		if (low == decimal.MaxValue || high == decimal.MinValue)
			return false;

		lowest = low;
		highest = high;
		return true;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0 || TrailingStepPips <= 0 || _entryPrice is null)
			return;

		var trailDistance = TrailingStopPips * _pipSize;
		var stepDistance = TrailingStepPips * _pipSize;

		if (Position > 0)
		{
			var current = candle.ClosePrice;
			var entry = _entryPrice.Value;
			if (current - entry > trailDistance + stepDistance)
			{
				var threshold = current - (trailDistance + stepDistance);
				if (_stopPrice is null || _stopPrice < threshold)
				{
					var newStop = current - trailDistance;
					if (_stopPrice is null || newStop > _stopPrice)
					{
						_stopPrice = newStop;
						_trailingStopPrice = newStop;
					}
				}
			}
		}
		else if (Position < 0)
		{
			var current = candle.ClosePrice;
			var entry = _entryPrice.Value;
			if (entry - current > trailDistance + stepDistance)
			{
				var threshold = current + trailDistance + stepDistance;
				if (_stopPrice is null || _stopPrice > threshold)
				{
					var newStop = current + trailDistance;
					if (_stopPrice is null || newStop < _stopPrice)
					{
						_stopPrice = newStop;
						_trailingStopPrice = newStop;
					}
				}
			}
		}
	}

	private bool CheckProtectiveExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				ResetTradeState();
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Position);
				ResetTradeState();
				return true;
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(volume);
				ResetTradeState();
				return true;
			}

			if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(volume);
				ResetTradeState();
				return true;
			}
		}
		else if (_entryPrice is not null)
		{
			ResetTradeState();
		}

		return false;
	}

	private decimal CalculateRiskBasedVolume(decimal stopDistance)
	{
		if (UseManualVolume || stopDistance <= 0m)
			return NormalizeVolume(Volume);

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;
		if (portfolioValue <= 0m)
			return NormalizeVolume(Volume);

		var riskAmount = portfolioValue * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return NormalizeVolume(Volume);

		var step = Security?.PriceStep ?? _pipSize;
		if (step <= 0m)
			step = _pipSize > 0m ? _pipSize : 1m;

		var stepValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? step;
		if (stepValue <= 0m)
			stepValue = step;

		var steps = stopDistance / step;
		if (steps <= 0m)
			return NormalizeVolume(Volume);

		var riskPerUnit = steps * stepValue;
		if (riskPerUnit <= 0m)
			return NormalizeVolume(Volume);

		var rawVolume = riskAmount / riskPerUnit;
		return NormalizeVolume(rawVolume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		if (Security is null)
			return volume;

		var step = Security.VolumeStep ?? 1m;
		if (step > 0m)
			volume = Math.Floor(volume / step) * step;

		if (volume < step)
			volume = step;

		return volume;
	}

	private static decimal GetAppliedPrice(ICandleMessage candle, AppliedPrices mode)
	{
		return mode switch
		{
			AppliedPrices.Open => candle.OpenPrice,
			AppliedPrices.High => candle.HighPrice,
			AppliedPrices.Low => candle.LowPrice,
			AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPrices.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPrices.Weighted => (candle.HighPrice + candle.LowPrice + 2m * candle.ClosePrice) / 4m,
			_ => candle.ClosePrice,
		};
	}

	private static IIndicator CreateMovingAverage(MovingAverageMethods method, int length)
	{
		return method switch
		{
			MovingAverageMethods.Sma => new SimpleMovingAverage { Length = length },
			MovingAverageMethods.Ema => new ExponentialMovingAverage { Length = length },
			MovingAverageMethods.Smma => new SmoothedMovingAverage { Length = length },
			MovingAverageMethods.Lwma => new WeightedMovingAverage { Length = length },
			_ => new SimpleMovingAverage { Length = length },
		};
	}

	private void ResetTradeState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_trailingStopPrice = null;
	}

	/// <summary>
	/// Available moving average calculation methods.
	/// </summary>
	public enum MovingAverageMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Sma,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Ema,

		/// <summary>
		/// Smoothed moving average (SMMA).
		/// </summary>
		Smma,

		/// <summary>
		/// Linear weighted moving average (LWMA).
		/// </summary>
		Lwma
	}

	/// <summary>
	/// Price sources supported by the moving averages.
	/// </summary>
	public enum AppliedPrices
	{
		/// <summary>
		/// Close price.
		/// </summary>
		Close,

		/// <summary>
		/// Open price.
		/// </summary>
		Open,

		/// <summary>
		/// High price.
		/// </summary>
		High,

		/// <summary>
		/// Low price.
		/// </summary>
		Low,

		/// <summary>
		/// Median price (high + low) / 2.
		/// </summary>
		Median,

		/// <summary>
		/// Typical price (high + low + close) / 3.
		/// </summary>
		Typical,

		/// <summary>
		/// Weighted close price (high + low + 2 * close) / 4.
		/// </summary>
		Weighted
	}
}