GitHub で見る

Hercules A.T.C. 2006戦略

概要

Hercules A.T.C. 2006は、2006年に公開されたMetaTraderエキスパートアドバイザーを再現する高時間軸のトレンドフォロー戦略です。 StockSharpバージョンは、プライマリ時間軸で完了したローソク足を監視し、高速EMA(1)と低速SMA(72)の強気/弱気クロスオーバーを 検出し、追加フィルターがブレイクアウトを確認した場合にのみトレードを開始します。戦略は独立したテイクプロフィットレベルを 持つ2つのトランシェにポジションを分割し、価格が進むとストップをトレーリングします。

インジケーターとデータ

  • プライマリローソク足: 設定可能(デフォルト: 1時間ローソク足)。
  • 高速MA: 長さ FastMaPeriod のEMA(デフォルト: 1)。
  • 低速MA: 長さ SlowMaPeriod のSMA(デフォルト: 72)。
  • RSIフィルター: RsiTimeFrame(デフォルト: 1時間)上の長さ RsiLength のRSI。
  • 日次エンベロープ: DailyEnvelopeTimeFrame 上の長さ DailyEnvelopePeriod のSMA、 ±DailyEnvelopeDeviation パーセントのオフセット付き。
  • H4エンベロープ: H4EnvelopeTimeFrame 上の長さ H4EnvelopePeriod のSMA、 ±H4EnvelopeDeviation パーセントのオフセット付き。
  • ローリング高値/安値: プライマリ時間軸での過去 HighLowHours 時間の最高値と最安値。

パラメーター

名前 デフォルト値 説明
TriggerPips 38 注文を発動する前にクロスオーバー価格に加算/減算するpipsのオフセット。
TrailingStopPips 90 トレーリングストップ距離(pips)(0でトレーリングを無効化)。
TakeProfit1Pips 210 ポジションの半分を削減する最初のテイクプロフィット距離(pips)。
TakeProfit2Pips 280 残りのポジションを閉じる最終テイクプロフィット距離(pips)。
FastMaPeriod 1 クロスオーバー検出器で使用する高速EMAの長さ。
SlowMaPeriod 72 低速SMAベースラインの長さ。
StopLossLookback 4 初期ストップ価格を計算するために使用する完了ローソク足の数。
HighLowHours 10 ブレイクアウトフィルターに使用するローリングウィンドウのサイズ(時間)。
BlackoutHours 144 トレードがクローズした後、新しいエントリーが許可されるまでのクールダウン期間(時間)。
RsiLength 10 上位時間軸フィルターのRSI長さ。
RsiUpper 55 ロングエントリーを許可するために必要な最小RSI値。
RsiLower 45 ショートエントリーをブロックする前に許可される最大RSI値。
DailyEnvelopePeriod 24 日次エンベロープフィルターのSMA長さ。
DailyEnvelopeDeviation 0.99 日次エンベロープの偏差(パーセント)。
H4EnvelopePeriod 96 4時間エンベロープフィルターのSMA長さ。
H4EnvelopeDeviation 0.1 4時間エンベロープの偏差(パーセント)。
CandleType 1時間 プライマリ作業ローソク足タイプ。
RsiTimeFrame 1時間 RSIフィルターに使用するローソク足タイプ。
DailyEnvelopeTimeFrame 1日 日次エンベロープに使用するローソク足タイプ。
H4EnvelopeTimeFrame 4時間 4時間エンベロープに使用するローソク足タイプ。

取引ルール

  1. クロスオーバー検出

    • 最後の3つの完了したバーのEMA(1)とSMA(72)の値を監視します。
    • EMAが前2バーのいずれかでSMAを上回るクロスをした場合、強気シグナルを検出。
    • EMAが前2バーのいずれかでSMAを下回るクロスをした場合、弱気シグナルを検出。
    • クロスオーバー価格(高速と低速の値の平均)を保存し、2バーのトリガーウィンドウを開始。
  2. トリガー条件

    • TriggerPrice = CrossPrice ± TriggerPips(価格単位に変換)を計算。
    • トリガーはクロスオーバー時刻後の2プライマリローソク足の間有効。
    • ロングはローソク足の高値が強気トリガー価格に達するか超える必要があります。
    • ショートはローソク足の安値が弱気トリガー価格に達するか割り込む必要があります。
  3. エントリーフィルター

    • 既存のポジションなし、アクティブなクールダウン(BlackoutHours)なし。
    • RSIフィルター: ロングは RSI > RsiUpper、ショートは RSI < RsiLower
    • ブレイクアウトフィルター: 現在の終値がロングはローリング高値を超えるか、ショートはローリング安値を下回る必要があります。
    • エンベロープ確認: 現在の終値がロングは両方の上側エンベロープバンドの上にあるか、ショートは両方の下側バンドの下にある必要があります。
  4. 注文実行

    • 戦略ボリューム(デフォルト: 2ユニット、つまり2つの等しいサブポジション)を使用して成行注文を送信。
    • ストップロス: StopLossLookback番目のローソク足の安値(ロング)または高値(ショート)。
    • テイクプロフィットレベル: 前半分に TakeProfit1Pips、残りに TakeProfit2Pips
    • BlackoutHours 時間の間新しいエントリーをブロックするためのブラックアウトタイマーを開始。
  5. ポジション管理

    • TrailingStopPips > 0 の場合、トレーリングストップは即座に有効になり、トレードに有利な方向にのみ動きます。
    • 最初のテイクプロフィットレベルでポジションの半分をスケールアウト。
    • 最終テイクプロフィットがトリガーされたとき、ストップロスに達したとき、またはトレーリングストップを価格が超えたときに残りのポジションをクローズ。

リスク管理

  • ストップは常に完了したローソク足から導出され、バー内のノイズを低減します。
  • 2つのテイクプロフィット目標により、トレードを続ける前に部分的な利益を確保します。
  • トレーリングストップは、市場が望む方向に動いた後、利益が保護されることを保証します。
  • 長いブラックアウト期間(デフォルト: 144時間)はブレイクアウト後の急速な再エントリーを防ぎ、元のEAの動作を反映します。

備考

  • StockSharpのポートは、戦略ボリュームをデフォルトで2ユニットに設定することで元のマネーマネジメントのアイデアを保持しており、部分エグジットでポジションの半分が引き続き実行されます。
  • MetaTraderのエンベロープシフト値は、高レベルAPIが前方シフトをサポートしていないため、最新のエンベロープ値を使用して近似されます。
  • 戦略はpip距離を正しく変換するために価格ステップ情報を必要とします。銘柄のメタデータが入力されていることを確認してください。
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>
/// Port of the Hercules A.T.C. 2006 MetaTrader expert advisor.
/// Detects EMA/SMA crossovers with trigger windows and multiple filters
/// before submitting two staged take-profit orders and applying a trailing stop.
/// </summary>
public class HerculesATC2006Strategy : Strategy
{
	private readonly StrategyParam<int> _triggerPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _takeProfit1Pips;
	private readonly StrategyParam<int> _takeProfit2Pips;
	private readonly StrategyParam<int> _fastMaPeriod;
	private readonly StrategyParam<int> _slowMaPeriod;
	private readonly StrategyParam<int> _stopLossLookback;
	private readonly StrategyParam<int> _highLowHours;
	private readonly StrategyParam<int> _blackoutHours;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _rsiUpper;
	private readonly StrategyParam<decimal> _rsiLower;
	private readonly StrategyParam<int> _dailyEnvelopePeriod;
	private readonly StrategyParam<decimal> _dailyEnvelopeDeviation;
	private readonly StrategyParam<int> _h4EnvelopePeriod;
	private readonly StrategyParam<decimal> _h4EnvelopeDeviation;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _rsiTimeFrame;
	private readonly StrategyParam<DataType> _dailyEnvelopeTimeFrame;
	private readonly StrategyParam<DataType> _h4EnvelopeTimeFrame;

	private readonly RelativeStrengthIndex _rsi = new();
	private readonly SimpleMovingAverage _dailyEnvelopeMa = new();
	private readonly SimpleMovingAverage _h4EnvelopeMa = new();

	private readonly decimal[] _fastHistory = new decimal[4];
	private readonly decimal[] _slowHistory = new decimal[4];
	private readonly DateTimeOffset[] _timeHistory = new DateTimeOffset[4];
	private int _historyCount;

	private readonly decimal[] _highStopHistory = new decimal[5];
	private readonly decimal[] _lowStopHistory = new decimal[5];
	private int _stopHistoryCount;

	private readonly Queue<decimal> _recentHighs = new();
	private readonly Queue<decimal> _recentLows = new();
	private decimal _rollingHigh;
	private decimal _rollingLow;

	private decimal _priceStep;
	private decimal _pipSize;
	private TimeSpan _primaryTimeFrame;
	private int _highLowLength;

	private int _pendingDirection;
	private decimal _triggerPrice;
	private DateTimeOffset? _windowEndTime;
	private decimal _crossPrice;

	private decimal _lastRsi;
	private bool _rsiReady;

	private decimal _dailyUpper;
	private decimal _dailyLower;
	private bool _dailyReady;

	private decimal _h4Upper;
	private decimal _h4Lower;
	private bool _h4Ready;

	private DateTimeOffset? _blackoutUntil;

	private decimal? _entryPrice;
	private decimal? _stopLoss;
	private decimal? _tp1;
	private decimal? _tp2;
	private decimal? _trailingStop;
	private bool _tp1Hit;

	/// <summary>
	/// Number of pips added to the crossover price to form the trigger level.
	/// </summary>
	public int TriggerPips
	{
		get => _triggerPips.Value;
		set => _triggerPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// First take-profit distance in pips.
	/// </summary>
	public int TakeProfit1Pips
	{
		get => _takeProfit1Pips.Value;
		set => _takeProfit1Pips.Value = value;
	}

	/// <summary>
	/// Second take-profit distance in pips.
	/// </summary>
	public int TakeProfit2Pips
	{
		get => _takeProfit2Pips.Value;
		set => _takeProfit2Pips.Value = value;
	}

	/// <summary>
	/// Fast EMA period used for the trigger.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastMaPeriod.Value;
		set => _fastMaPeriod.Value = value;
	}

	/// <summary>
	/// Slow SMA period used as the baseline.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowMaPeriod.Value;
		set => _slowMaPeriod.Value = value;
	}

	/// <summary>
	/// Number of completed candles used to fetch the stop-loss reference.
	/// </summary>
	public int StopLossLookback
	{
		get => _stopLossLookback.Value;
		set => _stopLossLookback.Value = value;
	}

	/// <summary>
	/// Number of hours used for the rolling high/low breakout filter.
	/// </summary>
	public int HighLowHours
	{
		get => _highLowHours.Value;
		set => _highLowHours.Value = value;
	}

	/// <summary>
	/// Cooldown duration in hours after a successful trade.
	/// </summary>
	public int BlackoutHours
	{
		get => _blackoutHours.Value;
		set => _blackoutHours.Value = value;
	}

	/// <summary>
	/// RSI length applied on the higher timeframe filter.
	/// </summary>
	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	/// <summary>
	/// Upper RSI threshold required for long positions.
	/// </summary>
	public decimal RsiUpper
	{
		get => _rsiUpper.Value;
		set => _rsiUpper.Value = value;
	}

	/// <summary>
	/// Lower RSI threshold required for short positions.
	/// </summary>
	public decimal RsiLower
	{
		get => _rsiLower.Value;
		set => _rsiLower.Value = value;
	}
	/// <summary>
	/// Daily envelope moving average period.
	/// </summary>
	public int DailyEnvelopePeriod
	{
		get => _dailyEnvelopePeriod.Value;
		set => _dailyEnvelopePeriod.Value = value;
	}

	/// <summary>
	/// Daily envelope deviation in percent.
	/// </summary>
	public decimal DailyEnvelopeDeviation
	{
		get => _dailyEnvelopeDeviation.Value;
		set => _dailyEnvelopeDeviation.Value = value;
	}

	/// <summary>
	/// Four-hour envelope moving average period.
	/// </summary>
	public int H4EnvelopePeriod
	{
		get => _h4EnvelopePeriod.Value;
		set => _h4EnvelopePeriod.Value = value;
	}

	/// <summary>
	/// Four-hour envelope deviation in percent.
	/// </summary>
	public decimal H4EnvelopeDeviation
	{
		get => _h4EnvelopeDeviation.Value;
		set => _h4EnvelopeDeviation.Value = value;
	}

	/// <summary>
	/// Primary candle type that drives entries and exits.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Candle type used to compute RSI.
	/// </summary>
	public DataType RsiTimeFrame
	{
		get => _rsiTimeFrame.Value;
		set => _rsiTimeFrame.Value = value;
	}

	/// <summary>
	/// Candle type used for the daily envelope filter.
	/// </summary>
	public DataType DailyEnvelopeTimeFrame
	{
		get => _dailyEnvelopeTimeFrame.Value;
		set => _dailyEnvelopeTimeFrame.Value = value;
	}

	/// <summary>
	/// Candle type used for the four-hour envelope filter.
	/// </summary>
	public DataType H4EnvelopeTimeFrame
	{
		get => _h4EnvelopeTimeFrame.Value;
		set => _h4EnvelopeTimeFrame.Value = value;
	}
	/// <summary>
	/// Initializes a new instance of <see cref="HerculesATC2006Strategy"/>.
	/// </summary>
	public HerculesATC2006Strategy()
	{
		_triggerPips = Param(nameof(TriggerPips), 38)
			.SetGreaterThanZero()
			.SetDisplay("Trigger Pips", "Distance above/below crossover required to trigger", "Entries")
			
			.SetOptimize(10, 80, 5);

		_trailingStopPips = Param(nameof(TrailingStopPips), 90)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk Management")
			
			.SetOptimize(20, 150, 10);

		_takeProfit1Pips = Param(nameof(TakeProfit1Pips), 210)
			.SetNotNegative()
			.SetDisplay("Take Profit 1 (pips)", "First take-profit distance", "Risk Management")
			
			.SetOptimize(100, 260, 10);

		_takeProfit2Pips = Param(nameof(TakeProfit2Pips), 280)
			.SetNotNegative()
			.SetDisplay("Take Profit 2 (pips)", "Second take-profit distance", "Risk Management")
			
			.SetOptimize(150, 360, 10);

		_fastMaPeriod = Param(nameof(FastMaPeriod), 1)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Length of the fast EMA", "Indicators");

		_slowMaPeriod = Param(nameof(SlowMaPeriod), 72)
			.SetGreaterThanZero()
			.SetDisplay("Slow SMA", "Length of the slow SMA", "Indicators")
			
			.SetOptimize(40, 120, 4);

		_stopLossLookback = Param(nameof(StopLossLookback), 4)
			.SetGreaterThanZero()
			.SetDisplay("Stop-Loss Lookback", "Number of completed candles used for stop-loss", "Risk Management");

		_highLowHours = Param(nameof(HighLowHours), 10)
			.SetGreaterThanZero()
			.SetDisplay("High/Low Window (hours)", "Duration used for breakout filter", "Filters");

		_blackoutHours = Param(nameof(BlackoutHours), 4)
			.SetGreaterThanZero()
			.SetDisplay("Blackout Hours", "Cooldown after a trade", "Filters");

		_rsiLength = Param(nameof(RsiLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("RSI Length", "RSI period on higher timeframe", "Filters");

		_rsiUpper = Param(nameof(RsiUpper), 55m)
			.SetDisplay("RSI Upper", "Upper RSI threshold for longs", "Filters");

		_rsiLower = Param(nameof(RsiLower), 45m)
			.SetDisplay("RSI Lower", "Lower RSI threshold for shorts", "Filters");

		_dailyEnvelopePeriod = Param(nameof(DailyEnvelopePeriod), 24)
			.SetGreaterThanZero()
			.SetDisplay("Daily Envelope Period", "Daily SMA length for envelope", "Filters");

		_dailyEnvelopeDeviation = Param(nameof(DailyEnvelopeDeviation), 0.99m)
			.SetGreaterThanZero()
			.SetDisplay("Daily Envelope %", "Envelope deviation in percent", "Filters");

		_h4EnvelopePeriod = Param(nameof(H4EnvelopePeriod), 96)
			.SetGreaterThanZero()
			.SetDisplay("H4 Envelope Period", "Four-hour SMA length for envelope", "Filters");

		_h4EnvelopeDeviation = Param(nameof(H4EnvelopeDeviation), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("H4 Envelope %", "Envelope deviation in percent", "Filters");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Primary Candle", "Working timeframe for entries", "General");

		_rsiTimeFrame = Param(nameof(RsiTimeFrame), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("RSI Candle", "Timeframe used for RSI filter", "Filters");

		_dailyEnvelopeTimeFrame = Param(nameof(DailyEnvelopeTimeFrame), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Daily Envelope TF", "Timeframe for the daily envelope", "Filters");

		_h4EnvelopeTimeFrame = Param(nameof(H4EnvelopeTimeFrame), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("H4 Envelope TF", "Timeframe for the four-hour envelope", "Filters");

		Volume = 2m;
	}
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		var uniqueTypes = new HashSet<DataType> { CandleType, RsiTimeFrame, DailyEnvelopeTimeFrame, H4EnvelopeTimeFrame };

		foreach (var type in uniqueTypes)
		{
			yield return (Security, type);
		}
	}

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

		_rsi.Reset();
		_rsi.Length = RsiLength;
		_dailyEnvelopeMa.Reset();
		_dailyEnvelopeMa.Length = DailyEnvelopePeriod;
		_h4EnvelopeMa.Reset();
		_h4EnvelopeMa.Length = H4EnvelopePeriod;

		Array.Clear(_fastHistory, 0, _fastHistory.Length);
		Array.Clear(_slowHistory, 0, _slowHistory.Length);
		Array.Clear(_timeHistory, 0, _timeHistory.Length);
		Array.Clear(_highStopHistory, 0, _highStopHistory.Length);
		Array.Clear(_lowStopHistory, 0, _lowStopHistory.Length);

		_historyCount = 0;
		_stopHistoryCount = 0;

		_recentHighs.Clear();
		_recentLows.Clear();
		_rollingHigh = 0m;
		_rollingLow = 0m;

		_priceStep = 0m;
		_pipSize = 0m;
		_primaryTimeFrame = default;
		_highLowLength = 0;

		_lastRsi = 0m;
		_rsiReady = false;

		_dailyUpper = 0m;
		_dailyLower = 0m;
		_dailyReady = false;

		_h4Upper = 0m;
		_h4Lower = 0m;
		_h4Ready = false;

		_blackoutUntil = null;

		_entryPrice = null;
		_stopLoss = null;
		_tp1 = null;
		_tp2 = null;
		_trailingStop = null;
		_tp1Hit = false;

		_pendingDirection = 0;
		_triggerPrice = 0m;
		_windowEndTime = null;
		_crossPrice = 0m;
	}
	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		StartProtection(null, null);

		_priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;
		var pipFactor = decimals is 3 or 5 ? 10m : 1m;
		_pipSize = _priceStep * pipFactor;

		_primaryTimeFrame = CandleType.Arg is TimeSpan span && span > TimeSpan.Zero ? span : TimeSpan.FromMinutes(1);
		_highLowLength = Math.Max(1, (int)Math.Round(HighLowHours * 60m / (decimal)_primaryTimeFrame.TotalMinutes, MidpointRounding.AwayFromZero));

		var fastMa = new EMA { Length = FastMaPeriod };
		var slowMa = new SMA { Length = SlowMaPeriod };

		var mainSubscription = SubscribeCandles(CandleType);

		mainSubscription
			.Bind(fastMa, slowMa, ProcessPrimary)
			.Start();

		_rsi.Length = RsiLength;
		SubscribeCandles(RsiTimeFrame)
			.Bind(_rsi, ProcessRsi)
			.Start();

		_dailyEnvelopeMa.Length = DailyEnvelopePeriod;
		SubscribeCandles(DailyEnvelopeTimeFrame)
			.Bind(_dailyEnvelopeMa, ProcessDailyEnvelope)
			.Start();

		_h4EnvelopeMa.Length = H4EnvelopePeriod;
		SubscribeCandles(H4EnvelopeTimeFrame)
			.Bind(_h4EnvelopeMa, ProcessH4Envelope)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, mainSubscription);
			DrawIndicator(area, fastMa);
			DrawIndicator(area, slowMa);
			DrawOwnTrades(area);
		}
	}
	private void ProcessPrimary(ICandleMessage candle, decimal fast, decimal slow)
	{
		if (candle.State != CandleStates.Finished)
			return;

		UpdateHighLow(candle);
		UpdateStopHistory(candle);
		UpdateHistory(candle, fast, slow);
		UpdateBlackout(candle.OpenTime);

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		EvaluateEntry(candle);
		ManagePosition(candle);
	}

	private void ProcessRsi(ICandleMessage candle, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_lastRsi = rsiValue;
		_rsiReady = true;
	}

	private void ProcessDailyEnvelope(ICandleMessage candle, decimal basis)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var deviation = DailyEnvelopeDeviation / 100m;
		_dailyUpper = basis * (1 + deviation);
		_dailyLower = basis * (1 - deviation);
		_dailyReady = _dailyEnvelopeMa.IsFormed;
	}

	private void ProcessH4Envelope(ICandleMessage candle, decimal basis)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var deviation = H4EnvelopeDeviation / 100m;
		_h4Upper = basis * (1 + deviation);
		_h4Lower = basis * (1 - deviation);
		_h4Ready = _h4EnvelopeMa.IsFormed;
	}

	private void UpdateBlackout(DateTimeOffset currentTime)
	{
		if (_blackoutUntil is DateTimeOffset until && currentTime >= until)
		{
			_blackoutUntil = null;
		}
	}
	private void UpdateHistory(ICandleMessage candle, decimal fast, decimal slow)
	{
		ShiftHistory(_fastHistory, fast);
		ShiftHistory(_slowHistory, slow);
		ShiftHistory(_timeHistory, candle.OpenTime);

		if (_historyCount < _fastHistory.Length)
		{
			_historyCount++;
		}

		if (_historyCount < _fastHistory.Length)
			return;

		var crossUp1 = _fastHistory[1] > _slowHistory[1] && _fastHistory[2] < _slowHistory[2];
		var crossUp2 = _fastHistory[2] > _slowHistory[2] && _fastHistory[3] < _slowHistory[3];
		var crossDown1 = _fastHistory[1] < _slowHistory[1] && _fastHistory[2] > _slowHistory[2];
		var crossDown2 = _fastHistory[2] < _slowHistory[2] && _fastHistory[3] > _slowHistory[3];

		if (crossUp1)
		{
			PrepareTrigger(1, (_fastHistory[1] + _fastHistory[2] + _slowHistory[1] + _slowHistory[2]) / 4m, _timeHistory[1]);
		}
		else if (crossUp2)
		{
			PrepareTrigger(1, (_fastHistory[2] + _fastHistory[3] + _slowHistory[2] + _slowHistory[3]) / 4m, _timeHistory[2]);
		}
		else if (crossDown1)
		{
			PrepareTrigger(-1, (_fastHistory[1] + _fastHistory[2] + _slowHistory[1] + _slowHistory[2]) / 4m, _timeHistory[1]);
		}
		else if (crossDown2)
		{
			PrepareTrigger(-1, (_fastHistory[2] + _fastHistory[3] + _slowHistory[2] + _slowHistory[3]) / 4m, _timeHistory[2]);
		}
	}

	private void PrepareTrigger(int direction, decimal crossPrice, DateTimeOffset crossTime)
	{
		_pendingDirection = direction;
		_crossPrice = crossPrice;
		_triggerPrice = direction > 0 ? crossPrice + TriggerPips * _pipSize : crossPrice - TriggerPips * _pipSize;
		_windowEndTime = crossTime + _primaryTimeFrame + _primaryTimeFrame;
	}

	private void UpdateStopHistory(ICandleMessage candle)
	{
		ShiftHistory(_highStopHistory, candle.HighPrice);
		ShiftHistory(_lowStopHistory, candle.LowPrice);

		if (_stopHistoryCount < _highStopHistory.Length)
		{
			_stopHistoryCount++;
		}
	}

	private void UpdateHighLow(ICandleMessage candle)
	{
		lock (_recentHighs)
		{
			_recentHighs.Enqueue(candle.HighPrice);
			TrimQueue(_recentHighs, _highLowLength);
			if (_recentHighs.Count >= _highLowLength)
			{
				var highs = new decimal[_recentHighs.Count];
				_recentHighs.CopyTo(highs, 0);
				_rollingHigh = GetExtreme(highs, true);
			}
		}

		lock (_recentLows)
		{
			_recentLows.Enqueue(candle.LowPrice);
			TrimQueue(_recentLows, _highLowLength);
			if (_recentLows.Count >= _highLowLength)
			{
				var lows = new decimal[_recentLows.Count];
				_recentLows.CopyTo(lows, 0);
				_rollingLow = GetExtreme(lows, false);
			}
		}
	}
	private void EvaluateEntry(ICandleMessage candle)
	{
		if (_pendingDirection == 0)
			return;

		if (_windowEndTime is DateTimeOffset end && candle.OpenTime > end)
		{
			_pendingDirection = 0;
			return;
		}

		if (_blackoutUntil is not null && candle.OpenTime < _blackoutUntil)
			return;

		if (Position != 0 || _entryPrice.HasValue)
			return;

		if (!_rsiReady)
			return;

		var priceReached = _pendingDirection > 0
			? candle.HighPrice >= _triggerPrice
			: candle.LowPrice <= _triggerPrice;

		if (!priceReached)
			return;

		if (_pendingDirection > 0)
		{
			if (_lastRsi <= RsiUpper)
				return;

			var stopLoss = GetStopPrice(false);
			if (stopLoss is null)
				return;

			BuyMarket();

			InitializePositionState(candle.ClosePrice, stopLoss.Value, true);
		}
		else
		{
			if (_lastRsi >= RsiLower)
				return;

			var stopLoss = GetStopPrice(true);
			if (stopLoss is null)
				return;

			SellMarket();

			InitializePositionState(candle.ClosePrice, stopLoss.Value, false);
		}

		_blackoutUntil = candle.OpenTime + TimeSpan.FromHours(BlackoutHours);
		_pendingDirection = 0;
	}

	private decimal? GetStopPrice(bool isShort)
	{
		if (_stopHistoryCount <= StopLossLookback)
			return null;

		var index = StopLossLookback;
		return isShort ? _highStopHistory[index] : _lowStopHistory[index];
	}

	private void InitializePositionState(decimal entryPrice, decimal stopPrice, bool isLong)
	{
		_entryPrice = entryPrice;
		_stopLoss = stopPrice;
		_tp1Hit = false;
		_trailingStop = null;

		if (TakeProfit1Pips > 0)
		{
			_tp1 = isLong ? entryPrice + TakeProfit1Pips * _pipSize : entryPrice - TakeProfit1Pips * _pipSize;
		}
		else
		{
			_tp1 = null;
		}

		if (TakeProfit2Pips > 0)
		{
			_tp2 = isLong ? entryPrice + TakeProfit2Pips * _pipSize : entryPrice - TakeProfit2Pips * _pipSize;
		}
		else
		{
			_tp2 = null;
		}
	}
	private void ManagePosition(ICandleMessage candle)
	{
		if (_entryPrice is null)
			return;

		if (Position > 0)
		{
			UpdateTrailingStop(candle.ClosePrice, true);

			if (_stopLoss is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}

			if (_trailingStop is decimal trail && candle.LowPrice <= trail)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}

			if (!_tp1Hit && _tp1 is decimal tp1 && candle.HighPrice >= tp1)
			{
				SellMarket(Position / 2m);
				_tp1Hit = true;
			}

			if (_tp2 is decimal tp2 && candle.HighPrice >= tp2)
			{
				SellMarket(Position);
				ResetPositionState();
			}
		}
		else if (Position < 0)
		{
			UpdateTrailingStop(candle.ClosePrice, false);

			if (_stopLoss is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return;
			}

			if (_trailingStop is decimal trail && candle.HighPrice >= trail)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
				return;
			}

			if (!_tp1Hit && _tp1 is decimal tp1 && candle.LowPrice <= tp1)
			{
				BuyMarket(Math.Abs(Position) / 2m);
				_tp1Hit = true;
			}

			if (_tp2 is decimal tp2 && candle.LowPrice <= tp2)
			{
				BuyMarket(Math.Abs(Position));
				ResetPositionState();
			}
		}
		else
		{
			ResetPositionState();
		}
	}

	private void UpdateTrailingStop(decimal closePrice, bool isLong)
	{
		if (TrailingStopPips <= 0)
			return;

		var candidate = isLong
			? closePrice - TrailingStopPips * _pipSize
			: closePrice + TrailingStopPips * _pipSize;

		if (_trailingStop is null)
		{
			_trailingStop = candidate;
		}
		else if (isLong && candidate > _trailingStop)
		{
			_trailingStop = candidate;
		}
		else if (!isLong && candidate < _trailingStop)
		{
			_trailingStop = candidate;
		}
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopLoss = null;
		_tp1 = null;
		_tp2 = null;
		_trailingStop = null;
		_tp1Hit = false;
	}

	private static void ShiftHistory<T>(T[] array, T value)
	{
		for (var i = array.Length - 1; i > 0; i--)
		{
			array[i] = array[i - 1];
		}

		array[0] = value;
	}

	private static void TrimQueue(Queue<decimal> queue, int maxLength)
	{
		while (queue.Count > maxLength)
		{
			queue.Dequeue();
		}
	}

	private static decimal GetExtreme(IEnumerable<decimal> values, bool isMax)
	{
		var extreme = isMax ? decimal.MinValue : decimal.MaxValue;

		foreach (var value in values)
		{
			extreme = isMax
				? (value > extreme ? value : extreme)
				: (value < extreme ? value : extreme);
		}

		return extreme;
	}
}