Auf GitHub ansehen

Zonen-Wiederherstellungsbereich Strategie

Überblick

Die Zonen-Wiederherstellungsbereich Strategie ist eine direkte Konvertierung des MetaTrader Expert Advisors "Zone Recovery Area" (Paket MQL/20266). Sie recreiert die ursprüngliche Absicherungslogik auf dem StockSharp High-Level API und fügt eine erschöpfende Parametrisierung hinzu, damit das Verhalten ohne Codeänderungen angepasst werden kann. Die Strategie kombiniert einen Trendfilter mit einem alternierenden Kauf/Verkauf-Wiederherstellungsgitter: Sobald ein primärer Trade eröffnet wird, werden zusätzliche Positionen gestapelt, wenn der Preis die vordefinierte Zone verlässt oder wieder eintritt, wodurch ein abgesicherter Korb entsteht, der darauf abzielt, schwebende Drawdowns zu erholen.

Kerneigenschaften:

  • Verwendet einen schnellen/langsamen einfachen gleitenden Durchschnittskrossing zusammen mit einem monatlichen MACD-Filter, um den Handelsvoreingenommenheit zu definieren.
  • Implementiert die Zonenwiederherstellungstechnik: Der erste Trade etabliert einen Basispreis, und alternierende Absicherungsorders werden ausgelöst, wenn der Markt die Zonengrenze überquert oder zum Basisniveau zurückkehrt.
  • Bietet geldbasierte, prozentbasierte und Trailing-Gewinnkontrollen, um den Korb zu verlassen, sobald ausreichend Gewinn gesichert ist.
  • Erlaubt sowohl multiplikative (Martingale-Stil) als auch additive Positionsgrößen für jeden Wiederherstellungsschritt.

Marktdaten & Indikatoren

  • Primäre Kerzen: Benutzerdefinierter Zeitrahmen (Standard 30 Minuten) für Einstiege und Wiederherstellungsmanagement.
  • Monatliche Kerzen: Bei Bedarf aus niedrigeren Zeitrahmen konstruiert; zur Berechnung der MACD (12/26/9)-Werte verwendet.
  • Indikatoren:
    • Einfacher gleitender Durchschnitt (schnell und langsam) auf dem primären Zeitrahmen.
    • Moving Average Convergence Divergence mit Signallinie auf dem monatlichen Zeitrahmen.

Handelslogik

  1. Trendvalidierung
    • Warten bis beide SMAs und der monatliche MACD vollständig geformt sind.
    • Ein bullisches Setup erfordert, dass der schnelle SMA unter dem langsamen SMA der vorherigen Bar liegt, während die monatliche MACD-Linie über ihrem Signal liegt.
    • Ein bärisches Setup erfordert, dass der schnelle SMA über dem langsamen SMA der vorherigen Bar liegt, während die monatliche MACD-Linie unter ihrem Signal liegt.
  2. Zyklusinitialisierung
    • Wenn ein bullisches (bärisches) Setup erkannt wird, die anfängliche Long-(Short-)Position mit InitialVolume eröffnen und den Einstiegspreis als Zyklusbasis speichern.
    • Interne Zähler und Gewinnverfolgung für den neuen Zyklus zurücksetzen.
  3. Zonenwiederherstellungsmotor
    • Zwei kritische Niveaus definieren: die Zonengrenze (ZoneRecoveryPips) vom Basispreis entfernt und das Take-Profit-Niveau (TakeProfitPips) in der günstigen Richtung.
    • Während der Zyklus aktiv ist, jede abgeschlossene Kerze überwachen:
      • Wenn der Preis das Take-Profit-Niveau erreicht, alle Nettoexposition schließen und den Zyklus beenden.
      • Wenn geld- oder prozentbasierte Gewinnziele erfüllt sind oder die Trailing-Gewinnsperre ausgelöst wird, den Zyklus schließen.
      • Andernfalls prüfen, ob eine neue Absicherung benötigt wird:
        • Für Long-Zyklen: einen zusätzlichen Short eröffnen, wenn der Preis unter base - zone fällt, und einen zusätzlichen Long, wenn der Preis wieder über den Basispreis steigt.
        • Für Short-Zyklen: einen zusätzlichen Long eröffnen, wenn der Preis über base + zone steigt, und einen zusätzlichen Short, wenn der Preis unter den Basispreis zurückkehrt.
      • Die Absicherungsrichtung wechselt automatisch; die nächste Ordergröße wird durch Multiplikation des vorherigen Volumens oder durch Hinzufügen eines festen Inkrements bestimmt.
    • Die Anzahl der Trades pro Korb ist durch MaxTrades begrenzt.
  4. Gewinnmanagement
    • UseMoneyTakeProfit: Den Korb schließen, sobald der nicht realisierte Gewinn den konfigurierten Währungsbetrag erreicht.
    • UsePercentTakeProfit: Den Korb schließen, sobald der nicht realisierte Gewinn dem angegebenen Prozentsatz des Portfoliowertes entspricht.
    • EnableTrailing: Sobald der Gewinn TrailingStartProfit übersteigt, den Peak verfolgen und den Zyklus verlassen, wenn der Gewinn um TrailingDrawdown fällt.

Alle Orders werden mit StockSharp High-Level-Hilfsprogrammen (BuyMarket/SellMarket) platziert, was die Implementierung konsistent mit den Framework-Best-Practices hält.

Parameter

Name Standard Beschreibung
CandleType 30-Minuten-Kerzen Zeitrahmen für Einstiege und Wiederherstellungsüberwachung.
MonthlyCandleType 30-Tages-Kerzen Höherer Zeitrahmen zur Erstellung des MACD-Trendfilters.
FastMaLength 20 Periode des schnellen SMA.
SlowMaLength 200 Periode des langsamen SMA.
TakeProfitPips 150 Abstand vom Basispreis zum Schließen des gesamten Korbs im Gewinn.
ZoneRecoveryPips 50 Halbbreite der Absicherungszone um den Basispreis.
InitialVolume 1 Volumen des ersten Trades in jedem Zyklus.
UseVolumeMultiplier true Wenn aktiviert, multipliziert jede neue Absicherung das vorherige Volumen.
VolumeMultiplier 2 Faktor, der auf das vorherige Volumen angewendet wird, wenn UseVolumeMultiplier true ist.
VolumeIncrement 0.5 Additiver Volumenzuwachs, wenn UseVolumeMultiplier false ist.
MaxTrades 6 Maximale Anzahl von Trades pro Wiederherstellungszyklus (einschließlich des anfänglichen).
UseMoneyTakeProfit false Geldbasiertes Take-Profit aktivieren.
MoneyTakeProfit 40 Gewinnziel in Kontowährung.
UsePercentTakeProfit false Prozentbasiertes Take-Profit aktivieren.
PercentTakeProfit 5 Gewinnziel als Prozentsatz des Portfoliowertes.
EnableTrailing true Trailing-Gewinnschutz aktivieren.
TrailingStartProfit 40 Gewinnschwelle, bevor Trailing aktiv wird.
TrailingDrawdown 10 Erlaubter Gewinnrückgang, sobald Trailing aktiv ist.

Pip-Konvertierung: TakeProfitPips und ZoneRecoveryPips werden in Preisoffsets unter Verwendung des Preis-Steps des Instruments konvertiert. Stellen Sie sicher, dass das gehandelte Instrument korrekte PriceStep und StepPrice-Werte bereitstellt.

Verwendungshinweise

  1. Strategie zur StockSharp-Lösung hinzufügen (Designer, API, Runner usw.).
  2. Gewünschtes Wertpapier und Portfolio vor dem Start zuweisen.
  3. Parameter an die Volatilität des Instruments, akzeptablen Drawdown und Kontogröße anpassen.
  4. Ausreichende historische Daten sicherstellen, damit sowohl SMAs als auch der monatliche MACD vor dem ersten Trade aufgewärmt werden können.
  5. Margenbenutzung sorgfältig überwachen: Wiederherstellungsschritte können die Exposition schnell erhöhen, besonders wenn der Multiplikator aktiviert ist.

Risikomanagement & Überlegungen

  • Zonenwiederherstellungs-/Martingale-Techniken können in Trendmärkten sehr große Positionen ansammeln. Immer mit konservativen Einstellungen testen und den MaxTrades-Parameter zur Risikobegrenzung verwenden.
  • Da StockSharp eine einzige Nettoposition pflegt, repliziert die interne Gewinnberechnung den Korb-PnL unter Verwendung von Instrumentspreise/-Schritt-Informationen. Die Zahlen mit dem Broker-Datenfeed validieren.
  • Geld- und Prozentziele hängen von der Portfoliobewertung ab. Beim Backtesting oder Paper-Trading sicherstellen, dass das Portfoliomodell BeginValue/CurrentValue korrekt liefert.
  • Kein automatischer harter Stop-Loss wird verwendet; Risiko wird über die Wiederherstellungsmechaniken verwaltet. Die Strategie mit externen Portfolio-Level-Stops kombinieren.

Dateien

  • CS/ZoneRecoveryAreaStrategy.cs — Implementierung der Strategie.
  • README.md — Englische Dokumentation (diese Datei).
  • README_ru.md — Russische Dokumentation.
  • README_zh.md — Chinesische Dokumentation.
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>
/// Zone recovery hedging strategy converted from MetaTrader expert advisor.
/// The strategy alternates buy and sell positions around a base price to recover drawdowns.
/// </summary>
public class ZoneRecoveryAreaStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _monthlyCandleType;
	private readonly StrategyParam<int> _fastMaLength;
	private readonly StrategyParam<int> _slowMaLength;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _zoneRecoveryPips;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<bool> _useVolumeMultiplier;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _volumeIncrement;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<bool> _useMoneyTakeProfit;
	private readonly StrategyParam<decimal> _moneyTakeProfit;
	private readonly StrategyParam<bool> _usePercentTakeProfit;
	private readonly StrategyParam<decimal> _percentTakeProfit;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<decimal> _trailingStartProfit;
	private readonly StrategyParam<decimal> _trailingDrawdown;

	private SimpleMovingAverage _fastMa = null!;
	private SimpleMovingAverage _slowMa = null!;
	private MovingAverageConvergenceDivergenceSignal _monthlyMacd = null!;

	private decimal _prevFast;
	private decimal _prevSlow;
	private bool _maInitialized;
	private bool _macdReady;
	private decimal _macdMain;
	private decimal _macdSignal;
	private bool _isLongCycle;
	private decimal _cycleBasePrice;
	private int _nextStepIndex;
	private decimal _peakCycleProfit;

	private readonly List<TradeStep> _steps = new();

	/// <summary>
	/// Initializes a new instance of <see cref="ZoneRecoveryAreaStrategy"/>.
	/// </summary>
	public ZoneRecoveryAreaStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Entry Candle", "Timeframe used for entries", "General");

		_monthlyCandleType = Param(nameof(MonthlyCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Monthly Candle", "Timeframe used for MACD filter", "General");

		_fastMaLength = Param(nameof(FastMaLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA", "Fast moving average period", "Trend Filter");

		_slowMaLength = Param(nameof(SlowMaLength), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA", "Slow moving average period", "Trend Filter");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance to close the cycle in profit", "Risk Management");

		_zoneRecoveryPips = Param(nameof(ZoneRecoveryPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Zone Width (pips)", "Distance that triggers hedging trades", "Risk Management");

		_initialVolume = Param(nameof(InitialVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Volume of the first trade", "Position Sizing");

		_useVolumeMultiplier = Param(nameof(UseVolumeMultiplier), true)
			.SetDisplay("Use Multiplier", "If true the next trades multiply the previous volume", "Position Sizing");

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Multiplier", "Factor applied when increasing volume", "Position Sizing");

		_volumeIncrement = Param(nameof(VolumeIncrement), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Increment", "Additional volume when multiplier is disabled", "Position Sizing");

		_maxTrades = Param(nameof(MaxTrades), 6)
			.SetGreaterThanZero()
			.SetDisplay("Max Trades", "Maximum number of trades in one cycle", "Risk Management");

		_useMoneyTakeProfit = Param(nameof(UseMoneyTakeProfit), false)
			.SetDisplay("Money Take Profit", "Enable profit target in account currency", "Risk Management");

		_moneyTakeProfit = Param(nameof(MoneyTakeProfit), 40m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit $", "Target profit in account currency", "Risk Management");

		_usePercentTakeProfit = Param(nameof(UsePercentTakeProfit), false)
			.SetDisplay("Percent Take Profit", "Enable profit target based on account balance", "Risk Management");

		_percentTakeProfit = Param(nameof(PercentTakeProfit), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit %", "Target profit as a percentage of balance", "Risk Management");

		_enableTrailing = Param(nameof(EnableTrailing), true)
			.SetDisplay("Trailing", "Enable trailing profit lock", "Risk Management");

		_trailingStartProfit = Param(nameof(TrailingStartProfit), 40m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Start", "Profit required before trailing starts", "Risk Management");

		_trailingDrawdown = Param(nameof(TrailingDrawdown), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Step", "Maximum profit giveback before exit", "Risk Management");
	}

	/// <summary>
	/// Working candle type for entries.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Monthly candle type used for the MACD filter.
	/// </summary>
	public DataType MonthlyCandleType
	{
		get => _monthlyCandleType.Value;
		set => _monthlyCandleType.Value = value;
	}

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

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

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

	/// <summary>
	/// Zone width in pips for opening hedging trades.
	/// </summary>
	public decimal ZoneRecoveryPips
	{
		get => _zoneRecoveryPips.Value;
		set => _zoneRecoveryPips.Value = value;
	}

	/// <summary>
	/// Volume of the first trade in a cycle.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Use multiplicative volume scaling.
	/// </summary>
	public bool UseVolumeMultiplier
	{
		get => _useVolumeMultiplier.Value;
		set => _useVolumeMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the previous volume.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Additional volume added when multiplier is disabled.
	/// </summary>
	public decimal VolumeIncrement
	{
		get => _volumeIncrement.Value;
		set => _volumeIncrement.Value = value;
	}

	/// <summary>
	/// Maximum number of trades per recovery cycle.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Enable profit target in account currency.
	/// </summary>
	public bool UseMoneyTakeProfit
	{
		get => _useMoneyTakeProfit.Value;
		set => _useMoneyTakeProfit.Value = value;
	}

	/// <summary>
	/// Profit target in account currency.
	/// </summary>
	public decimal MoneyTakeProfit
	{
		get => _moneyTakeProfit.Value;
		set => _moneyTakeProfit.Value = value;
	}

	/// <summary>
	/// Enable profit target based on account percentage.
	/// </summary>
	public bool UsePercentTakeProfit
	{
		get => _usePercentTakeProfit.Value;
		set => _usePercentTakeProfit.Value = value;
	}

	/// <summary>
	/// Profit target as a percentage of account balance.
	/// </summary>
	public decimal PercentTakeProfit
	{
		get => _percentTakeProfit.Value;
		set => _percentTakeProfit.Value = value;
	}

	/// <summary>
	/// Enable trailing profit lock.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Profit level where trailing begins.
	/// </summary>
	public decimal TrailingStartProfit
	{
		get => _trailingStartProfit.Value;
		set => _trailingStartProfit.Value = value;
	}

	/// <summary>
	/// Allowed drawdown from the peak profit before closing.
	/// </summary>
	public decimal TrailingDrawdown
	{
		get => _trailingDrawdown.Value;
		set => _trailingDrawdown.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security != null)
		{
			yield return (Security, CandleType);
			yield return (Security, MonthlyCandleType);
		}
	}

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

		_steps.Clear();
		_prevFast = 0m;
		_prevSlow = 0m;
		_maInitialized = false;
		_macdReady = false;
		_macdMain = 0m;
		_macdSignal = 0m;
		_isLongCycle = false;
		_cycleBasePrice = 0m;
		_nextStepIndex = 0;
		_peakCycleProfit = 0m;
	}

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

		_fastMa = new SimpleMovingAverage { Length = FastMaLength };
		_slowMa = new SimpleMovingAverage { Length = SlowMaLength };
		_monthlyMacd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = 12 },
				LongMa = { Length = 26 }
			},
			SignalMa = { Length = 9 }
		};

		var mainSubscription = SubscribeCandles(CandleType);
		mainSubscription
			.Bind(_fastMa, _slowMa, ProcessMainCandle)
			.Start();

		var monthlySubscription = SubscribeCandles(MonthlyCandleType);
		monthlySubscription
			.Bind(ProcessMonthlyCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, mainSubscription);
			DrawIndicator(area, _fastMa);
			DrawIndicator(area, _slowMa);
			DrawOwnTrades(area);

			// MACD is manually processed so cannot be drawn via DrawIndicator
		}
	}

	private void ProcessMonthlyCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var macdResult = _monthlyMacd.Process(candle);
		if (macdResult.IsEmpty || !_monthlyMacd.IsFormed)
			return;

		var macd = (MovingAverageConvergenceDivergenceSignalValue)macdResult;
		if (macd.Macd is not decimal macdLine || macd.Signal is not decimal signalLine)
			return;

		_macdMain = macdLine;
		_macdSignal = signalLine;
		_macdReady = true;
	}

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

		if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_macdReady)
		{
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		if (!_maInitialized)
		{
			_prevFast = fastValue;
			_prevSlow = slowValue;
			_maInitialized = true;
			return;
		}

		if (_steps.Count > 0)
		{
			HandleExistingCycle(candle.ClosePrice);
		}
		else
		{
			TryStartCycle(candle.ClosePrice);
		}

		_prevFast = fastValue;
		_prevSlow = slowValue;
	}

	private void TryStartCycle(decimal price)
	{
		var macdBullish = _macdMain > _macdSignal;
		var macdBearish = _macdMain < _macdSignal;

		var bullishSetup = _prevFast < _prevSlow && macdBullish;
		var bearishSetup = _prevFast > _prevSlow && macdBearish;

		if (bullishSetup)
		{
			StartCycle(true, price);
		}
		else if (bearishSetup)
		{
			StartCycle(false, price);
		}
	}

	private void StartCycle(bool isLong, decimal price)
	{
		if (InitialVolume <= 0m)
			return;

		_steps.Clear();
		_isLongCycle = isLong;
		_cycleBasePrice = price;
		_nextStepIndex = 1;
		_peakCycleProfit = 0m;

		ExecuteOrder(isLong, InitialVolume, price);
	}

	private void HandleExistingCycle(decimal price)
	{
		var takeProfitOffset = GetPriceOffset(TakeProfitPips);
		if (takeProfitOffset > 0m)
		{
			if (_isLongCycle && price >= _cycleBasePrice + takeProfitOffset)
			{
				CloseCycle();
				return;
			}

			if (!_isLongCycle && price <= _cycleBasePrice - takeProfitOffset)
			{
				CloseCycle();
				return;
			}
		}

		var cycleProfit = CalculateCycleProfit(price);

		if (UseMoneyTakeProfit && MoneyTakeProfit > 0m && cycleProfit >= MoneyTakeProfit)
		{
			CloseCycle();
			return;
		}

		if (UsePercentTakeProfit && PercentTakeProfit > 0m && TryGetPercentTarget(out var percentTarget) && cycleProfit >= percentTarget)
		{
			CloseCycle();
			return;
		}

		if (EnableTrailing && TrailingStartProfit > 0m && TrailingDrawdown > 0m)
		{
			if (cycleProfit >= TrailingStartProfit)
			{
				_peakCycleProfit = Math.Max(_peakCycleProfit, cycleProfit);
			}

			if (_peakCycleProfit > 0m && cycleProfit <= _peakCycleProfit - TrailingDrawdown)
			{
				CloseCycle();
				return;
			}
		}
		else
		{
			_peakCycleProfit = 0m;
		}

		if (_steps.Count >= MaxTrades)
			return;

		if (!ShouldOpenNextTrade(price))
			return;

		var nextIsBuy = GetNextDirection();
		var volume = GetNextVolume();

		ExecuteOrder(nextIsBuy, volume, price);
		_nextStepIndex++;
	}

	private bool ShouldOpenNextTrade(decimal price)
	{
		var zoneOffset = GetPriceOffset(ZoneRecoveryPips);
		if (zoneOffset <= 0m)
			return false;

		var nextIsBuy = GetNextDirection();

		if (_isLongCycle)
		{
			if (nextIsBuy)
				return price >= _cycleBasePrice;

			return price <= _cycleBasePrice - zoneOffset;
		}

		if (nextIsBuy)
			return price >= _cycleBasePrice + zoneOffset;

		return price <= _cycleBasePrice;
	}

	private bool GetNextDirection()
	{
		var isOddStep = _nextStepIndex % 2 == 1;
		if (_isLongCycle)
			return !isOddStep;

		return isOddStep;
	}

	private decimal GetNextVolume()
	{
		if (_steps.Count == 0)
			return InitialVolume;

		var lastVolume = _steps[^1].Volume;
		decimal nextVolume;

		if (UseVolumeMultiplier)
		{
			nextVolume = lastVolume * VolumeMultiplier;
		}
		else
		{
			nextVolume = lastVolume + VolumeIncrement;
		}

		return nextVolume <= 0m ? InitialVolume : decimal.Round(nextVolume, 6);
	}

	private decimal CalculateCycleProfit(decimal price)
	{
		if (_steps.Count == 0 || Security == null)
			return 0m;

		var priceStep = Security.PriceStep ?? 0m;
		var stepPrice = Security.PriceStep ?? 0m;

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

		decimal pnl = 0m;
		foreach (var step in _steps)
		{
			var diff = price - step.Price;
			var stepsCount = diff / priceStep;
			var direction = step.IsBuy ? 1m : -1m;
			pnl += stepsCount * stepPrice * step.Volume * direction;
		}

		return pnl;
	}

	private bool TryGetPercentTarget(out decimal target)
	{
		target = 0m;
		if (Portfolio == null)
			return false;

		var balance = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
		if (balance <= 0m)
			return false;

		target = balance * PercentTakeProfit / 100m;
		return true;
	}

	private decimal GetPriceOffset(decimal pips)
	{
		if (Security == null)
			return 0m;

		var priceStep = Security.PriceStep ?? 0m;
		return priceStep <= 0m ? 0m : pips * priceStep;
	}

	private void ExecuteOrder(bool isBuy, decimal volume, decimal price)
	{
		if (volume <= 0m)
			return;

		if (isBuy)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}

		_steps.Add(new TradeStep(isBuy, price, volume));
	}

	private void CloseCycle()
	{
		if (Position > 0m)
		{
			SellMarket(Position);
		}
		else if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
		}

		_steps.Clear();
		_nextStepIndex = 0;
		_cycleBasePrice = 0m;
		_peakCycleProfit = 0m;
	}

	private sealed record TradeStep(bool IsBuy, decimal Price, decimal Volume);
}