Открыть на GitHub

Стратегия JS Chaos

Общее описание

Стратегия JS Chaos переносит функциональность советника MetaTrader "JS-Chaos" на платформу StockSharp. Логика строится вокруг структуры аллигатора Билла Уильямса и прорывов по фракталам, подтверждается осцилляторами AO и AC, а управление позицией выполняется через трейлинг, перевод в безубыток и гибкую фильтрацию по времени.

Основные элементы

  1. Набор индикаторов
    • Аллигатор: сглаженные скользящие средние с периодами 13/8/5 и смещениями 8/5/3, рассчитываемые по средней цене свечи.
    • Awesome Oscillator и его 5-периодная SMA для получения Acceleration/Deceleration.
    • 21-периодная SMMA для трейлинга.
    • 10-периодное стандартное отклонение как дополнительный фильтр для подтягивания стопа.
    • Определение фракталов на последних пяти максимумах/минимумах с хранением актуальных значений в течение десяти баров.
  2. Формирование сигналов
    • Для покупок требуется AO[0] > AO[1] > 0 и расположение линий аллигатора Lips > Teeth > Jaw.
    • Для продаж требуется AO[0] < AO[1] < 0 и Lips < Teeth < Jaw.
  3. Постановка заявок
    • При совпадении условий и разрешённом времени торговли выставляются две стоп-заявки в направлении сигнала: основная (объём 2× базового) и дополнительная (1× базового), обе на уровне последнего фрактала, находящегося за линией губ.
    • Цели по прибыли рассчитываются как Lips ± (Fractal − Lips) * Fibo1 для основной и Fibo2 для дополнительной заявки.
  4. Менеджмент позиций
    • Опциональное закрытие позиций, если линия губ пересекает цену открытия предыдущей свечи.
    • Трейлинг подтягивает стоп к 21-периодной SMMA при одновременном росте стандартного отклонения, AO и AC в сторону позиции.
    • Логика безубытка переносит стоп дополнительной сделки к цене входа с запасом по заданному числу пунктов, когда первая сделка закрылась и цена прошла нужное расстояние.
    • Стоп-лоссы и тейк-профиты контролируются вручную: при достижении уровней позиции закрываются рыночными приказами.
  5. Фильтр по времени
    • Торговое окно задаётся часами открытия/закрытия (поддерживается переход через полночь) и сезонными ограничениями: торговля запрещена до 03:00 понедельника, после 18:00 пятницы, в первые девять дней января и после 20 декабря. Параметр UseTime = false отключает фильтр полностью.

Параметры

Параметр Значение
UseTime Включение фильтра времени.
OpenHour / CloseHour Часовые границы торговли (0-23).
BaseVolume Базовый объём, используемый для расчёта ступенчатых входов (2× и 1×).
IndentingPips Отступ от уровня фрактала в пунктах.
Fibo1 / Fibo2 Множители для расчёта целей по прибыли.
UseClosePositions Принудительное закрытие позиций при пересечении губ предыдущего открытия.
UseTrailing Активация трейлинг-стопа.
UseBreakeven Перевод дополнительной позиции в безубыток.
BreakevenPlusPips Запас в пунктах при переносе стопа в безубыток.
CandleType Тип/период используемых свечей.

Дополнительно

  • Конвертация сохраняет двухэтапную структуру входа и алгоритмы управления из оригинального MQL5-советника, используя подписку на свечи StockSharp.
  • Расчёты выполняются по закрытым свечам; внутрибаравая логика оригинала имитируется отправкой рыночных приказов после подтверждения диапазона.
  • Пересчёт пунктов автоматически адаптируется к инструментам с трёх- и пятизначными квотами.
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>
/// JS Chaos strategy converted from the original MQL5 expert advisor.
/// </summary>
public class JSChaosStrategy : Strategy
{
	private readonly StrategyParam<bool> _useTime;
	private readonly StrategyParam<int> _fractalLookback;
	private readonly StrategyParam<int> _jawShift;
	private readonly StrategyParam<int> _teethShift;
	private readonly StrategyParam<int> _lipsShift;
	private readonly StrategyParam<int> _openHour;
	private readonly StrategyParam<int> _closeHour;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<int> _indentPips;
	private readonly StrategyParam<decimal> _fibo1;
	private readonly StrategyParam<decimal> _fibo2;
	private readonly StrategyParam<bool> _useClosePositions;
	private readonly StrategyParam<bool> _useTrailing;
	private readonly StrategyParam<bool> _useBreakeven;
	private readonly StrategyParam<int> _breakevenPlusPips;
	private readonly StrategyParam<DataType> _candleType;

	private SmoothedMovingAverage _jaw = null!;
	private SmoothedMovingAverage _teeth = null!;
	private SmoothedMovingAverage _lips = null!;
	private SmoothedMovingAverage _ma21 = null!;
	private SimpleMovingAverage _aoShort = null!;
	private SimpleMovingAverage _aoLong = null!;
	private SimpleMovingAverage _aoSma = null!;
	private StandardDeviation _stdDev = null!;

	private readonly List<decimal> _jawQueue = new();
	private readonly List<decimal> _teethQueue = new();
	private readonly List<decimal> _lipsQueue = new();

	private decimal? _jawValue;
	private decimal? _teethValue;
	private decimal? _lipsValue;
	private decimal? _ma21Value;

	private decimal? _aoCurrent;
	private decimal? _aoPrev;
	private decimal? _acCurrent;
	private decimal? _acPrev;
	private decimal? _stdDevCurrent;
	private decimal? _stdDevPrev;

	private readonly decimal?[] _highs = new decimal?[5];
	private readonly decimal?[] _lows = new decimal?[5];
	private int _bufferCount;

	private readonly List<FractalLevel> _upFractals = new();
	private readonly List<FractalLevel> _downFractals = new();

	private readonly List<PendingOrder> _pendingOrders = new();
	private readonly List<ActiveTrade> _activeTrades = new();

	private decimal _pipSize;
	private decimal _indentValue;
	private decimal _breakevenPlusValue;
	private bool _priceSettingsReady;

	private decimal? _prevOpen;

	/// <summary>
	/// Use time window filter.
	/// </summary>
	public bool UseTime
	{
		get => _useTime.Value;
		set => _useTime.Value = value;
	}

	/// <summary>
	/// Trading session start hour.
	/// </summary>
	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Trading session end hour.
	/// </summary>
	public int CloseHour
	{
		get => _closeHour.Value;
		set => _closeHour.Value = value;
	}

	/// <summary>
	/// Base volume for staged entries.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fractal indentation in pips.
	/// </summary>
	public int IndentingPips
	{
		get => _indentPips.Value;
		set => _indentPips.Value = value;
	}

	/// <summary>
	/// Primary take-profit multiplier.
	/// </summary>
	public decimal Fibo1
	{
		get => _fibo1.Value;
		set => _fibo1.Value = value;
	}

	/// <summary>
	/// Secondary take-profit multiplier.
	/// </summary>
	public decimal Fibo2
	{
		get => _fibo2.Value;
		set => _fibo2.Value = value;
	}

	/// <summary>
	/// Close positions when lips cross the previous open.
	/// </summary>
	public bool UseClosePositions
	{
		get => _useClosePositions.Value;
		set => _useClosePositions.Value = value;
	}

	/// <summary>
	/// Enable MA-based trailing stop.
	/// </summary>
	public bool UseTrailing
	{
		get => _useTrailing.Value;
		set => _useTrailing.Value = value;
	}

	/// <summary>
	/// Enable breakeven management for the secondary order.
	/// </summary>
	public bool UseBreakeven
	{
		get => _useBreakeven.Value;
		set => _useBreakeven.Value = value;
	}

	/// <summary>
	/// Extra pips for breakeven stop placement.
	/// </summary>
	public int BreakevenPlusPips
	{
		get => _breakevenPlusPips.Value;
		set => _breakevenPlusPips.Value = value;
	}

	/// <summary>
	/// Number of completed bars used to confirm fractals.
	/// </summary>
	public int FractalLookback
	{
		get => _fractalLookback.Value;
		set => _fractalLookback.Value = value;
	}

	/// <summary>
	/// Shift applied to the jaw moving average.
	/// </summary>
	public int JawShift
	{
		get => _jawShift.Value;
		set => _jawShift.Value = value;
	}

	/// <summary>
	/// Shift applied to the teeth moving average.
	/// </summary>
	public int TeethShift
	{
		get => _teethShift.Value;
		set => _teethShift.Value = value;
	}

	/// <summary>
	/// Shift applied to the lips moving average.
	/// </summary>
	public int LipsShift
	{
		get => _lipsShift.Value;
		set => _lipsShift.Value = value;
	}

	/// <summary>
	/// Candle type used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="JSChaosStrategy"/>.
	/// </summary>
	public JSChaosStrategy()
	{
		_useTime = Param(nameof(UseTime), false)
			.SetDisplay("Use Time", "Enable trading window", "General");

		_openHour = Param(nameof(OpenHour), 7)
			.SetRange(0, 23)
			.SetDisplay("Open Hour", "Hour to start trading", "General");

		_closeHour = Param(nameof(CloseHour), 18)
			.SetRange(0, 23)
			.SetDisplay("Close Hour", "Hour to stop trading", "General");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Base volume for staged entries", "Trading");

		_indentPips = Param(nameof(IndentingPips), 0)
			.SetRange(0, 1000)
			.SetDisplay("Indenting (pips)", "Offset from fractal level", "Trading");

		_fibo1 = Param(nameof(Fibo1), 1.618m)
			.SetGreaterThanZero()
			.SetDisplay("Fibo 1", "Primary take-profit multiplier", "Targets");

		_fibo2 = Param(nameof(Fibo2), 4.618m)
			.SetGreaterThanZero()
			.SetDisplay("Fibo 2", "Secondary take-profit multiplier", "Targets");

		_useClosePositions = Param(nameof(UseClosePositions), true)
			.SetDisplay("Close Positions", "Exit when lips cross previous open", "Risk");

		_useTrailing = Param(nameof(UseTrailing), true)
			.SetDisplay("Use Trailing", "Enable MA trailing stop", "Risk");

		_useBreakeven = Param(nameof(UseBreakeven), true)
			.SetDisplay("Use Breakeven", "Move secondary trade to breakeven", "Risk");

		_breakevenPlusPips = Param(nameof(BreakevenPlusPips), 1)
			.SetRange(0, 1000)
			.SetDisplay("Breakeven Plus", "Additional pips for breakeven", "Risk");

		_fractalLookback = Param(nameof(FractalLookback), 10)
			.SetGreaterThanZero()
			.SetDisplay("Fractal Lookback", "Bars required to confirm fractal levels", "Indicator")
			;

		_jawShift = Param(nameof(JawShift), 8)
			.SetRange(1, 30)
			.SetDisplay("Jaw Shift", "Shift applied to the jaw moving average", "Indicator")
			;

		_teethShift = Param(nameof(TeethShift), 5)
			.SetRange(1, 30)
			.SetDisplay("Teeth Shift", "Shift applied to the teeth moving average", "Indicator")
			;

		_lipsShift = Param(nameof(LipsShift), 3)
			.SetRange(1, 30)
			.SetDisplay("Lips Shift", "Shift applied to the lips moving average", "Indicator")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to process", "General");
	}

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

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

		_jawQueue.Clear();
		_teethQueue.Clear();
		_lipsQueue.Clear();
		Array.Clear(_highs);
		Array.Clear(_lows);
		_bufferCount = 0;
		_upFractals.Clear();
		_downFractals.Clear();
		_pendingOrders.Clear();
		_activeTrades.Clear();
		_jawValue = null;
		_teethValue = null;
		_lipsValue = null;
		_ma21Value = null;
		_aoCurrent = null;
		_aoPrev = null;
		_acCurrent = null;
		_acPrev = null;
		_stdDevCurrent = null;
		_stdDevPrev = null;
		_pipSize = 0m;
		_indentValue = 0m;
		_breakevenPlusValue = 0m;
		_priceSettingsReady = false;
		_prevOpen = null;
	}

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

		UpdatePriceSettings();

		_jaw = new SmoothedMovingAverage { Length = 13 };
		_teeth = new SmoothedMovingAverage { Length = 8 };
		_lips = new SmoothedMovingAverage { Length = 5 };
		_ma21 = new SmoothedMovingAverage { Length = 21 };
		_aoShort = new SMA { Length = 5 };
		_aoLong = new SMA { Length = 34 };
		_aoSma = new SMA { Length = 5 };
		_stdDev = new StandardDeviation { Length = 10 };

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

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

		if (!_priceSettingsReady)
			UpdatePriceSettings();

		var median = (candle.HighPrice + candle.LowPrice) / 2m;

		UpdateAlligator(median, candle);

		var maValue = _ma21.Process(new DecimalIndicatorValue(_ma21, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		if (maValue.IsFormed)
			_ma21Value = maValue.ToDecimal();

		var aoShortValue = _aoShort.Process(new DecimalIndicatorValue(_aoShort, median, candle.OpenTime) { IsFinal = true });
		var aoLongValue = _aoLong.Process(new DecimalIndicatorValue(_aoLong, median, candle.OpenTime) { IsFinal = true });
		if (!_aoShort.IsFormed || !_aoLong.IsFormed)
			return;

		var ao = aoShortValue.ToDecimal() - aoLongValue.ToDecimal();
		var aoSmaValue = _aoSma.Process(new DecimalIndicatorValue(_aoSma, ao, candle.OpenTime) { IsFinal = true });
		if (!_aoSma.IsFormed)
			return;

		var aoSma = aoSmaValue.ToDecimal();
		var ac = ao - aoSma;

		var stdValue = _stdDev.Process(new DecimalIndicatorValue(_stdDev, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		if (!_stdDev.IsFormed)
			return;

		var stdDev = stdValue.ToDecimal();

		if (_jawValue is null || _teethValue is null || _lipsValue is null || _ma21Value is null)
			return;

		UpdateHistory(ref _aoCurrent, ref _aoPrev, ao);
		UpdateHistory(ref _acCurrent, ref _acPrev, ac);
		UpdateStdDev(stdDev);
		UpdateFractals(candle);

		if (UseTrailing)
			UpdateTrailing(candle.ClosePrice);

		UpdateBreakeven(candle.ClosePrice);
		HandleStopsAndTargets(candle);
		UpdateBreakeven(candle.ClosePrice);

		if (UseClosePositions)
		{
			ApplyLipsExit();
			UpdateBreakeven(candle.ClosePrice);
		}

		var signal = GetSignal();
		var canTrade = IsTradingTime(candle.OpenTime);

		if (canTrade)
			TryPlaceOrders(signal, candle.ClosePrice);

		TriggerPendingOrders(candle);

		if (signal == 2)
			_pendingOrders.RemoveAll(o => o.Side == Sides.Buy);
		else if (signal == 1)
			_pendingOrders.RemoveAll(o => o.Side == Sides.Sell);

		_prevOpen = candle.OpenPrice;
	}

	private void UpdateAlligator(decimal median, ICandleMessage candle)
	{
		var jawValue = _jaw.Process(new DecimalIndicatorValue(_jaw, median, candle.OpenTime) { IsFinal = true });
		if (jawValue.IsFormed)
		{
			_jawQueue.Add(jawValue.ToDecimal());
			if (_jawQueue.Count > JawShift)
			{
				_jawValue = _jawQueue[0];
				try { _jawQueue.RemoveAt(0); } catch { }
			}
		}

		var teethValue = _teeth.Process(new DecimalIndicatorValue(_teeth, median, candle.OpenTime) { IsFinal = true });
		if (teethValue.IsFormed)
		{
			_teethQueue.Add(teethValue.ToDecimal());
			if (_teethQueue.Count > TeethShift)
			{
				_teethValue = _teethQueue[0];
				try { _teethQueue.RemoveAt(0); } catch { }
			}
		}

		var lipsValue = _lips.Process(new DecimalIndicatorValue(_lips, median, candle.OpenTime) { IsFinal = true });
		if (lipsValue.IsFormed)
		{
			_lipsQueue.Add(lipsValue.ToDecimal());
			if (_lipsQueue.Count > LipsShift)
			{
				_lipsValue = _lipsQueue[0];
				try { _lipsQueue.RemoveAt(0); } catch { }
			}
		}
	}

	private void UpdateHistory(ref decimal? current, ref decimal? previous, decimal value)
	{
		previous = current;
		current = value;
	}

	private void UpdateStdDev(decimal value)
	{
		_stdDevPrev = _stdDevCurrent;
		_stdDevCurrent = value;
	}

	private void UpdateFractals(ICandleMessage candle)
	{
		for (var i = 0; i < 4; i++)
		{
			_highs[i] = _highs[i + 1];
			_lows[i] = _lows[i + 1];
		}

		_highs[4] = candle.HighPrice;
		_lows[4] = candle.LowPrice;

		if (_bufferCount < 5)
			_bufferCount++;

		IncrementFractalAges(_upFractals);
		IncrementFractalAges(_downFractals);

		if (_bufferCount < 5)
		{
			TrimFractals(_upFractals);
			TrimFractals(_downFractals);
			return;
		}

		decimal? upFractal = null;
		decimal? downFractal = null;

		var h0 = _highs[0];
		var h1 = _highs[1];
		var h2 = _highs[2];
		var h3 = _highs[3];
		var h4 = _highs[4];

		if (h2.HasValue && h0.HasValue && h1.HasValue && h3.HasValue && h4.HasValue &&
			h2.Value > h0.Value && h2.Value > h1.Value && h2.Value > h3.Value && h2.Value > h4.Value)
			upFractal = h2.Value;

		var l0 = _lows[0];
		var l1 = _lows[1];
		var l2 = _lows[2];
		var l3 = _lows[3];
		var l4 = _lows[4];

		if (l2.HasValue && l0.HasValue && l1.HasValue && l3.HasValue && l4.HasValue &&
			l2.Value < l0.Value && l2.Value < l1.Value && l2.Value < l3.Value && l2.Value < l4.Value)
			downFractal = l2.Value;

		if (upFractal.HasValue)
		{
			var price = NormalizePrice(upFractal.Value + _indentValue);
			_upFractals.Insert(0, new FractalLevel(price));
		}

		if (downFractal.HasValue)
		{
			var price = NormalizePrice(downFractal.Value - _indentValue);
			_downFractals.Insert(0, new FractalLevel(price));
		}

		TrimFractals(_upFractals);
		TrimFractals(_downFractals);
	}

	private void IncrementFractalAges(List<FractalLevel> levels)
	{
		foreach (var level in levels)
			level.Age++;
	}

	private void TrimFractals(List<FractalLevel> levels)
	{
		for (var i = levels.Count - 1; i >= 0; i--)
		{
			if (levels[i].Age >= FractalLookback)
				levels.RemoveAt(i);
		}
	}

	private int GetSignal()
	{
		if (_aoCurrent is not decimal ao0 || _aoPrev is not decimal ao1 ||
			_lipsValue is not decimal lips || _teethValue is not decimal teeth || _jawValue is not decimal jaw)
			return 0;

		if (ao0 > ao1 && ao1 > 0m && lips > teeth && teeth > jaw)
			return 1;

		if (ao0 < ao1 && ao1 < 0m && lips < teeth && teeth < jaw)
			return 2;

		return 0;
	}

	private void TryPlaceOrders(int signal, decimal closePrice)
	{
		if (signal == 1)
		{
			var upFractal = GetLatestFractal(_upFractals);
			if (upFractal.HasValue)
				TryCreateBuyOrders(upFractal.Value, _lipsValue!.Value, closePrice);
		}
		else if (signal == 2)
		{
			var downFractal = GetLatestFractal(_downFractals);
			if (downFractal.HasValue)
				TryCreateSellOrders(downFractal.Value, _lipsValue!.Value, closePrice);
		}
	}

	private decimal? GetLatestFractal(List<FractalLevel> levels)
	{
		return levels.Count > 0 ? levels[0].Price : null;
	}

	private void TryCreateBuyOrders(decimal upFractal, decimal lips, decimal closePrice)
	{
		if (upFractal <= lips)
			return;

		if (_activeTrades.Exists(t => t.Side == Sides.Buy))
			return;

		var hasPrimary = _pendingOrders.Exists(o => o.Side == Sides.Buy && o.IsPrimary);
		var hasSecondary = _pendingOrders.Exists(o => o.Side == Sides.Buy && !o.IsPrimary);

		if (!hasPrimary)
		{
			var distance = upFractal - lips;
			if (_pipSize > 0m)
			{
				if (distance <= _pipSize)
					return;

				if (closePrice + _pipSize >= upFractal)
					return;
			}

			var tp = lips + distance * Fibo1;
			if (tp <= 0m)
				return;

			if (_pipSize > 0m && tp - upFractal <= _pipSize)
				return;

			var order = new PendingOrder
			{
				Side = Sides.Buy,
				Price = NormalizePrice(upFractal),
				StopLoss = NormalizePrice(lips),
				TakeProfit = NormalizePrice(tp),
				Volume = BaseVolume * 2m,
				IsPrimary = true
			};

			if (order.Volume > 0m)
				_pendingOrders.Add(order);
		}

		hasPrimary = _pendingOrders.Exists(o => o.Side == Sides.Buy && o.IsPrimary);
		if (!hasPrimary || hasSecondary)
			return;

		var distanceSecondary = upFractal - lips;
		if (_pipSize > 0m)
		{
			if (distanceSecondary <= _pipSize)
				return;

			if (closePrice + _pipSize >= upFractal)
				return;
		}

		var tp2 = lips + distanceSecondary * Fibo2;
		if (tp2 <= 0m)
			return;

		if (_pipSize > 0m && tp2 - upFractal <= _pipSize)
			return;

		var secondary = new PendingOrder
		{
			Side = Sides.Buy,
			Price = NormalizePrice(upFractal),
			StopLoss = NormalizePrice(lips),
			TakeProfit = NormalizePrice(tp2),
			Volume = BaseVolume,
			IsPrimary = false
		};

		if (secondary.Volume > 0m)
			_pendingOrders.Add(secondary);
	}

	private void TryCreateSellOrders(decimal downFractal, decimal lips, decimal closePrice)
	{
		if (downFractal >= lips)
			return;

		if (_activeTrades.Exists(t => t.Side == Sides.Sell))
			return;

		var hasPrimary = _pendingOrders.Exists(o => o.Side == Sides.Sell && o.IsPrimary);
		var hasSecondary = _pendingOrders.Exists(o => o.Side == Sides.Sell && !o.IsPrimary);

		if (!hasPrimary)
		{
			var distance = lips - downFractal;
			if (_pipSize > 0m)
			{
				if (distance <= _pipSize)
					return;

				if (closePrice - _pipSize <= downFractal)
					return;
			}

			var tp = lips - distance * Fibo1;
			if (tp <= 0m)
				return;

			if (_pipSize > 0m && downFractal - tp <= _pipSize)
				return;

			var order = new PendingOrder
			{
				Side = Sides.Sell,
				Price = NormalizePrice(downFractal),
				StopLoss = NormalizePrice(lips),
				TakeProfit = NormalizePrice(tp),
				Volume = BaseVolume * 2m,
				IsPrimary = true
			};

			if (order.Volume > 0m)
				_pendingOrders.Add(order);
		}

		hasPrimary = _pendingOrders.Exists(o => o.Side == Sides.Sell && o.IsPrimary);
		if (!hasPrimary || hasSecondary)
			return;

		var distanceSecondary = lips - downFractal;
		if (_pipSize > 0m)
		{
			if (distanceSecondary <= _pipSize)
				return;

			if (closePrice - _pipSize <= downFractal)
				return;
		}

		var tp2 = lips - distanceSecondary * Fibo2;
		if (tp2 <= 0m)
			return;

		if (_pipSize > 0m && downFractal - tp2 <= _pipSize)
			return;

		var secondary = new PendingOrder
		{
			Side = Sides.Sell,
			Price = NormalizePrice(downFractal),
			StopLoss = NormalizePrice(lips),
			TakeProfit = NormalizePrice(tp2),
			Volume = BaseVolume,
			IsPrimary = false
		};

		if (secondary.Volume > 0m)
			_pendingOrders.Add(secondary);
	}

	private void TriggerPendingOrders(ICandleMessage candle)
	{
		// indicators checked above

		for (var i = _pendingOrders.Count - 1; i >= 0; i--)
		{
			var pending = _pendingOrders[i];
			var triggered = pending.Side == Sides.Buy
				? candle.HighPrice >= pending.Price
				: candle.LowPrice <= pending.Price;

			if (!triggered)
				continue;

			ExecuteTrade(pending);
			try { _pendingOrders.RemoveAt(i); } catch { }
		}
	}

	private void ExecuteTrade(PendingOrder order)
	{
		if (order.Volume <= 0m)
			return;

		if (order.Side == Sides.Buy)
			BuyMarket();
		else
			SellMarket();

		_activeTrades.Add(new ActiveTrade
		{
			Side = order.Side,
			Volume = order.Volume,
			EntryPrice = order.Price,
			StopLoss = order.StopLoss,
			TakeProfit = order.TakeProfit,
			IsPrimary = order.IsPrimary
		});
	}

	private void UpdateTrailing(decimal closePrice)
	{
		if (_ma21Value is not decimal ma21 ||
			_stdDevCurrent is not decimal stdDev0 || _stdDevPrev is not decimal stdDev1 ||
			_aoCurrent is not decimal ao0 || _aoPrev is not decimal ao1 ||
			_acCurrent is not decimal ac0 || _acPrev is not decimal ac1)
			return;

		foreach (var trade in _activeTrades)
		{
			if (trade.Side == Sides.Buy)
			{
				if ((trade.StopLoss <= 0m || (trade.StopLoss != ma21 && trade.StopLoss < ma21)) &&
					stdDev0 > stdDev1 && ao0 > ao1 && ac0 > ac1)
				{
					if (_pipSize <= 0m || ma21 + _pipSize <= closePrice)
						trade.StopLoss = NormalizePrice(ma21);
				}
			}
			else
			{
				if ((trade.StopLoss <= 0m || (trade.StopLoss != ma21 && trade.StopLoss > ma21)) &&
					stdDev0 > stdDev1 && ao0 < ao1 && ac0 < ac1)
				{
					if (_pipSize <= 0m || ma21 - _pipSize >= closePrice)
						trade.StopLoss = NormalizePrice(ma21);
				}
			}
		}
	}

	private void UpdateBreakeven(decimal closePrice)
	{
		if (!UseBreakeven || _breakevenPlusValue <= 0m)
			return;

		foreach (var trade in _activeTrades)
		{
			if (trade.IsPrimary || trade.MovedToBreakeven)
				continue;

			var primaryExists = _activeTrades.Exists(t => t.Side == trade.Side && t.IsPrimary);
			if (primaryExists)
				continue;

			if (trade.Side == Sides.Buy)
			{
				if (closePrice >= trade.EntryPrice + _breakevenPlusValue && trade.StopLoss < trade.EntryPrice)
				{
					trade.StopLoss = NormalizePrice(trade.EntryPrice + _breakevenPlusValue);
					trade.MovedToBreakeven = true;
				}
			}
			else
			{
				if (closePrice <= trade.EntryPrice - _breakevenPlusValue && trade.StopLoss > trade.EntryPrice)
				{
					trade.StopLoss = NormalizePrice(trade.EntryPrice - _breakevenPlusValue);
					trade.MovedToBreakeven = true;
				}
			}
		}
	}

	private void HandleStopsAndTargets(ICandleMessage candle)
	{
		for (var i = _activeTrades.Count - 1; i >= 0; i--)
		{
			var trade = _activeTrades[i];
			var close = false;

			if (trade.Side == Sides.Buy)
			{
				if (trade.TakeProfit > 0m && candle.HighPrice >= trade.TakeProfit)
					close = true;
				else if (trade.StopLoss > 0m && candle.LowPrice <= trade.StopLoss)
					close = true;
			}
			else
			{
				if (trade.TakeProfit > 0m && candle.LowPrice <= trade.TakeProfit)
					close = true;
				else if (trade.StopLoss > 0m && candle.HighPrice >= trade.StopLoss)
					close = true;
			}

			if (!close)
				continue;

			CloseTrade(trade);
			try { _activeTrades.RemoveAt(i); } catch { }
		}
	}

	private void ApplyLipsExit()
	{
		if (_prevOpen is null || _lipsValue is null)
			return;

		var prevOpen = _prevOpen.Value;
		var lips = _lipsValue.Value;

		if (lips > prevOpen)
			CloseTrades(Sides.Buy);

		if (lips < prevOpen)
			CloseTrades(Sides.Sell);
	}

	private void CloseTrades(Sides side)
	{
		for (var i = _activeTrades.Count - 1; i >= 0; i--)
		{
			var trade = _activeTrades[i];
			if (trade.Side != side)
				continue;

			CloseTrade(trade);
			try { _activeTrades.RemoveAt(i); } catch { }
		}
	}

	private void CloseTrade(ActiveTrade trade)
	{
		if (trade.Side == Sides.Buy)
			SellMarket();
		else
			BuyMarket();
	}

	private bool IsTradingTime(DateTime time)
	{
		if (!UseTime)
			return true;

		var hour = time.Hour;
		var trading = false;

		if (OpenHour > CloseHour)
			trading = hour <= CloseHour || hour >= OpenHour;
		else if (OpenHour < CloseHour)
			trading = hour >= OpenHour && hour <= CloseHour;
		else
			trading = hour == OpenHour;

		var dayOfWeek = (int)time.DayOfWeek;

		if (dayOfWeek == 1 && hour < 3)
			trading = false;

		if (dayOfWeek >= 5 && hour > 18)
			trading = false;

		if (time.Month == 1 && time.Day < 10)
			trading = false;

		if (time.Month == 12 && time.Day > 20)
			trading = false;

		return trading;
	}

	private void UpdatePriceSettings()
	{
		if (Security is null)
			return;

		var step = Security.PriceStep;
		if (step is not decimal priceStep || priceStep <= 0m)
			return;

		var decimals = CountDecimals(priceStep);
		var pip = priceStep;

		if (decimals == 3 || decimals == 5)
			pip = priceStep * 10m;

		_pipSize = pip;
		_indentValue = pip * IndentingPips;
		_breakevenPlusValue = pip * BreakevenPlusPips;
		_priceSettingsReady = true;
	}

	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep;
		if (step is decimal priceStep && priceStep > 0m)
			return Math.Round(price / priceStep, MidpointRounding.AwayFromZero) * priceStep;

		return price;
	}

	private static int CountDecimals(decimal value)
	{
		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}

	private sealed class FractalLevel
	{
		public FractalLevel(decimal price)
		{
			Price = price;
		}

		public decimal Price { get; }
		public int Age { get; set; }
	}

	private sealed class PendingOrder
	{
		public Sides Side { get; init; }
		public decimal Price { get; init; }
		public decimal StopLoss { get; init; }
		public decimal TakeProfit { get; init; }
		public decimal Volume { get; init; }
		public bool IsPrimary { get; init; }
	}

	private sealed class ActiveTrade
	{
		public Sides Side { get; init; }
		public decimal Volume { get; init; }
		public decimal EntryPrice { get; init; }
		public decimal StopLoss { get; set; }
		public decimal TakeProfit { get; init; }
		public bool IsPrimary { get; init; }
		public bool MovedToBreakeven { get; set; }
	}
}