在 GitHub 上查看

JS Chaos 策略

概述

JS Chaos 策略在 StockSharp 高级 API 中复刻了 MetaTrader 专家顾问 “JS-Chaos” 的行为。策略围绕比尔·威廉姆斯的短吻鳄结构与分形突破构建入场,辅以 Awesome Oscillator 与 Acceleration/Deceleration 指标确认,并通过追踪止损、保本与复杂的时间过滤器管理持仓。

核心逻辑

  1. 指标体系
    • 使用 13/8/5 周期、8/5/3 位移的平滑移动平均构成的短吻鳄指标(以中价为输入)。
    • Awesome Oscillator 及其 5 周期 SMA 推导出的 AC 指标。
    • 21 周期平滑均线作为追踪止损的核心。
    • 10 周期标准差作为追踪触发条件之一。
    • 最近五根 K 线高低点的分形检测,记录 10 根 K 线内最新形成的分形。
  2. 信号生成
    • 多头背景:AO[0] > AO[1] > 0Lips > Teeth > Jaw
    • 空头背景:AO[0] < AO[1] < 0Lips < Teeth < Jaw
  3. 挂单布置
    • 条件满足且当前时间允许交易时,同向布置两笔突破单:主单使用 2× 基础手数,次单使用 1× 基础手数,触发价为最近符合条件且位于短吻鳄唇线之外的分形价。
    • 主单止盈为 Lips ± (Fractal − Lips) * Fibo1,次单止盈使用 Fibo2 倍数。
  4. 仓位管理
    • 可选:当唇线突破上一根 K 线开盘价时强制平仓对应方向持仓。
    • 追踪止损:当标准差、AO 与 AC 同向走强时,将止损推至 21 周期 SMMA。
    • 保本逻辑:主单离场且价格已行进到设定额外点数后,将次单止损移至保本位。
    • 通过监控价格区间,当触及止盈/止损时以市价单平仓。
  5. 时间过滤器
    • 通过开盘/收盘小时(支持跨日)限定交易窗口,并包含季节性约束:周一 03:00 之前禁用、周五 18:00 之后禁用、1 月前 9 天及 12 月 20 日之后禁用。UseTime=false 可完全关闭过滤。

参数

参数 说明
UseTime 是否启用时间过滤。
OpenHour / CloseHour 交易时段的起止小时(0-23)。
BaseVolume 基础下单手数(主单 2×、次单 1×)。
IndentingPips 分形挂单的偏移量(以点数表示)。
Fibo1 / Fibo2 计算止盈目标的斐波那契比例。
UseClosePositions 当唇线穿越上一根 K 线开盘价时是否强制平仓。
UseTrailing 是否启用基于均线与振荡指标的追踪止损。
UseBreakeven 是否启用次单的保本逻辑。
BreakevenPlusPips 移动到保本位时在入场价基础上增加的点数。
CandleType 策略所处理的 K 线类型/周期。

说明

  • 转换保留了原 MQL5 机器人分批挂单和仓位管理的结构,同时利用 StockSharp 的 K 线订阅流程。
  • 所有计算基于已收盘 K 线,原策略的盘中触发通过在价格突破后发送市价单模拟实现。
  • 点值换算会根据报价精度(3 或 5 位小数)自动调整,适用于外汇品种。
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; }
	}
}