GitHub で見る

ゾーン回復エリア戦略

概要

ゾーン回復エリア戦略は、MetaTrader エキスパートアドバイザー「Zone Recovery Area」(パッケージ MQL/20266)の直接変換です。StockSharp の高レベル API 上で元のヘッジロジックを再現し、コードに触れることなく動作を調整できるよう詳細なパラメータ化を追加しています。この戦略はトレンドフィルターと交互の買い/売り回復グリッドを組み合わせています:最初のトレードが開かれると、価格が事前定義されたゾーンを出たり再び入ったりするたびに追加ポジションが積み重ねられ、浮動ドローダウンの回復を目指したヘッジバスケットを作成します。

主な特徴:

  • 高速/低速単純移動平均クロスオーバーと月次 MACD フィルターを組み合わせてトレードバイアスを定義します。
  • ゾーン回復テクニックを実装:最初のトレードがベース価格を確立し、市場がゾーン境界を越えるかベースレベルに戻るたびに交互のヘッジオーダーが発動されます。
  • 十分な利益が確保されたらバスケットを終了するための資金ベース、パーセンテージベース、トレーリング利益管理を提供します。
  • 各回復ステップに対して乗算的(マーチンゲールスタイル)と加算的両方のポジションサイジングを許可します。

市場データと指標

  • プライマリローソク足: エントリーと回復管理のためのユーザー定義タイムフレーム(デフォルト 30 分)。
  • 月次ローソク足: 必要に応じてより低いタイムフレームから構築;MACD (12/26/9) 値の計算に使用。
  • 指標:
    • プライマリタイムフレームの単純移動平均(高速と低速)。
    • 月次タイムフレームのシグナルラインつき Moving Average Convergence Divergence。

トレードロジック

  1. トレンド検証
    • 両方の SMA と月次 MACD が完全に形成されるまで待機。
    • 強気セットアップ:前のバーで高速 SMA が低速 SMA を下回り、月次 MACD ラインがシグナルを上回っている必要があります。
    • 弱気セットアップ:前のバーで高速 SMA が低速 SMA を上回り、月次 MACD ラインがシグナルを下回っている必要があります。
  2. サイクル初期化
    • 強気(弱気)セットアップが検出された場合、InitialVolume で最初のロング(ショート)ポジションを開き、エントリー価格をサイクルベースとして保存。
    • 新しいサイクルの内部カウンターと利益追跡をリセット。
  3. ゾーン回復エンジン
    • 2 つの重要なレベルを定義:ベース価格から離れたゾーン境界ZoneRecoveryPips)と有利な方向のテイクプロフィットレベルTakeProfitPips)。
    • サイクルがアクティブな間、完了した各ローソク足を監視:
      • 価格がテイクプロフィットレベルに達したら、すべてのネットエクスポージャーを閉じてサイクルを終了。
      • 資金またはパーセンテージの利益目標が達成されるか、トレーリング利益ロックが作動したら、サイクルを閉じる。
      • それ以外の場合、新しいヘッジが必要かどうかを評価:
        • ロングサイクルの場合:価格が base - zone を下回ったら追加ショートを開き、価格がベース価格を再び上回ったら追加ロングを開く。
        • ショートサイクルの場合:価格が base + zone を上回ったら追加ロングを開き、価格がベース価格の下に戻ったら追加ショートを開く。
      • ヘッジ方向は自動的に交互になる;次の注文サイズは前のボリュームを乗算するか固定増分を加えることで決定されます。
    • バスケットごとのトレード数は MaxTrades によって制限されます。
  4. 利益管理
    • UseMoneyTakeProfit: 未実現利益が設定された通貨額に達したらバスケットを閉じる。
    • UsePercentTakeProfit: 未実現利益がポートフォリオ価値の指定パーセンテージと等しくなったらバスケットを閉じる。
    • EnableTrailing: 利益が TrailingStartProfit を超えたらピークを追跡し、利益が TrailingDrawdown だけ落ちたらサイクルを終了する。

すべての注文は StockSharp の高レベルヘルパー(BuyMarket/SellMarket)を使用して行われ、フレームワークのベストプラクティスと一致した実装を維持します。

パラメーター

名前 デフォルト 説明
CandleType 30 分ローソク足 エントリーと回復監視のためのタイムフレーム。
MonthlyCandleType 30 日ローソク足 MACD トレンドフィルターを構築するために使用される上位タイムフレーム。
FastMaLength 20 高速 SMA の期間。
SlowMaLength 200 低速 SMA の期間。
TakeProfitPips 150 利益でバスケット全体を閉じるためのベース価格からの距離。
ZoneRecoveryPips 50 ベース価格周辺のヘッジゾーンの半幅。
InitialVolume 1 各サイクルの最初のトレードのボリューム。
UseVolumeMultiplier true 有効な場合、新しいヘッジごとに前のボリュームが乗算されます。
VolumeMultiplier 2 UseVolumeMultiplier が true の場合に前のボリュームに適用される係数。
VolumeIncrement 0.5 UseVolumeMultiplier が false の場合の加算的ボリューム増加。
MaxTrades 6 回復サイクルごとの最大トレード数(最初のものを含む)。
UseMoneyTakeProfit false 資金ベースのテイクプロフィットを有効にする。
MoneyTakeProfit 40 口座通貨での利益目標。
UsePercentTakeProfit false パーセンテージベースのテイクプロフィットを有効にする。
PercentTakeProfit 5 ポートフォリオ価値のパーセンテージとしての利益目標。
EnableTrailing true トレーリング利益保護を有効にする。
TrailingStartProfit 40 トレーリングが有効になる前に必要な利益閾値。
TrailingDrawdown 10 トレーリングがアクティブになった後の許容利益後退。

ピップ変換: TakeProfitPipsZoneRecoveryPips は銘柄の価格ステップを使用して価格オフセットに変換されます。取引される銘柄が正しい PriceStepStepPrice 値を提供することを確認してください。

使用上の注意

  1. StockSharp ソリューション(Designer、API、Runner など)に戦略を追加する。
  2. 開始前に希望の銘柄とポートフォリオを割り当てる。
  3. 銘柄のボラティリティ、許容ドローダウン、口座サイズに合わせてパラメーターを調整する。
  4. 最初のトレードの前に両方の SMA と月次 MACD がウォームアップできるよう十分な履歴データを確保する。
  5. 証拠金使用を注意深く監視する:乗数が有効な場合、回復ステップは急速にエクスポージャーを増加させる可能性があります。

リスク管理と考慮事項

  • ゾーン回復/マーチンゲール手法はトレンド相場で非常に大きなポジションを蓄積する可能性があります。常に保守的な設定でテストし、リスクを限定するために MaxTrades パラメーターを使用してください。
  • StockSharp は単一のネットポジションを維持するため、内部の利益計算は銘柄の価格/ステップ情報を使用してバスケット PnL を再現します。ブローカーのデータフィードで数値を検証してください。
  • 資金とパーセンテージ目標はポートフォリオの評価に依存します。バックテストまたはペーパートレードの場合、ポートフォリオモデルが BeginValue/CurrentValue を正しく供給していることを確認してください。
  • 自動的なハードストップロスは使用されません;リスクは回復メカニクスを通じて管理されます。外部ポートフォリオレベルのストップと戦略を組み合わせることを検討してください。

ファイル

  • CS/ZoneRecoveryAreaStrategy.cs — 戦略の実装。
  • README.md — 英語ドキュメント(このファイル)。
  • README_ru.md — ロシア語ドキュメント。
  • README_zh.md — 中国語ドキュメント。
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);
}