Auf GitHub ansehen

MacdPatternTraderV01 Strategie

Überblick

MacdPatternTraderV01Strategy ist eine originalgetreue StockSharp-Portierung des FORTRADER „MacdPatternTraderv01“ MetaTrader 4-Expertenberaters. Das System sucht nach MACD-Hook-Mustern, die auftreten, nachdem der Oszillator ein extremes Niveau erreicht hat und dann in Richtung der Nulllinie zurückrollt. Wenn sich nach einem überkauften Anstieg ein bärischer Haken bildet, eröffnet die Strategie Short-Positionen, während ein bullischer Haken nach einem überverkauften Rückgang Long-Positionen auslöst. Die StockSharp-Version behält das ursprüngliche mehrschichtige Risikomanagement bei, einschließlich rekursiver Stop-Loss- und Take-Profit-Levels sowie einer abgestuften Positionsskalierung.

Die C#-Implementierung verwendet das High-Level-Kerzenabonnement API mit den Indikatoren MACD, ExponentialMovingAverage und SimpleMovingAverage. Alle Berechnungen werden an fertigen Kerzen durchgeführt und spiegeln die Aufrufe iMACD und iMA mit expliziten Balkenverschiebungen aus der Version MQL wider. Zusätzliche Hilfslogik verfolgt manuell die jüngsten Höchst- und Tiefststände, um die rekursiven Preissuchen zu reproduzieren, die EA für Schutzaufträge verwendet.

Signallogik

  1. Scharfschaltbedingungen
    • Ein bärisches Setup wird aktiviert, sobald die MACD-Hauptlinie BearishThreshold überschreitet. Das Scharfschaltflag wird gelöscht, sobald MACD unter Null fällt.
    • Ein bullisches Setup wird aktiviert, sobald die MACD-Hauptlinie unter BullishThreshold fällt. Das Flag wird gelöscht, wenn MACD positiv wird.
  2. Hook-Bestätigung
    • Für kurze Einstiege ist es erforderlich, dass macd₀ < BearishThreshold, macd₀ < macd₁, macd₁ > macd₂, die bärische Flagge aktiv bleibt, und macd₂ < BearishThreshold, während macd₀ über Null bleibt.
    • Lange Einträge erfordern macd₀ > BullishThreshold, macd₀ > macd₁, macd₁ < macd₂, die bullische Flagge, um aktiv zu bleiben, und macd₂ > BullishThreshold, während macd₀ negativ bleibt.
  3. Auftragsausführung
    • Wenn der Hook abgeschlossen ist, sendet die Strategie eine Marktorder mit dem Volumen OrderVolume. Gleichzeitig werden die berechneten Stop-Loss- und Take-Profit-Preise zur späteren Überwachung gespeichert.

Risikomanagement

Stop-Loss

Der Stop-Loss ahmt die MQL-Funktion StopLoss(type) nach:

  • Short-Trades suchen nach dem höchsten Hoch der letzten StopLossBars Kerzen ausgenommen des frisch geschlossenen Balkens und addieren dann OffsetPoints * PriceStep zum Ergebnis.
  • Long-Trades suchen nach dem niedrigsten Tief der letzten StopLossBars historischen Kerzen und subtrahieren den gleichen Offset.

Diese Logik wird mit manuellen Extremwertsuchen über einen begrenzten In-Memory-Puffer (1.000 Werte) implementiert, um den Aufbau großer benutzerdefinierter Sammlungen zu vermeiden.

Take-Profit

Der Take-Profit reproduziert die rekursive Routine TakeProfit(type) MQL:

  1. Beginnen Sie mit dem neuesten Block von TakeProfitBars-Werten. Fügen Sie die Kerze hinzu, die das Signal ausgelöst hat.
  2. Berechnen Sie das Extrem (niedrig für Short-Positionen, hoch für Long-Positionen) innerhalb dieses Blocks.
  3. Gehen Sie um TakeProfitBars Kerzen zurück und wiederholen Sie den Vorgang, während der neue Block ein günstigeres Extrem liefert.
  4. Stoppen Sie beim ersten Block, der das Extrem nicht verbessert, und verwenden Sie den zuletzt aufgezeichneten Wert als Take-Profit.

Teilpositionsverwaltung

  • Nach dem Einstieg erfasst die Strategie das ursprüngliche Volumen und den Einstiegspreis.
  • Teilausstiege sind erst zulässig, wenn der in der Kontowährung ausgedrückte variable Gewinn ProfitThreshold übersteigt.
  • Für Long-Positionen:
    1. Schließen Sie ein Drittel des ursprünglichen Volumens, wenn der Schlusskurs der Kerze über den Mittelwert EMA (EmaMediumPeriod) steigt.
    2. Schließen Sie die Hälfte der verbleibenden Position, wenn das Kerzenhoch den Durchschnitt der Werte SmaPeriod und EmaLongPeriod durchbricht.
  • Für Short-Positionen spiegeln sich die Regeln wider, wobei der Kerzenschluss unter dem Mittelwert EMA und der Kerzentiefstwert unter dem zusammengesetzten Durchschnitt liegt.

Schutzbefehle werden vor der Skalierung überprüft, um sicherzustellen, dass harte Stopps oder Ziele immer Vorrang haben.

Parameter

Parameter Standard Beschreibung
StopLossBars 6 Anzahl der historischen Kerzen für die Stop-Loss-Swing-Suche.
TakeProfitBars 20 Blockgröße, die vom rekursiven Take-Profit-Algorithmus verwendet wird.
OffsetPoints 10 Zusätzliche Punkte werden zum Stop-Loss-Preis hinzugefügt.
MacdFastPeriod 5 Schnelle EMA-Länge des MACD-Indikators.
MacdSlowPeriod 13 Langsame EMA-Länge des MACD-Indikators.
MacdSignalPeriod 1 Signallänge EMA des Indikators MACD.
BearishThreshold 0,0045 Positiver MACD-Pegel, der kurze Setups aktiviert.
BullishThreshold -0,0045 Negativer MACD-Pegel, der lange Setups aktiviert.
OrderVolume 1 Volumen pro Market-Order.
EmaShortPeriod 7 Schnelles EMA wird im ersten Teilexit verwendet.
EmaMediumPeriod 21 Mittel EMA wird in Filtern und Teilexits verwendet.
SmaPeriod 98 SMA wird im zusammengesetzten Exit-Durchschnitt verwendet.
EmaLongPeriod 365 Langes EMA kombiniert mit dem SMA für den zweiten Teilausstieg.
ProfitThreshold 5 Minimaler variabler Gewinn (in Währungseinheiten) vor der Skalierung.
CandleType 1-stündiger Zeitrahmen Von der Strategie verarbeitete Kerzenserie.

Alle Parameter werden über StrategyParam<T> verfügbar gemacht und unterstützen gegebenenfalls die Optimierung.

Implementierungshinweise

  • Die Strategie basiert ausschließlich auf SubscribeCandles-Bindungen auf hoher Ebene. Gemäß den Projektrichtlinien werden keine Indikatoren in die Indicators-Sammlung verschoben.
  • Der MACD-Verlauf wird mithilfe eines kompakten dreiwertigen Schieberegisters (_macdPrev1..3) gespeichert, um den iMACD(..., shift)-Zugriff nachzuahmen.
  • Schutzpreisniveaus werden als Dezimalzahlen verfolgt; Wenn Kerzen einen Stop oder ein Ziel erreichen, schließt die Strategie die gesamte Position mit Marktaufträgen und setzt die interne Zustandsmaschine zurück.
  • Der variable PnL wird unter Verwendung von PriceStep/StepPrice geschätzt, sodass der Teilausstiegsschwellenwert unabhängig von der Preisskala des Instruments konsistent bleibt.
  • Die Kerzenpuffer für Hochs und Tiefs sind auf 1.000 Elemente begrenzt, was für die Standardparameter ausreichend ist, aber unkontrolliertes Wachstum verhindert.

Nutzung

  1. Instanziieren Sie MacdPatternTraderV01Strategy und weisen Sie die gewünschte Sicherheit, das gewünschte Portfolio und den gewünschten Connector zu.
  2. Passen Sie optional Parameter wie CandleType, StopLossBars oder OrderVolume an das gehandelte Instrument an.
  3. Starten Sie die Strategie; Es abonniert die konfigurierte Kerzenserie, zeichnet MACD und handelt mit Markierungen im Diagramm und verwaltet Aufträge automatisch.

Die Strategie enthält ausführliche Inline-Kommentare, die jeden übersetzten Block beschreiben, um die Wartung und weitere Anpassung zu erleichtern.

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;
}
}