GitHub で見る

日次トレンド反転

概要

Daily Trend Reversal は、MetaTrader 4 エキスパート アドバイザー dailyTrendReversal_D1 の移植版です。この戦略は、日中取引をその日の始値、高値、安値に固定し、価格動向と商品チャネル指数 (CCI) の両方が同じ方向性の偏りを確認する場合にのみ参加します。取引は設定可能なGMTセッションに限定され、毎日の利益目標に達した後にオプションで停止し、フィルターが反対側に切り替わるとすぐにポジションを手放すことができます。

戦略ロジック

日次バイアスフィルター

  • 方向性のあるステップ – この戦略は、毎日のバイアスを検証するために最大 3 つの条件を評価します。
    1. 現在の価格から毎日の極値までの距離は、ピップで表されるリスクしきい値を超える必要があります。
    2. 始値から反対端までの距離もリスクしきい値を超え、価格は日次始値から 10 ピップス以内に留まらなければなりません。
    3. (オプション) 現在のローソク足は、価格が日足の始値から 10 ピップス以内にある間に、動きの方向に閉じる必要があります。
  • レンジドミナンス – オープンから高値までの距離と、オープンから安値までの距離を比較します。長い辺がアクティブなトレンドを定義します。
  • CCI トレンド – 最後の 3 つの終了した CCI 値は、単調増加 (ロングの場合) または単調減少 (ショートの場合) する必要があります。

エントリールール

  • 長いエントリ
    • 営業日の設定された GMT 取引ウィンドウ中にのみ許可されます。
    • 現在の価格は日次始値を上回っている必要があり、方向性のステップは上昇トレンドを裏付けている必要があり、レンジの優位性は上値を支持している必要があり、CCI トレンドは上昇している必要があります。
    • ネットポジションがフラットまたはショートの場合にのみロングポジションをオープンします(ショートエクスポージャーはロングへの反転の一部としてクローズされます)。
  • 短いエントリー
    • ミラーリングされた条件: 価格が日次始値を下回っており、方向性のあるステップは下降トレンドを裏付けており、レンジ優勢は下値を支持しており、CCI トレンドは下落しています。
    • ネットポジションがフラットまたはロングの場合にのみオープンします。

終了ルール

  • 固定テイクプロフィット/ストップロス – エントリーに対する相対的なピップで表されます。 0 の値は、それぞれのレベルを無効にします。
  • セッションと保持の制御 – GMT の終了時間に達するか、保持時間が時間単位で経過すると、収益性の高いポジションはすぐに閉じられます。負けた取引は損益分岐点モードに切り替わり、価格がエントリーに戻るとすぐに取引を終了します。
  • リバーサルエグジット(オプション) – 有効にすると、下降フィルターが一致したときにロングが閉じられます(価格が始値を下回り、日足/CCIのトレンドが下を向いている)。上向きのフィルターが揃うと、ショーツは対称的に閉じられます。
  • 日次プロフィットストップ – その日の最初の取引以降の実現利益と変動損益を組み合わせます。設定されたしきい値に達すると、パラメータが手動で再度有効になるまで、すべてのポジションがクローズされ、新しいエントリーは一時停止されます。

パラメーター

  • 自動取引 – 戦略で新しい取引を開始できるかどうかを切り替えます。
  • 反転エグジット – 逆の日次トレンドが確認された場合、即時エグジットが可能になります。
  • トレンド ステップ – 日次バイアスを検証するために通過する必要があるステップ フィルター (1 ~ 3) の数を選択します。
  • 出来高 – 市場エントリーの注文量。
  • 利益確定 (pips) – 固定利益目標距離。無効にするには、0 に設定します。
  • ストップロス (pips) – 保護ストップ距離。無効にするには、0 に設定します。
  • プロフィットストップ – その日の残りの取引を一時停止する価格単位での利益目標。 0 は機能を無効にします。
  • GMT 差 – チャート時刻から GMT を差し引いた時間 (時間単位)。 GMT セッション境界をチャート時間に変換するために使用されます。
  • 開始時間 / 終了時間 – 新しいポジションの取引ウィンドウを制限する GMT 時間。
  • クロージングアワー – GMT 時間以降、戦略は強制的に終了するか、損益分岐点ロジックを有効にします。
  • 保持時間 – セッション ロジックがトリガーされるまでに取引を開いたままにしておくことができる最大時間。
  • リスク (ピップ) – 方向ステップで使用されるピップ距離。
  • CCI 期間 – 商品チャネル指数の期間数。
  • ローソク足タイプ – 計算を行う時間枠 (デフォルト: 15 分ローソク足)。

追加の注意事項

  • この戦略は、証券の価格ステップからピップサイズを検出します。 5 桁および 3 桁の FX シンボルは、設定されたピップ距離を価格増分に自動的に変換します。
  • 毎日の利益追跡は、現在の実現損益を新しいベースラインとして取得することにより、新しい取引日の最初のローソク足でリセットされます。
  • この戦略には Python 実装はありません。 API パッケージでは C# バージョンのみが提供されます。
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 MetaTrader strategy dailyTrendReversal_D1.
/// Combines daily open/high/low levels with a multi-step filter and CCI trend confirmation.
/// Applies strict session control, optional reversal exits, and a configurable daily profit stop.
/// </summary>
public class DailyTrendReversalStrategy : Strategy
{
	private readonly StrategyParam<bool> _enableAutoTrading;
	private readonly StrategyParam<bool> _enableReversal;
	private readonly StrategyParam<int> _trendSteps;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _profitStop;
	private readonly StrategyParam<int> _gmtDiff;
	private readonly StrategyParam<int> _gmtStartHour;
	private readonly StrategyParam<int> _gmtEndHour;
	private readonly StrategyParam<int> _gmtClosingHour;
	private readonly StrategyParam<int> _holdingHours;
	private readonly StrategyParam<int> _riskPips;
	private readonly StrategyParam<int> _cciPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private CommodityChannelIndex _cci = null!;

	private readonly List<decimal> _cciHistory = new(3);

	private decimal _pipSize;
	private decimal _tenPips;

	private DateTime? _currentDay;
	private decimal _dailyOpen;
	private decimal _dailyHigh;
	private decimal _dailyLow;
	private decimal _dayPnLBase;

	private bool _tradingSuspended;

	private decimal _lastClose;
	private DateTimeOffset _lastCandleTime;

	private decimal _longEntryPrice;
	private DateTimeOffset? _longEntryTime;
	private decimal _longTakeProfitPrice;
	private decimal _longStopPrice;
	private bool _longBreakEvenActive;

	private decimal _shortEntryPrice;
	private DateTimeOffset? _shortEntryTime;
	private decimal _shortTakeProfitPrice;
	private decimal _shortStopPrice;
	private bool _shortBreakEvenActive;

	private DateTimeOffset _lastEntryTime;

	private enum TrendDirections
	{
		Flat,
		Up,
		Down,
	}

	/// <summary>
	/// Enables automated entries within the trading window.
	/// </summary>
	public bool EnableAutoTrading
	{
		get => _enableAutoTrading.Value;
		set => _enableAutoTrading.Value = value;
	}

	/// <summary>
	/// Enables closing positions when the opposite trend is confirmed.
	/// </summary>
	public bool EnableReversal
	{
		get => _enableReversal.Value;
		set => _enableReversal.Value = value;
	}

	/// <summary>
	/// Number of step filters applied to the daily trend evaluation.
	/// </summary>
	public int TrendSteps
	{
		get => _trendSteps.Value;
		set => _trendSteps.Value = value;
	}


	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Daily profit target that halts trading when reached (includes floating PnL).
	/// </summary>
	public decimal ProfitStop
	{
		get => _profitStop.Value;
		set => _profitStop.Value = value;
	}

	/// <summary>
	/// Difference between chart time and GMT in hours.
	/// </summary>
	public int GmtDiff
	{
		get => _gmtDiff.Value;
		set => _gmtDiff.Value = value;
	}

	/// <summary>
	/// GMT start hour of the active trading window.
	/// </summary>
	public int GmtStartHour
	{
		get => _gmtStartHour.Value;
		set => _gmtStartHour.Value = value;
	}

	/// <summary>
	/// GMT end hour for accepting new entries.
	/// </summary>
	public int GmtEndHour
	{
		get => _gmtEndHour.Value;
		set => _gmtEndHour.Value = value;
	}

	/// <summary>
	/// GMT hour when all trades should be protected or closed.
	/// </summary>
	public int GmtClosingHour
	{
		get => _gmtClosingHour.Value;
		set => _gmtClosingHour.Value = value;
	}

	/// <summary>
	/// Maximum holding time in hours before forcing exits.
	/// </summary>
	public int HoldingHours
	{
		get => _holdingHours.Value;
		set => _holdingHours.Value = value;
	}

	/// <summary>
	/// Risk threshold in pips used by the trend step filter.
	/// </summary>
	public int RiskPips
	{
		get => _riskPips.Value;
		set => _riskPips.Value = value;
	}

	/// <summary>
	/// Length of the Commodity Channel Index indicator.
	/// </summary>
	public int CciPeriod
	{
		get => _cciPeriod.Value;
		set => _cciPeriod.Value = value;
	}

	/// <summary>
	/// Candle type that drives the strategy calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="DailyTrendReversalStrategy"/> class.
	/// </summary>
	public DailyTrendReversalStrategy()
	{
		_enableAutoTrading = Param(nameof(EnableAutoTrading), true)
		.SetDisplay("Auto Trading", "Enable automated entries inside the session", "Trading");

		_enableReversal = Param(nameof(EnableReversal), true)
		.SetDisplay("Reversal Exit", "Close positions on confirmed opposite trend", "Trading");

		_trendSteps = Param(nameof(TrendSteps), 3)
		.SetRange(0, 3)
		.SetDisplay("Trend Steps", "Number of filters used for daily direction", "Trend Filter");


		_takeProfitPips = Param(nameof(TakeProfitPips), 30m)
		.SetRange(0m, 1000m)
		.SetDisplay("Take Profit (pips)", "Distance to fixed take profit (0 disables)", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetRange(0m, 1000m)
		.SetDisplay("Stop Loss (pips)", "Distance to protective stop loss (0 disables)", "Risk");

		_profitStop = Param(nameof(ProfitStop), 100m)
		.SetRange(0m, 100000m)
		.SetDisplay("Profit Stop", "Daily profit target that pauses trading", "Risk");

		_gmtDiff = Param(nameof(GmtDiff), 0)
		.SetDisplay("GMT Diff", "Chart time minus GMT in hours", "Session");

		_gmtStartHour = Param(nameof(GmtStartHour), 5)
		.SetRange(0, 23)
		.SetDisplay("Start Hour", "Session start hour in GMT", "Session");

		_gmtEndHour = Param(nameof(GmtEndHour), 14)
		.SetRange(0, 23)
		.SetDisplay("End Hour", "Session end hour for new trades (GMT)", "Session");

		_gmtClosingHour = Param(nameof(GmtClosingHour), 18)
		.SetRange(0, 23)
		.SetDisplay("Closing Hour", "Session close hour for active trades (GMT)", "Session");

		_holdingHours = Param(nameof(HoldingHours), 10)
		.SetRange(0, 48)
		.SetDisplay("Holding Hours", "Maximum holding time for positions", "Risk");

		_riskPips = Param(nameof(RiskPips), 30)
		.SetRange(0, 1000)
		.SetDisplay("Risk (pips)", "Risk filter threshold used by trend steps", "Trend Filter");

		_cciPeriod = Param(nameof(CciPeriod), 15)
		.SetGreaterThanZero()
		.SetDisplay("CCI Period", "Length of the Commodity Channel Index", "Trend Filter");

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

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

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

		_cciHistory.Clear();
		_pipSize = 0m;
		_tenPips = 0m;
		_currentDay = null;
		_dailyOpen = 0m;
		_dailyHigh = 0m;
		_dailyLow = 0m;
		_dayPnLBase = 0m;
		_tradingSuspended = false;
		_lastClose = 0m;
		_lastCandleTime = default;

		_longEntryPrice = 0m;
		_longEntryTime = null;
		_longTakeProfitPrice = 0m;
		_longStopPrice = 0m;
		_longBreakEvenActive = false;

		_shortEntryPrice = 0m;
		_shortEntryTime = null;
		_shortTakeProfitPrice = 0m;
		_shortStopPrice = 0m;
		_shortBreakEvenActive = false;

		_lastEntryTime = default;
	}

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

		_pipSize = GetPipSize();
		_tenPips = 10m * _pipSize;
		_dayPnLBase = PnL;
		_tradingSuspended = false;

		_cci = new CommodityChannelIndex
		{
			Length = CciPeriod,
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_cci, ProcessCandle)
		.Start();

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

		StartProtection(null, null);
	}

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

		_lastCandleTime = candle.CloseTime;
		_lastClose = candle.ClosePrice;

		UpdateDailyLevels(candle);
		UpdateCciHistory(cciValue);

		var riskDistance = Math.Max(0, RiskPips) * _pipSize;
		var trend = GetDirectionalTrend(candle, riskDistance);
		var rangeTrend = GetRangeTrend();
		var cciTrend = GetCciTrend();

		ManageExistingPositions(candle, rangeTrend, cciTrend, riskDistance);
		HandleProfitStop();

		if (!CanOpenPositions(candle))
		return;

		if (!_cci.IsFormed)
		return;

		EvaluateEntries(candle, trend, rangeTrend, cciTrend);
	}

	private void EvaluateEntries(ICandleMessage candle, TrendDirections trend, TrendDirections rangeTrend, TrendDirections cciTrend)
	{
		if (_lastEntryTime != default && candle.CloseTime - _lastEntryTime < TimeSpan.FromHours(3))
			return;

		var price = candle.ClosePrice;

		if (trend == TrendDirections.Up && rangeTrend == TrendDirections.Up && cciTrend == TrendDirections.Up && price > _dailyOpen && Position <= 0m)
		{
			var volume = Volume + Math.Max(0m, -Position);
			if (volume > 0m)
			{
				BuyMarket(volume);
				_lastEntryTime = candle.CloseTime;
				LogInfo($"Enter long at {price} due to daily trend confirmation.");
			}
		}

		if (trend == TrendDirections.Down && rangeTrend == TrendDirections.Down && cciTrend == TrendDirections.Down && price < _dailyOpen && Position >= 0m)
		{
			var volume = Volume + Math.Max(0m, Position);
			if (volume > 0m)
			{
				SellMarket(volume);
				_lastEntryTime = candle.CloseTime;
				LogInfo($"Enter short at {price} due to daily trend confirmation.");
			}
		}
	}

	private void ManageExistingPositions(ICandleMessage candle, TrendDirections rangeTrend, TrendDirections cciTrend, decimal riskDistance)
	{
		if (Position > 0m)
		{
			ManageLongPosition(candle, rangeTrend, cciTrend, riskDistance);
		}
		else if (Position < 0m)
		{
			ManageShortPosition(candle, rangeTrend, cciTrend, riskDistance);
		}
	}

	private void ManageLongPosition(ICandleMessage candle, TrendDirections rangeTrend, TrendDirections cciTrend, decimal riskDistance)
	{
		var price = candle.ClosePrice;

		if (_longTakeProfitPrice > 0m && price >= _longTakeProfitPrice)
		{
			SellMarket(Position);
			LogInfo("Long take profit reached.");
			return;
		}

		if (_longStopPrice > 0m && price <= _longStopPrice)
		{
			SellMarket(Position);
			LogInfo("Long stop loss triggered.");
			return;
		}

		var holdingExceeded = HoldingHours > 0 && _longEntryTime is DateTimeOffset longEntry && candle.CloseTime - longEntry >= TimeSpan.FromHours(HoldingHours);
		var closingHourReached = GmtClosingHour > 0 && IsAfterOrEqualHour(candle.CloseTime, GmtClosingHour + GmtDiff);

		if ((holdingExceeded || closingHourReached) && _longEntryPrice != 0m)
		{
			if (price > _longEntryPrice)
			{
				SellMarket(Position);
				LogInfo("Long closed with profit due to session or holding limit.");
				return;
			}

			if (!_longBreakEvenActive)
			{
				_longBreakEvenActive = true;
				LogInfo("Long switched to break-even mode due to session or holding limit.");
			}
		}

		if (_longBreakEvenActive && _longEntryPrice != 0m && price >= _longEntryPrice)
		{
			SellMarket(Position);
			_longBreakEvenActive = false;
			LogInfo("Long closed at break-even after session limit.");
			return;
		}

		if (EnableReversal && _dailyOpen != 0m)
		{
			var step1 = TrendSteps >= 0 && price - _dailyLow > riskDistance;
			var step2 = TrendSteps >= 2 && _dailyHigh - _dailyOpen >= riskDistance && _dailyOpen - price <= _tenPips;

			if (price < _dailyOpen && (step1 || step2) && rangeTrend == TrendDirections.Down && cciTrend == TrendDirections.Down)
			{
				SellMarket(Position);
				LogInfo("Long reversed due to opposite trend confirmation.");
			}
		}
	}

	private void ManageShortPosition(ICandleMessage candle, TrendDirections rangeTrend, TrendDirections cciTrend, decimal riskDistance)
	{
		var price = candle.ClosePrice;

		if (_shortTakeProfitPrice > 0m && price <= _shortTakeProfitPrice)
		{
			BuyMarket(Math.Abs(Position));
			LogInfo("Short take profit reached.");
			return;
		}

		if (_shortStopPrice > 0m && price >= _shortStopPrice)
		{
			BuyMarket(Math.Abs(Position));
			LogInfo("Short stop loss triggered.");
			return;
		}

		var holdingExceeded = HoldingHours > 0 && _shortEntryTime is DateTimeOffset shortEntry && candle.CloseTime - shortEntry >= TimeSpan.FromHours(HoldingHours);
		var closingHourReached = GmtClosingHour > 0 && IsAfterOrEqualHour(candle.CloseTime, GmtClosingHour + GmtDiff);

		if ((holdingExceeded || closingHourReached) && _shortEntryPrice != 0m)
		{
			if (price < _shortEntryPrice)
			{
				BuyMarket(Math.Abs(Position));
				LogInfo("Short closed with profit due to session or holding limit.");
				return;
			}

			if (!_shortBreakEvenActive)
			{
				_shortBreakEvenActive = true;
				LogInfo("Short switched to break-even mode due to session or holding limit.");
			}
		}

		if (_shortBreakEvenActive && _shortEntryPrice != 0m && price <= _shortEntryPrice)
		{
			BuyMarket(Math.Abs(Position));
			_shortBreakEvenActive = false;
			LogInfo("Short closed at break-even after session limit.");
			return;
		}

		if (EnableReversal && _dailyOpen != 0m)
		{
			var step1 = TrendSteps >= 0 && _dailyHigh - price > riskDistance;
			var step2 = TrendSteps >= 2 && _dailyOpen - _dailyLow >= riskDistance && price - _dailyOpen <= _tenPips;

			if (price > _dailyOpen && (step1 || step2) && rangeTrend == TrendDirections.Up && cciTrend == TrendDirections.Up)
			{
				BuyMarket(Math.Abs(Position));
				LogInfo("Short reversed due to opposite trend confirmation.");
			}
		}
	}

	private void HandleProfitStop()
	{
		if (ProfitStop <= 0m || _tradingSuspended)
		return;

		var realized = PnL - _dayPnLBase;
		var floating = 0m;

		var total = realized + floating;

		if (total >= ProfitStop)
		{
			_tradingSuspended = true;
			if (Position > 0)
				SellMarket();
			else if (Position < 0)
				BuyMarket();
			LogInfo($"Trading suspended after reaching daily profit stop of {ProfitStop}.");
		}
	}

	private bool CanOpenPositions(ICandleMessage candle)
	{
		if (!EnableAutoTrading || _tradingSuspended)
		return false;

		if (_currentDay is null)
		return false;

		if (!IsWeekday(candle.OpenTime))
		return false;

		if (!IsWithinTradingWindow(candle.OpenTime))
		return false;

		return true;
	}

	private void UpdateDailyLevels(ICandleMessage candle)
	{
		var day = candle.OpenTime.Date;

		if (_currentDay != day)
		{
			_currentDay = day;
			_dailyOpen = candle.OpenPrice;
			_dailyHigh = candle.HighPrice;
			_dailyLow = candle.LowPrice;
			_dayPnLBase = PnL;
			_longBreakEvenActive = false;
			_shortBreakEvenActive = false;
		}
		else
		{
			_dailyHigh = Math.Max(_dailyHigh, candle.HighPrice);
			_dailyLow = Math.Min(_dailyLow, candle.LowPrice);
		}
	}

	private void UpdateCciHistory(decimal cciValue)
	{
		if (_cci.IsFormed)
		{
			_cciHistory.Insert(0, cciValue);
			if (_cciHistory.Count > 3)
			_cciHistory.RemoveAt(_cciHistory.Count - 1);
		}
	}

	private TrendDirections GetCciTrend()
	{
		if (_cciHistory.Count < 3)
		return TrendDirections.Flat;

		var current = _cciHistory[0];
		var previous = _cciHistory[1];
		var older = _cciHistory[2];

		if (current >= previous && previous >= older)
		return TrendDirections.Up;

		if (current <= previous && previous <= older)
		return TrendDirections.Down;

		return TrendDirections.Flat;
	}

	private TrendDirections GetDirectionalTrend(ICandleMessage candle, decimal riskDistance)
	{
		if (_currentDay is null)
		return TrendDirections.Flat;

		var price = candle.ClosePrice;

		if (price > _dailyOpen)
		{
			var step1 = TrendSteps >= 0 && _dailyHigh - price > riskDistance;
			var step2 = TrendSteps >= 2 && _dailyOpen - _dailyLow >= riskDistance && price - _dailyOpen <= _tenPips;
			var step3 = TrendSteps >= 3 && price - _dailyOpen <= _tenPips && candle.ClosePrice > candle.OpenPrice;

			if (step1 || step2 || step3)
			return TrendDirections.Up;
		}
		else if (price < _dailyOpen)
		{
			var step1 = TrendSteps >= 0 && price - _dailyLow > riskDistance;
			var step2 = TrendSteps >= 2 && _dailyHigh - _dailyOpen >= riskDistance && _dailyOpen - price <= _tenPips;
			var step3 = TrendSteps >= 3 && _dailyOpen - price <= _tenPips && candle.ClosePrice < candle.OpenPrice;

			if (step1 || step2 || step3)
			return TrendDirections.Down;
		}

		return TrendDirections.Flat;
	}

	private TrendDirections GetRangeTrend()
	{
		var upDistance = _dailyHigh - _dailyOpen;
		var downDistance = _dailyOpen - _dailyLow;

		if (upDistance > downDistance)
		return TrendDirections.Up;

		if (upDistance < downDistance)
		return TrendDirections.Down;

		return TrendDirections.Flat;
	}

	private bool IsWeekday(DateTimeOffset time)
	{
		var day = time.DayOfWeek;
		return day is not DayOfWeek.Saturday and not DayOfWeek.Sunday;
	}

	private bool IsWithinTradingWindow(DateTimeOffset time)
	{
		var hour = time.Hour;
		var start = NormalizeHour(GmtStartHour + GmtDiff);
		var end = NormalizeHour(GmtEndHour + GmtDiff);

		if (start == end)
		return false;

		return start < end ? hour >= start && hour < end : hour >= start || hour < end;
	}

	private bool IsAfterOrEqualHour(DateTimeOffset time, int targetHour)
	{
		var hour = time.Hour;
		var normalizedTarget = NormalizeHour(targetHour);
		return hour >= normalizedTarget;
	}

	private static int NormalizeHour(int hour)
	{
		var normalized = hour % 24;
		return normalized < 0 ? normalized + 24 : normalized;
	}

	private decimal GetPipSize()
	{
		var step = Security.PriceStep ?? 0.0001m;
		var decimals = Security.Decimals ?? 0;

		if (decimals >= 3)
		return step * 10m;

		return step > 0m ? step : 0.0001m;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (Position > 0m)
		{
			_longEntryPrice = _lastClose;
			_longEntryTime = _lastCandleTime;
			_longTakeProfitPrice = TakeProfitPips > 0m ? _longEntryPrice + TakeProfitPips * _pipSize : 0m;
			_longStopPrice = StopLossPips > 0m ? _longEntryPrice - StopLossPips * _pipSize : 0m;
			_longBreakEvenActive = false;
		}
		else
		{
			_longEntryTime = null;
			_longTakeProfitPrice = 0m;
			_longStopPrice = 0m;
			if (Position <= 0m)
			_longEntryPrice = 0m;
		}

		if (Position < 0m)
		{
			_shortEntryPrice = _lastClose;
			_shortEntryTime = _lastCandleTime;
			_shortTakeProfitPrice = TakeProfitPips > 0m ? _shortEntryPrice - TakeProfitPips * _pipSize : 0m;
			_shortStopPrice = StopLossPips > 0m ? _shortEntryPrice + StopLossPips * _pipSize : 0m;
			_shortBreakEvenActive = false;
		}
		else
		{
			_shortEntryTime = null;
			_shortTakeProfitPrice = 0m;
			_shortStopPrice = 0m;
			if (Position >= 0m)
			_shortEntryPrice = 0m;
		}
	}
}