Ver en GitHub

Estrategia de Área de Recuperación por Zonas

Descripción general

La Estrategia de Área de Recuperación por Zonas es una conversión directa del expert advisor de MetaTrader "Zone Recovery Area" (paquete MQL/20266). Recrea la lógica de cobertura original sobre la API de alto nivel de StockSharp y agrega parametrización exhaustiva para que el comportamiento pueda ajustarse sin modificar el código. La estrategia combina un filtro de tendencia con una cuadrícula de recuperación de compra/venta alternante: una vez que se abre una operación primaria, se apilan posiciones adicionales cada vez que el precio sale o vuelve a entrar en la zona predefinida, creando una cesta cubierta que apunta a recuperar las reducciones flotantes.

Características principales:

  • Utiliza un cruce de media móvil simple rápida/lenta junto con un filtro MACD mensual para definir el sesgo de trading.
  • Implementa la técnica de recuperación por zonas: la primera operación establece un precio base, y las órdenes de cobertura alternantes se activan cada vez que el mercado cruza el límite de la zona o regresa al nivel base.
  • Proporciona controles de beneficio basados en dinero, porcentaje y trailing para salir de la cesta una vez que se ha asegurado suficiente beneficio.
  • Permite tanto el dimensionamiento de posición multiplicativo (estilo martingala) como aditivo para cada paso de recuperación.

Datos de mercado e indicadores

  • Velas principales: marco temporal definido por el usuario (por defecto 30 minutos) para entradas y gestión de recuperación.
  • Velas mensuales: construidas a partir de marcos temporales inferiores si es necesario; usadas para calcular los valores de MACD (12/26/9).
  • Indicadores:
    • Media Móvil Simple (rápida y lenta) en el marco temporal principal.
    • Convergencia/Divergencia de Medias Móviles con línea de señal en el marco temporal mensual.

Lógica de trading

  1. Validación de tendencia
    • Esperar hasta que ambas SMA y el MACD mensual estén completamente formados.
    • Una configuración alcista requiere que la SMA rápida esté por debajo de la lenta en la barra anterior mientras la línea MACD mensual está por encima de su señal.
    • Una configuración bajista requiere que la SMA rápida esté por encima de la lenta en la barra anterior mientras la línea MACD mensual está por debajo de su señal.
  2. Inicialización del ciclo
    • Cuando se detecta una configuración alcista (bajista), abrir la posición larga (corta) inicial con InitialVolume y almacenar el precio de entrada como base del ciclo.
    • Restablecer los contadores internos y el seguimiento de beneficios para el nuevo ciclo.
  3. Motor de recuperación por zonas
    • Definir dos niveles críticos: el límite de zona (ZoneRecoveryPips) alejado del precio base y el nivel de take-profit (TakeProfitPips) en la dirección favorable.
    • Mientras el ciclo está activo, monitorear cada vela completada:
      • Si el precio alcanza el nivel de take-profit, cerrar toda la exposición neta y terminar el ciclo.
      • Si se alcanzan los objetivos de dinero o porcentaje de beneficio, o se activa el bloqueo de beneficio trailing, cerrar el ciclo.
      • De lo contrario, evaluar si se necesita una nueva cobertura:
        • Para ciclos largos: abrir un corto adicional cuando el precio cae por debajo de base - zone, y abrir un largo adicional cuando el precio regresa por encima del precio base.
        • Para ciclos cortos: abrir un largo adicional cuando el precio sube por encima de base + zone, y abrir un corto adicional cuando el precio regresa por debajo del precio base.
      • La dirección de cobertura alterna automáticamente; el tamaño de la próxima orden se determina multiplicando el volumen anterior o añadiendo un incremento fijo.
    • El número de operaciones por cesta está limitado por MaxTrades.
  4. Gestión de beneficios
    • UseMoneyTakeProfit: cerrar la cesta una vez que el beneficio no realizado alcance el importe de divisa configurado.
    • UsePercentTakeProfit: cerrar la cesta una vez que el beneficio no realizado iguale el porcentaje especificado del valor de la cartera.
    • EnableTrailing: una vez que el beneficio supera TrailingStartProfit, seguir el pico y salir del ciclo si el beneficio cae por TrailingDrawdown.

Todas las órdenes se colocan usando los helpers de alto nivel de StockSharp (BuyMarket/SellMarket), lo que mantiene la implementación consistente con las mejores prácticas del framework.

Parámetros

Nombre Por defecto Descripción
CandleType Velas de 30 minutos Marco temporal para entradas y monitoreo de recuperación.
MonthlyCandleType Velas de 30 días Marco temporal superior utilizado para construir el filtro de tendencia MACD.
FastMaLength 20 Período de la SMA rápida.
SlowMaLength 200 Período de la SMA lenta.
TakeProfitPips 150 Distancia desde el precio base para cerrar toda la cesta en beneficio.
ZoneRecoveryPips 50 Semiancho de la zona de cobertura alrededor del precio base.
InitialVolume 1 Volumen de la primera operación en cada ciclo.
UseVolumeMultiplier true Si está habilitado, cada nueva cobertura multiplica el volumen anterior.
VolumeMultiplier 2 Factor aplicado al volumen anterior cuando UseVolumeMultiplier es true.
VolumeIncrement 0.5 Incremento de volumen aditivo cuando UseVolumeMultiplier es false.
MaxTrades 6 Número máximo de operaciones por ciclo de recuperación (incluida la inicial).
UseMoneyTakeProfit false Habilitar take-profit basado en dinero.
MoneyTakeProfit 40 Objetivo de beneficio en divisa de la cuenta.
UsePercentTakeProfit false Habilitar take-profit basado en porcentaje.
PercentTakeProfit 5 Objetivo de beneficio como porcentaje del valor de la cartera.
EnableTrailing true Habilitar protección de beneficio trailing.
TrailingStartProfit 40 Umbral de beneficio requerido antes de que el trailing se active.
TrailingDrawdown 10 Retroceso de beneficio permitido una vez que el trailing está activo.

Conversión de pips: TakeProfitPips y ZoneRecoveryPips se convierten en desplazamientos de precio usando el paso de precio del instrumento. Asegúrese de que el instrumento negociado proporcione valores correctos de PriceStep y StepPrice.

Notas de uso

  1. Agregue la estrategia a su solución StockSharp (Designer, API, Runner, etc.).
  2. Asigne el instrumento y la cartera deseados antes de iniciar.
  3. Ajuste los parámetros para que coincidan con la volatilidad del instrumento, la reducción aceptable y el tamaño de la cuenta.
  4. Asegúrese de que haya suficientes datos históricos para que tanto las SMA como el MACD mensual puedan calentarse antes de la primera operación.
  5. Monitoree cuidadosamente el uso de margen: los pasos de recuperación pueden aumentar rápidamente la exposición, especialmente cuando el multiplicador está habilitado.

Gestión de riesgos y consideraciones

  • Las técnicas de recuperación por zonas/martingala pueden acumular posiciones muy grandes en mercados en tendencia. Siempre pruebe con configuraciones conservadoras y use el parámetro MaxTrades para limitar el riesgo.
  • Dado que StockSharp mantiene una única posición neta, el cálculo interno de beneficios replica el PnL de la cesta usando información de precio/paso del instrumento. Valide las cifras con el feed de datos de su broker.
  • Los objetivos de dinero y porcentaje dependen de la valoración de la cartera. Al realizar backtesting o paper trading, asegúrese de que el modelo de cartera suministre correctamente BeginValue/CurrentValue.
  • No se usa stop-loss duro automático; el riesgo se gestiona a través de la mecánica de recuperación. Considere combinar la estrategia con stops de nivel de cartera externos.

Archivos

  • CS/ZoneRecoveryAreaStrategy.cs — implementación de la estrategia.
  • README.md — documentación en inglés (este archivo).
  • README_ru.md — documentación en ruso.
  • README_zh.md — documentación en chino.
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);
}