GitHub で見る

MacdPatternTraderV01 戦略

概要

MacdPatternTraderV01Strategy は、FORTRADER "MacdPatternTraderv01" MetaTrader 4 エキスパート アドバイザーの忠実な StockSharp ポートです。システムは、オシレーターが極端なレベルまで伸びた後にゼロラインに向かってロールバックした後に現れる MACD フック パターンを探します。買われすぎのスパイクの後に弱気のフックが形成されると、戦略はショートポジションを開きますが、売られ過ぎの下落の後の強気のフックはロングを引き起こします。 StockSharp バージョンでは、再帰的なストップロスとテイクプロフィットのレベルや段階的なポジションのスケーリングなど、元の多層リスク管理が維持されています。

C# 実装では、MACDExponentialMovingAverage、および SimpleMovingAverage インジケーターを備えた高レベルのキャンドル サブスクリプション API を使用します。すべての計算は完成したローソク足で実行され、MQL バージョンからの明示的なバー シフトを伴う iMACD および iMA の呼び出しをミラーリングします。追加のヘルパー ロジックは、最近の高値と安値を手動で追跡し、EA が保護注文に使用する再帰的な価格検索を再現します。

信号ロジック

  1. アーミング条件
    • MACD メインラインが BearishThreshold を超えると、弱気 セットアップが準備されます。アーミングフラグは、MACD がゼロを下回るとすぐにクリアされます。
    • MACD メインラインが BullishThreshold を下回ると、強気 セットアップが準備されます。このフラグは、MACD が正になるとクリアされます。
  2. フック確認
    • ショートエントリーでは、弱気フラグである macd₀ < BearishThresholdmacd₀ < macd₁macd₁ > macd₂ をアクティブにしておく必要があり、macd₀ がゼロより高い間は macd₂ < BearishThreshold が必要です。
    • ロングエントリーでは、macd₀ > BullishThresholdmacd₀ > macd₁macd₁ < macd₂、強気フラグを有効にしておく必要があり、macd₀ がネガティブなままである間は macd₂ > BullishThreshold が必要です。
  3. 注文の実行
    • フックが完了すると、ストラテジーは数量 OrderVolume の成行注文を送信します。計算されたストップロスとテイクプロフィットの価格を、後で監視できるように同時に保存します。

リスク管理

ストップロス

ストップロスは、MQL 関数 StopLoss(type) を模倣します。

  • ショートトレードでは、直近の StopLossBars ローソク足(ただし、新しく閉じた足を除く)の最高値を探し、結果に OffsetPoints * PriceStep を追加します。
  • ロング取引では、同じオフセットを差し引いて、過去の StopLossBars の過去のローソク足の安値を検索します。

このロジックは、大規模なカスタム コレクションの構築を避けるために、上限のあるメモリ内バッファ (1,000 値) に対する手動の極値検索を使用して実装されています。

テイクプロフィット

テイクプロフィットは、再帰的な TakeProfit(type) MQL ルーチンを再現します。

  1. TakeProfitBars 値の最新のブロックから始めます。シグナルをトリガーしたローソク足を含めます。
  2. そのブロック内の極値 (ショートの場合は低く、ロングの場合は高い) を計算します。
  3. TakeProfitBars キャンドル分だけ戻り、新しいブロックがより有利な極端値を生み出すまで繰り返します。
  4. 極端な値を改善しない最初のブロックで停止し、最後に記録された値をテイクプロフィットとして使用します。

部分的なポジション管理

  • エントリー後、戦略は元の出来高とエントリー価格を記録します。
  • 部分的な撤退は、アカウント通貨で表される変動利益が ProfitThreshold を超えた場合にのみ許可されます。
  • ロングポジションの場合:
    1. ローソク足の終値が中値 EMA (EmaMediumPeriod) を上回ったときに、初期出来高の 3 分の 1 を閉じます。
    2. ローソクの高値が SmaPeriodEmaLongPeriod の値の平均を突き抜けたときに、残りのポジションの半分を閉じます。
  • ショート ポジションの場合、ルールは中値 EMA を下回るローソク足と複合平均を下回るローソク足に反映されます。

保護命令はスケーリング前にチェックされ、ハードストップまたはターゲットが常に優先されることが保証されます。

パラメーター

パラメータ デフォルト 説明
StopLossBars 6 ストップロススイング検索の過去のローソク足の数。
TakeProfitBars 20 再帰的テイクプロフィットアルゴリズムで使用されるブロックサイズ。
OffsetPoints 10 ストップロス価格に追加ポイントが追加されます。
MacdFastPeriod 5 MACD インジケーターの高速な EMA 長さ。
MacdSlowPeriod 13 MACD インジケーターの EMA の長さが遅いです。
MacdSignalPeriod 1 MACD インジケーターの EMA の長さを信号します。
BearishThreshold 0.0045 ショートセットアップを準備する正の MACD レベル。
BullishThreshold -0.0045 長いセットアップを準備する負の MACD レベル。
OrderVolume 1 成行注文あたりの出来高。
EmaShortPeriod 7 最初の部分的な終了で使用される高速な EMA。
EmaMediumPeriod 21 中 EMA はフィルタと部分的な終了で使用されます。
SmaPeriod 98 SMA は複合出口平均内で使用されます。
EmaLongPeriod 365 2 番目の部分出口の長い EMA と SMA を組み合わせたもの。
ProfitThreshold 5 スケールアウト前の最小変動利益 (通貨単位)。
CandleType 1時間枠 戦略的に加工されたキャンドルシリーズ。

すべてのパラメータは StrategyParam<T> 経由で公開され、必要に応じて最適化をサポートします。

実装メモ

  • この戦略は、高レベルの SubscribeCandles バインディングのみに依存します。プロジェクトのガイドラインに従って、インジケーターを Indicators コレクションにプッシュしません。
  • MACD 履歴は、iMACD(..., shift) アクセスを模倣するコンパクトな 3 値シフト レジスタ (_macdPrev1..3) を使用して保存されます。
  • 保護価格レベルは小数として追跡されます。ローソク足がストップまたはターゲットに達すると、戦略は成行注文でポジション全体をクローズし、内部ステートマシンをリセットします。
  • 変動損益は PriceStep/StepPrice を使用して推定されるため、商品の価格スケールに関係なく、部分決済しきい値が一定に保たれます。
  • 高値と安値のローソク足バッファーは 1,000 要素に制限されており、これはデフォルトのパラメーターとしては十分ですが、制御されない増加を防ぎます。

使用法

  1. MacdPatternTraderV01Strategy をインスタンス化し、必要なセキュリティ、ポートフォリオ、コネクタを割り当てます。
  2. 必要に応じて、取引商品に合わせて CandleTypeStopLossBars、または OrderVolume などのパラメータを調整します。
  3. 戦略を開始します。設定されたローソク足シリーズをサブスクライブし、チャート上に MACD と取引マーカーを描画し、注文を自動的に管理します。

この戦略には、メンテナンスとさらなるカスタマイズを容易にするために、翻訳された各ブロックを説明する広範なインライン コメントが含まれています。

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>
/// MACD pattern strategy converted from the FORTRADER "MacdPatternTraderv01" expert advisor.
/// </summary>
public class MacdPatternTraderV01Strategy : Strategy
{
	private readonly StrategyParam<int> _stopLossBars;
	private readonly StrategyParam<int> _takeProfitBars;
	private readonly StrategyParam<int> _offsetPoints;
	private readonly StrategyParam<int> _historyLimit;
	private readonly StrategyParam<int> _macdFastPeriod;
	private readonly StrategyParam<int> _macdSlowPeriod;
	private readonly StrategyParam<int> _macdSignalPeriod;
	private readonly StrategyParam<decimal> _bearishThreshold;
	private readonly StrategyParam<decimal> _bullishThreshold;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _emaShortPeriod;
	private readonly StrategyParam<int> _emaMediumPeriod;
	private readonly StrategyParam<int> _smaPeriod;
	private readonly StrategyParam<int> _emaLongPeriod;
	private readonly StrategyParam<decimal> _profitThreshold;
	private readonly StrategyParam<DataType> _candleType;

	private MACD _macd = null!;
	private ExponentialMovingAverage _emaShort = null!;
	private ExponentialMovingAverage _emaMedium = null!;
	private SimpleMovingAverage _sma = null!;
	private ExponentialMovingAverage _emaLong = null!;

	private decimal? _macdPrev1;
	private decimal? _macdPrev2;
	private decimal? _macdPrev3;

	private bool _bearishArmed;
	private bool _bullishArmed;
	private bool _pendingSell;
	private bool _pendingBuy;

	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal? _entryPrice;
	private decimal? _initialVolume;

	private int _buyPartialStage;
	private int _sellPartialStage;

	private readonly List<decimal> _recentLows = new();
	private readonly List<decimal> _recentHighs = new();

	/// <summary>
	/// Number of finished candles used to determine the stop-loss reference high/low.
	/// </summary>
	public int StopLossBars { get => _stopLossBars.Value; set => _stopLossBars.Value = value; }

	/// <summary>
	/// Number of candles used by the recursive take-profit search.
	/// </summary>
	public int TakeProfitBars { get => _takeProfitBars.Value; set => _takeProfitBars.Value = value; }

	/// <summary>
	/// Offset in points added to the calculated stop level.
	/// </summary>
	public int OffsetPoints { get => _offsetPoints.Value; set => _offsetPoints.Value = value; }

	/// <summary>
	/// Number of historical candles stored for swing detection.
	/// </summary>
	public int HistoryLimit { get => _historyLimit.Value; set => _historyLimit.Value = value; }

	/// <summary>
	/// Fast EMA period for the MACD indicator.
	/// </summary>
	public int MacdFastPeriod { get => _macdFastPeriod.Value; set => _macdFastPeriod.Value = value; }

	/// <summary>
	/// Slow EMA period for the MACD indicator.
	/// </summary>
	public int MacdSlowPeriod { get => _macdSlowPeriod.Value; set => _macdSlowPeriod.Value = value; }

	/// <summary>
	/// Signal EMA period for the MACD indicator.
	/// </summary>
	public int MacdSignalPeriod { get => _macdSignalPeriod.Value; set => _macdSignalPeriod.Value = value; }

	/// <summary>
	/// Positive MACD level that arms the bearish hook setup.
	/// </summary>
	public decimal BearishThreshold { get => _bearishThreshold.Value; set => _bearishThreshold.Value = value; }

	/// <summary>
	/// Negative MACD level that arms the bullish hook setup.
	/// </summary>
	public decimal BullishThreshold { get => _bullishThreshold.Value; set => _bullishThreshold.Value = value; }

	/// <summary>
	/// Volume for every market order.
	/// </summary>
	public decimal OrderVolume { get => _orderVolume.Value; set => _orderVolume.Value = value; }

	/// <summary>
	/// EMA period used by the first partial exit condition.
	/// </summary>
	public int EmaShortPeriod { get => _emaShortPeriod.Value; set => _emaShortPeriod.Value = value; }

	/// <summary>
	/// EMA period used by the crossover filter and the the first partial exit.
	/// </summary>
	public int EmaMediumPeriod { get => _emaMediumPeriod.Value; set => _emaMediumPeriod.Value = value; }

	/// <summary>
	/// SMA period participating in the second partial exit.
	/// </summary>
	public int SmaPeriod { get => _smaPeriod.Value; set => _smaPeriod.Value = value; }

	/// <summary>
	/// Long-term EMA period used inside the composite exit average.
	/// </summary>
	public int EmaLongPeriod { get => _emaLongPeriod.Value; set => _emaLongPeriod.Value = value; }

	/// <summary>
	/// Minimal floating profit (in currency) required before partial exits are allowed.
	/// </summary>
	public decimal ProfitThreshold { get => _profitThreshold.Value; set => _profitThreshold.Value = value; }

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

	/// <summary>
	/// Initializes a new instance of <see cref="MacdPatternTraderV01Strategy"/>.
	/// </summary>
	public MacdPatternTraderV01Strategy()
	{
		_stopLossBars = Param(nameof(StopLossBars), 6)
		.SetGreaterThanZero()
		.SetDisplay("Stop-Loss Bars", "Bars used to determine the stop-loss swing", "Risk")
		;

		_takeProfitBars = Param(nameof(TakeProfitBars), 20)
		.SetGreaterThanZero()
		.SetDisplay("Take-Profit Bars", "Bars per block for the recursive take-profit", "Risk")
		;

		_offsetPoints = Param(nameof(OffsetPoints), 10)
		.SetGreaterThanZero()
		.SetDisplay("Stop Offset", "Additional points added to the stop-loss", "Risk");

		_historyLimit = Param(nameof(HistoryLimit), 1000)
		.SetGreaterThanZero()
		.SetDisplay("History Limit", "Number of recent candles stored for swing detection", "Risk");

		_macdFastPeriod = Param(nameof(MacdFastPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("MACD Fast", "Fast EMA length for MACD", "Indicators");

		_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 13)
		.SetGreaterThanZero()
		.SetDisplay("MACD Slow", "Slow EMA length for MACD", "Indicators");

		_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 1)
		.SetGreaterThanZero()
		.SetDisplay("MACD Signal", "Signal EMA length for MACD", "Indicators");

		_bearishThreshold = Param(nameof(BearishThreshold), 50m)
		.SetDisplay("Bearish Threshold", "Positive MACD level that arms short trades", "Signals");

		_bullishThreshold = Param(nameof(BullishThreshold), -50m)
		.SetDisplay("Bullish Threshold", "Negative MACD level that arms long trades", "Signals");

		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Trade volume for every market order", "General");

		_emaShortPeriod = Param(nameof(EmaShortPeriod), 7)
		.SetGreaterThanZero()
		.SetDisplay("EMA Short", "Short EMA period for position management", "Indicators");

		_emaMediumPeriod = Param(nameof(EmaMediumPeriod), 21)
		.SetGreaterThanZero()
		.SetDisplay("EMA Medium", "Medium EMA period for filters", "Indicators");

		_smaPeriod = Param(nameof(SmaPeriod), 98)
		.SetGreaterThanZero()
		.SetDisplay("SMA Period", "SMA period used in the composite exit", "Indicators");

		_emaLongPeriod = Param(nameof(EmaLongPeriod), 365)
		.SetGreaterThanZero()
		.SetDisplay("EMA Long", "Long EMA period used in the composite exit", "Indicators");

		_profitThreshold = Param(nameof(ProfitThreshold), 5m)
		.SetDisplay("Profit Threshold", "Minimum floating profit before scaling out", "Risk");

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

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

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

	_macdPrev1 = null;
	_macdPrev2 = null;
	_macdPrev3 = null;
	_bearishArmed = false;
	_bullishArmed = false;
	_pendingSell = false;
	_pendingBuy = false;
	_stopPrice = null;
	_takePrice = null;
	_entryPrice = null;
	_initialVolume = null;
	_buyPartialStage = 0;
	_sellPartialStage = 0;
	_recentLows.Clear();
	_recentHighs.Clear();
}

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

	Volume = OrderVolume;

	_macd = new MACD();
	_macd.ShortMa.Length = MacdFastPeriod;
	_macd.LongMa.Length = MacdSlowPeriod;

_emaShort = new EMA { Length = EmaShortPeriod };
_emaMedium = new EMA { Length = EmaMediumPeriod };
_sma = new SMA { Length = SmaPeriod };
_emaLong = new EMA { Length = EmaLongPeriod };

var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_macd, _emaShort, _emaMedium, _sma, _emaLong, ProcessCandle)
.Start();

var area = CreateChartArea();
if (area != null)
{
	DrawCandles(area, subscription);
	DrawIndicator(area, _macd);
	DrawIndicator(area, _emaMedium);
	DrawOwnTrades(area);
}
}

private void ProcessCandle(ICandleMessage candle, decimal macdLine, decimal emaShortValue, decimal emaMediumValue, decimal smaValue, decimal emaLongValue)
{
	if (candle.State != CandleStates.Finished)
	return;

		// Check protective targets before generating new signals.
	HandleProtectiveExits(candle);
		// Try scaling out according to the EMA/SMA management rules.
	HandlePartialExits(candle, emaMediumValue, smaValue, emaLongValue);

	if (!_macd.IsFormed)
	{
		// Keep collecting MACD values while the indicator is not formed yet.
		UpdateMacdHistory(macdLine);
		StoreCandleExtremes(candle);
		return;
	}

	// Store the freshly calculated MACD value for pattern detection.
UpdateMacdHistory(macdLine);

if (_macdPrev1 is null || _macdPrev2 is null || _macdPrev3 is null)
{
	StoreCandleExtremes(candle);
	return;
}

var macdCurr = _macdPrev1.Value;
var macdLast = _macdPrev2.Value;
var macdLast3 = _macdPrev3.Value;

	// A new bullish MACD extreme arms the potential bearish hook sequence.
if (macdCurr > BearishThreshold)
_bearishArmed = true;

if (macdCurr < 0m)
_bearishArmed = false;

if (macdCurr < BearishThreshold && macdCurr < macdLast && macdLast > macdLast3 && _bearishArmed && macdCurr > 0m && macdLast3 < BearishThreshold)
{
	_pendingSell = true;
}

	// Execute a short entry once all bearish requirements are met.
if (_pendingSell)
{
	TryEnterShort(candle);
}

	// A deep negative MACD swing arms the bullish hook setup.
if (macdCurr < BullishThreshold)
_bullishArmed = true;

if (macdCurr > 0m)
_bullishArmed = false;

if (macdCurr > BullishThreshold && macdCurr < 0m && macdCurr > macdLast && macdLast < macdLast3 && _bullishArmed && macdLast3 > BullishThreshold)
{
	_pendingBuy = true;
}

	// Execute a long entry once all bullish requirements are satisfied.
if (_pendingBuy)
{
	TryEnterLong(candle);
}

StoreCandleExtremes(candle);
}

private void HandleProtectiveExits(ICandleMessage candle)
{
	if (Position > 0)
	{
		if (_stopPrice is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket(Position);
			ResetPositionState();
			return;
		}

	if (_takePrice is decimal take && candle.HighPrice >= take)
	{
		SellMarket(Position);
		ResetPositionState();
		return;
	}
}
else if (Position < 0)
{
	var volume = Math.Abs(Position);

	if (_stopPrice is decimal stop && candle.HighPrice >= stop)
	{
		BuyMarket(volume);
		ResetPositionState();
		return;
	}

if (_takePrice is decimal take && candle.LowPrice <= take)
{
	BuyMarket(volume);
	ResetPositionState();
}
}
}

private void HandlePartialExits(ICandleMessage candle, decimal emaMediumValue, decimal smaValue, decimal emaLongValue)
{
	if (Position == 0 || _entryPrice is null)
	return;

	// Evaluate the floating PnL to decide whether partial exits are allowed.
	var profit = CalculateFloatingProfit(candle.ClosePrice);
	if (profit < ProfitThreshold)
	return;

	if (Position > 0)
	{
		if (_buyPartialStage == 0 && candle.ClosePrice > emaMediumValue)
		{
			var volume = GetInitialPortionVolume(3m);
			if (volume > 0m)
			{
				SellMarket(Math.Min(volume, Position));
				_buyPartialStage = 1;
			}
	}
else if (_buyPartialStage == 1)
{
		// Combine the slow averages to reproduce the MQL composite threshold.
	var composite = (smaValue + emaLongValue) / 2m;
	if (candle.HighPrice > composite)
	{
		var volume = Math.Abs(Position) / 2m;
		if (volume > 0m)
		{
			SellMarket(volume);
			_buyPartialStage = 2;
		}
}
}
}
else if (Position < 0)
{
	var absPosition = Math.Abs(Position);

	if (_sellPartialStage == 0 && candle.ClosePrice < emaMediumValue)
	{
		var volume = GetInitialPortionVolume(3m);
		if (volume > 0m)
		{
			BuyMarket(Math.Min(volume, absPosition));
			_sellPartialStage = 1;
		}
}
else if (_sellPartialStage == 1)
{
	var composite = (smaValue + emaLongValue) / 2m;
	if (candle.LowPrice < composite)
	{
		var volume = absPosition / 2m;
		if (volume > 0m)
		{
			BuyMarket(volume);
			_sellPartialStage = 2;
		}
}
}
}
}

private decimal GetInitialPortionVolume(decimal divider)
{
	if (_initialVolume is null || _initialVolume.Value <= 0m)
	return 0m;

	return _initialVolume.Value / divider;
}

private decimal CalculateFloatingProfit(decimal price)
{
	if (_entryPrice is null || Security is null)
	return 0m;

	var positionVolume = Math.Abs(Position);
	if (positionVolume == 0m)
	return 0m;

	var priceStep = Security.PriceStep ?? 0m;
	var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? priceStep;

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

	var diff = price - _entryPrice.Value;
	var steps = diff / priceStep;
	var money = steps * stepPrice * positionVolume;

	return Position > 0 ? money : -money;
}

private void TryEnterShort(ICandleMessage candle)
{
	_pendingSell = false;
	_bearishArmed = false;

	if (Position < 0)
	return;

	if (OrderVolume <= 0m)
	return;

	// Replicate the MQL stop-loss that scans past highs plus the configured offset.
	var stop = CalculateStopPrice(false);
	// Compute the recursive take-profit using the finished candle lows.
	var take = CalculateTakePrice(false, candle);

	if (stop is null || take is null)
	return;

	SellMarket(OrderVolume);

	_stopPrice = stop;
	_takePrice = take;
	_entryPrice = candle.ClosePrice;
	_initialVolume = OrderVolume;
	_sellPartialStage = 0;
	_buyPartialStage = 0;
}

private void TryEnterLong(ICandleMessage candle)
{
	_pendingBuy = false;
	_bullishArmed = false;

	if (Position > 0)
	return;

	if (OrderVolume <= 0m)
	return;

	// Derive the long stop-loss from the recent swing lows.
	var stop = CalculateStopPrice(true);
	// Search for the layered bullish target via the recursive high scan.
	var take = CalculateTakePrice(true, candle);

	if (stop is null || take is null)
	return;

	BuyMarket(OrderVolume);

	_stopPrice = stop;
	_takePrice = take;
	_entryPrice = candle.ClosePrice;
	_initialVolume = OrderVolume;
	_buyPartialStage = 0;
	_sellPartialStage = 0;
}

private decimal? CalculateStopPrice(bool isLong)
{
	var length = StopLossBars;
	// Invalid configuration means the level cannot be evaluated.
	if (length <= 0)
	return null;

	if (isLong)
	{
		// Not enough historical lows to reproduce the original lookback.
		if (_recentLows.Count < length)
		return null;

			// Manual iteration avoids allocating additional buffers for extrema search.
		var min = decimal.MaxValue;
		for (var i = _recentLows.Count - length; i < _recentLows.Count; i++)
		{
			var value = _recentLows[i];
			if (value < min)
			min = value;
		}

	var offset = GetOffsetPrice();
	return min - offset;
}
else
{
	if (_recentHighs.Count < length)
	return null;

			// Mirror the same manual search for the highest value.
	var max = decimal.MinValue;
	for (var i = _recentHighs.Count - length; i < _recentHighs.Count; i++)
	{
		var value = _recentHighs[i];
		if (value > max)
		max = value;
	}

var offset = GetOffsetPrice();
return max + offset;
}
}

private decimal? CalculateTakePrice(bool isLong, ICandleMessage candle)
{
	var length = TakeProfitBars;
	if (length <= 0)
	return null;

	var totalWithCurrent = _recentHighs.Count + 1;
	if (totalWithCurrent < length)
	return null;
		// Iterate over consecutive blocks until the extrema stop improving.
		decimal? best = null;
	var segment = 0;
	while (true)
	{
			// Evaluate the next block by combining historical data with the current candle.
		var extreme = isLong
		? GetSegmentExtreme(_recentHighs, candle.HighPrice, length, segment, false)
		: GetSegmentExtreme(_recentLows, candle.LowPrice, length, segment, true);

		if (extreme is null)
		break;

		if (best is null)
		{
			best = extreme;
			segment++;
			continue;
		}

	if (isLong)
	{
		if (extreme.Value > best.Value)
		{
			best = extreme;
			segment++;
			continue;
		}
}
else
{
	if (extreme.Value < best.Value)
	{
		best = extreme;
		segment++;
		continue;
	}
}

break;
}

return best;
}

private decimal? GetSegmentExtreme(List<decimal> source, decimal currentValue, int length, int segmentIndex, bool isMin)
{
	// Treat the finished candle as an extra sample appended to the stored history.
	var total = source.Count + 1;
	var end = total - 1 - segmentIndex * length;
	var start = end - length + 1;

	if (start < 0)
	return null;

	decimal extreme = isMin ? decimal.MaxValue : decimal.MinValue;

	for (var i = start; i <= end; i++)
	{
		var value = i == source.Count ? currentValue : source[i];

		if (isMin)
		{
			if (value < extreme)
			extreme = value;
		}
	else
	{
		if (value > extreme)
		extreme = value;
	}
}

return extreme;
}

private decimal GetOffsetPrice()
{
	// Convert the configured offset from points into an absolute price distance.
	var priceStep = Security?.PriceStep ?? 0m;
	if (priceStep <= 0m)
	return 0m;

	return OffsetPoints * priceStep;
}

private void StoreCandleExtremes(ICandleMessage candle)
{
	_recentLows.Add(candle.LowPrice);
	_recentHighs.Add(candle.HighPrice);

	if (_recentLows.Count > HistoryLimit)
	_recentLows.RemoveAt(0);

	if (_recentHighs.Count > HistoryLimit)
	_recentHighs.RemoveAt(0);
}

private void ResetPositionState()
{
	_stopPrice = null;
	_takePrice = null;
	_entryPrice = null;
	_initialVolume = null;
	_buyPartialStage = 0;
	_sellPartialStage = 0;
}

private void UpdateMacdHistory(decimal macdLine)
{
	_macdPrev3 = _macdPrev2;
	_macdPrev2 = _macdPrev1;
	_macdPrev1 = macdLine;
}
}