在 GitHub 上查看

Ten Pips Opposite Last N Hour Trend 策略

概述

该策略是 MetaTrader 专家 10pipsOnceADayOppositeLastNHourTrend 的移植版本。它每天只在一个指定的小时交易一次,并且刻意反向跟随最近 N 根小时 K 线的价格变化。原始脚本面向 5 位小数的外汇品种,C# 版本会根据 PriceStep 和小数位数自动换算点值,因此同样适用于 3 位小数的品种。

到达交易时间后,策略会比较 HoursToCheckTrend 小时之前的收盘价与最近一根已完成小时 K 线的收盘价:

  • 如果较早的收盘价 更高,说明价格近段时间下跌,于是开出 多头 仓位。
  • 否则价格上涨,则开出 空头 仓位。

仓位可以被保护性止损/止盈、持仓超时或超出交易时段等条件关闭。

资金管理

头寸规模完全复制原始 EA 的“阶梯式”马丁格尔逻辑:

  1. 基础手数来自 FixedVolume。当它为 0 时,按照 Portfolio.CurrentValue * MaximumRisk / 1000 计算,并四舍五入到 0.1 手。
  2. 结果会受到 MinimumVolumeMaximumVolume、交易所的最小/最大手数以及软限制 Portfolio.CurrentValue / 1000 的约束。
  3. 策略会保存最近五笔平仓结果。准备下一次进场时,按从近到远的顺序查找第一次出现的亏损,并使用 FirstMultiplierFifthMultiplier 中对应的倍数调整手数,完全模拟 MQL 中层层嵌套的 OrderSelect 判断。

风险控制

  • StopLossPipsTakeProfitPipsTrailingStopPips 以点为单位。移植时按照外汇常用的 3/5 位小数规则自动放大 10 倍。
  • 多、空两侧的跟踪止损采用同一套逻辑。原始 EA 在空头方向存在符号错误导致永远不会移动止损,C# 版本修复了这一问题。
  • OrderMaxAge 用于平掉持仓时间超过阈值(默认 21 小时)的订单。
  • 如果当前小时不在允许列表内,策略会立即平仓并等待下一次机会。
  • MaxOrders 确保在有持仓或挂单时不会重复进场。

工作流程

  1. 订阅 CandleType 指定的 K 线(默认 1 小时)。
  2. 将每根完成 K 线的收盘价写入滚动缓冲区。
  3. 在达到设定交易小时的第一根完成 K 线上:
    • 检查连接状态并确认没有持仓。
    • 确保历史缓冲区中至少包含 HoursToCheckTrend 根 K 线。
    • 比较当前收盘价与 HoursToCheckTrend 小时前的收盘价,得出买卖方向。
    • 根据资金管理规则计算手数并发送市价单。
  4. 持仓期间:
    • 根据 K 线的最高价/最低价检查止损、止盈和跟踪止损是否触发。
    • 创出新高/新低时,更新跟踪止损的位置。
    • 记录建仓时间,用于判断 OrderMaxAge
    • 平仓时保存盈亏结果供下一次手数调整使用。

参数

参数 说明 默认值
FixedVolume 固定下单手数。设为 0 时改用风险百分比。 0.1
MinimumVolume 下单量下限。 0.1
MaximumVolume 下单量上限。 5
MaximumRisk FixedVolume = 0 时的风险比例。 0.05
MaxOrders 允许同时存在的订单/仓位数量。 1
TradingHour 允许进场的小时(0–23)。 7
HoursToCheckTrend 回溯的小时数量。 30
OrderMaxAge 持仓最长时间。 21 小时
StopLossPips 止损距离(点)。 50
TakeProfitPips 止盈距离(点)。 10
TrailingStopPips 跟踪止损距离(点)。 0(关闭)
FirstMultiplierFifthMultiplier 在最近第 1…5 笔亏损出现时的手数乘数。 4, 2, 5, 5, 1
CandleType 计算所用的 K 线类型。 1 小时

与 MQL 版本的差异

  • 马丁格尔、持仓时间和交易时间窗口等核心逻辑保持一致,唯一的改动是修复了空头方向的跟踪止损。
  • 保护性止损/止盈在下一根完成 K 线上以市价平仓,这与原专家的实际效果一致。
  • 账户权益读取自 Portfolio.CurrentValue。若连接器未提供该字段,则退回到策略的基础 Volume(默认为 1)。
  • 允许的交易小时列表为 0…23。如需限制具体工作日,可在构造函数中修改 _tradingDayHours

使用建议

  • 推荐在外汇小时级别数据上运行,确保点值换算符合预期。
  • 请确认连接器提供 VolumeStepVolumeMinVolumeMax 等信息,以便策略能够调整手数。
  • 为避免错过当日唯一的交易信号,应在目标交易小时之前启动策略。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Trades once per day against the direction of the last N hourly candles.
/// Lot sizing mimics the martingale multipliers of the original MQL expert.
/// </summary>
public class TenPipsOppositeLastNHourTrendStrategy : Strategy
{
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<decimal> _minimumVolume;
	private readonly StrategyParam<decimal> _maximumVolume;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<int> _maxOrders;
	private readonly StrategyParam<int> _tradingHour;
	private readonly StrategyParam<int> _hoursToCheckTrend;
	private readonly StrategyParam<TimeSpan> _orderMaxAge;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _firstMultiplier;
	private readonly StrategyParam<decimal> _secondMultiplier;
	private readonly StrategyParam<decimal> _thirdMultiplier;
	private readonly StrategyParam<decimal> _fourthMultiplier;
	private readonly StrategyParam<decimal> _fifthMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<int> _tradingDayHours;
	private readonly List<decimal> _closedTradeProfits = new();
	private readonly List<decimal> _closeHistory = new();

	private decimal _pipSize;
	private DateTimeOffset? _lastBarTraded;
	private Sides? _entrySide;
	private decimal _entryVolume;
	private decimal? _entryPrice;
	private DateTimeOffset? _entryTime;
	private decimal? _trailingStopPrice;

	/// <summary>
	/// Fixed volume for market entries. When zero the strategy uses risk based sizing.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Minimum allowed volume after all adjustments.
	/// </summary>
	public decimal MinimumVolume
	{
		get => _minimumVolume.Value;
		set => _minimumVolume.Value = value;
	}

	/// <summary>
	/// Maximum allowed volume after all adjustments.
	/// </summary>
	public decimal MaximumVolume
	{
		get => _maximumVolume.Value;
		set => _maximumVolume.Value = value;
	}

	/// <summary>
	/// Fraction of account value risked when FixedVolume is zero.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneously open orders and positions.
	/// </summary>
	public int MaxOrders
	{
		get => _maxOrders.Value;
		set => _maxOrders.Value = value;
	}

	/// <summary>
	/// Hour (0-23) when the strategy is allowed to open a trade.
	/// </summary>
	public int TradingHour
	{
		get => _tradingHour.Value;
		set => _tradingHour.Value = value;
	}

	/// <summary>
	/// Number of hours used to evaluate the opposite trend.
	/// </summary>
	public int HoursToCheckTrend
	{
		get => _hoursToCheckTrend.Value;
		set => _hoursToCheckTrend.Value = value;
	}

	/// <summary>
	/// Maximum allowed lifetime for an open position.
	/// </summary>
	public TimeSpan OrderMaxAge
	{
		get => _orderMaxAge.Value;
		set => _orderMaxAge.Value = value;
	}

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

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

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

	/// <summary>
	/// Multiplier applied after the most recent losing trade.
	/// </summary>
	public decimal FirstMultiplier
	{
		get => _firstMultiplier.Value;
		set => _firstMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied when the last trade was profitable but the previous one lost.
	/// </summary>
	public decimal SecondMultiplier
	{
		get => _secondMultiplier.Value;
		set => _secondMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied when only the third most recent trade lost.
	/// </summary>
	public decimal ThirdMultiplier
	{
		get => _thirdMultiplier.Value;
		set => _thirdMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied when only the fourth most recent trade lost.
	/// </summary>
	public decimal FourthMultiplier
	{
		get => _fourthMultiplier.Value;
		set => _fourthMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied when only the fifth most recent trade lost.
	/// </summary>
	public decimal FifthMultiplier
	{
		get => _fifthMultiplier.Value;
		set => _fifthMultiplier.Value = value;
	}

	/// <summary>
	/// Type of candles processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="TenPipsOppositeLastNHourTrendStrategy"/> class.
	/// </summary>
	public TenPipsOppositeLastNHourTrendStrategy()
	{
		_fixedVolume = Param(nameof(FixedVolume), 0.1m)
		.SetDisplay("Fixed Volume", "Fixed volume for entries", "Risk")
		
		.SetOptimize(0m, 1m, 0.1m);

		_minimumVolume = Param(nameof(MinimumVolume), 0.1m)
		.SetDisplay("Minimum Volume", "Minimum allowed volume", "Risk");

		_maximumVolume = Param(nameof(MaximumVolume), 5m)
		.SetDisplay("Maximum Volume", "Maximum allowed volume", "Risk");

		_maximumRisk = Param(nameof(MaximumRisk), 0.05m)
		.SetDisplay("Maximum Risk", "Risk fraction when Fixed Volume is zero", "Risk")
		
		.SetOptimize(0m, 0.2m, 0.01m);

		_maxOrders = Param(nameof(MaxOrders), 1)
		.SetDisplay("Max Orders", "Maximum simultaneous orders", "Trading")
		
		.SetOptimize(1, 3, 1);

		_tradingHour = Param(nameof(TradingHour), 7)
		.SetDisplay("Trading Hour", "Hour when entries are allowed", "Trading");

		_hoursToCheckTrend = Param(nameof(HoursToCheckTrend), 30)
		.SetDisplay("Hours To Check Trend", "Look-back hours for trend detection", "Trading")
		.SetGreaterThanZero();

		_orderMaxAge = Param(nameof(OrderMaxAge), TimeSpan.FromSeconds(75600))
		.SetDisplay("Order Max Age", "Maximum position lifetime", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 10m)
		.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 0m)
		.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk");

		_firstMultiplier = Param(nameof(FirstMultiplier), 4m)
		.SetDisplay("First Multiplier", "Multiplier after the last loss", "Money Management");

		_secondMultiplier = Param(nameof(SecondMultiplier), 2m)
		.SetDisplay("Second Multiplier", "Multiplier if only the previous trade lost", "Money Management");

		_thirdMultiplier = Param(nameof(ThirdMultiplier), 5m)
		.SetDisplay("Third Multiplier", "Multiplier if only the third trade lost", "Money Management");

		_fourthMultiplier = Param(nameof(FourthMultiplier), 5m)
		.SetDisplay("Fourth Multiplier", "Multiplier if only the fourth trade lost", "Money Management");

		_fifthMultiplier = Param(nameof(FifthMultiplier), 1m)
		.SetDisplay("Fifth Multiplier", "Multiplier if only the fifth trade lost", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Candle type used for analysis", "Trading");

		_tradingDayHours = new List<int>(24);
		for (var hour = 0; hour < 24; hour++)
		_tradingDayHours.Add(hour);
	}

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

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

		_closedTradeProfits.Clear();
		_closeHistory.Clear();
		_lastBarTraded = null;
		_entrySide = null;
		_entryVolume = 0m;
		_entryPrice = null;
		_entryTime = null;
		_trailingStopPrice = null;
		_pipSize = 0m;
	}

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

		InitializePipSize();

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

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

		StartProtection(null, null);
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		UpdateCloseHistory(candle.ClosePrice);

		if (Position != 0 && UpdateProtectiveLogic(candle))
		return;

		if (Position != 0 && CloseExpiredPosition(candle.CloseTime))
		return;

		if (!IsTradingHour(candle.CloseTime))
		{
			FlattenOutsideTradingHours();
			return;
		}

		if (!HasTrendSample())
		return;

		if (!CanOpenOnBar(candle.OpenTime))
		return;

		if (Position != 0)
		return;

		var direction = DetermineDirection();
		if (direction == 0)
		return;

		var volume = CalculateOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
		return;

		if (direction > 0)
		{
			// Enter long against a bearish move in the look-back window.
			BuyMarket(volume);
		}
		else
		{
			// Enter short against a bullish move in the look-back window.
			SellMarket(volume);
		}

		_lastBarTraded = candle.OpenTime;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade?.Order == null || trade.Trade == null)
		return;

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;
		var time = trade.Trade.ServerTime;

		if (volume <= 0m || price <= 0m)
		return;

		if (_entrySide == null || _entrySide == trade.Order.Side)
		{
			RegisterEntryTrade(price, volume, trade.Order.Side, time);
		}
		else
		{
			RegisterExitTrade(price, volume, time);
		}
	}

	private void RegisterEntryTrade(decimal price, decimal volume, Sides side, DateTimeOffset time)
	{
		// Weighted-average entry price for pyramided fills.
		var totalVolume = _entryVolume + volume;
		if (totalVolume <= 0m)
		{
			_entryVolume = 0m;
			_entryPrice = null;
			_entrySide = null;
			_entryTime = null;
			_trailingStopPrice = null;
			return;
		}

		_entryPrice = _entryVolume > 0m && _entryPrice.HasValue
		? ((_entryPrice.Value * _entryVolume) + (price * volume)) / totalVolume
		: price;

		_entryVolume = totalVolume;
		_entrySide = side;
		_entryTime ??= time;

		var trailingDistance = GetTrailingDistance();
		if (TrailingStopPips > 0m && trailingDistance > 0m)
		{
			_trailingStopPrice = side == Sides.Buy
			? _entryPrice - trailingDistance
			: _entryPrice + trailingDistance;
		}
	}

	private void RegisterExitTrade(decimal price, decimal volume, DateTimeOffset time)
	{
		if (_entrySide == null || !_entryPrice.HasValue || _entryVolume <= 0m)
		return;

		var remaining = _entryVolume - volume;
		if (remaining < 0m)
		remaining = 0m;

		decimal profit = 0m;
		if (_entrySide == Sides.Buy)
		profit = (price - _entryPrice.Value) * volume;
		else if (_entrySide == Sides.Sell)
		profit = (_entryPrice.Value - price) * volume;

		AddClosedTradeProfit(profit);

		if (remaining == 0m)
		{
			ResetEntryState();
		}
		else
		{
			_entryVolume = remaining;
			_entryTime = time;
		}
	}

	private bool UpdateProtectiveLogic(ICandleMessage candle)
	{
		if (_entrySide == null || !_entryPrice.HasValue || _entryVolume <= 0m)
		return false;

		var pip = EnsurePipSize();
		if (pip <= 0m)
		return false;

		var stopLoss = StopLossPips * pip;
		var takeProfit = TakeProfitPips * pip;
		var trailingDistance = TrailingStopPips * pip;

		if (_entrySide == Sides.Buy)
		{
			if (StopLossPips > 0m && candle.LowPrice <= _entryPrice.Value - stopLoss)
			{
				SellMarket(Math.Abs(Position));
				return true;
			}

			if (TakeProfitPips > 0m && candle.HighPrice >= _entryPrice.Value + takeProfit)
			{
				SellMarket(Math.Abs(Position));
				return true;
			}

			if (TrailingStopPips > 0m && trailingDistance > 0m)
			{
				var candidate = candle.HighPrice - trailingDistance;
				if (candidate > (_trailingStopPrice ?? decimal.MinValue) && candle.HighPrice - _entryPrice.Value > trailingDistance)
				_trailingStopPrice = candidate;

				if (_trailingStopPrice.HasValue && candle.LowPrice <= _trailingStopPrice.Value)
				{
					SellMarket(Math.Abs(Position));
					return true;
				}
			}
		}
		else if (_entrySide == Sides.Sell)
		{
			if (StopLossPips > 0m && candle.HighPrice >= _entryPrice.Value + stopLoss)
			{
				BuyMarket(Math.Abs(Position));
				return true;
			}

			if (TakeProfitPips > 0m && candle.LowPrice <= _entryPrice.Value - takeProfit)
			{
				BuyMarket(Math.Abs(Position));
				return true;
			}

			if (TrailingStopPips > 0m && trailingDistance > 0m)
			{
				var candidate = candle.LowPrice + trailingDistance;
				if (!_trailingStopPrice.HasValue || candidate < _trailingStopPrice.Value)
				_trailingStopPrice = candidate;

				if (_trailingStopPrice.HasValue && candle.HighPrice >= _trailingStopPrice.Value)
				{
					BuyMarket(Math.Abs(Position));
					return true;
				}
			}
		}

		return false;
	}

	private bool CloseExpiredPosition(DateTimeOffset time)
	{
		if (OrderMaxAge <= TimeSpan.Zero || _entryTime == null)
		return false;

		if (time - _entryTime < OrderMaxAge)
		return false;

		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
			return true;
		}

		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			return true;
		}

		return false;
	}

	private bool IsTradingHour(DateTimeOffset time)
	{
		if (TradingHour < 0 || TradingHour > 23)
		return false;

		if (!_tradingDayHours.Contains(time.Hour))
		return false;

		return time.Hour == TradingHour;
	}

	private bool CanOpenOnBar(DateTimeOffset barOpenTime)
	{
		if (_lastBarTraded.HasValue && _lastBarTraded.Value == barOpenTime)
		return false;

		return true;
	}

	private void FlattenOutsideTradingHours()
	{
		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
		}
	}

	private bool HasTrendSample()
	{
		return HoursToCheckTrend > 0 && _closeHistory.Count >= HoursToCheckTrend;
	}

	private int DetermineDirection()
	{
		if (_closeHistory.Count == 0)
		return 0;

		var latestIndex = _closeHistory.Count - 1;
		var recentClose = _closeHistory[latestIndex];

		var olderIndex = _closeHistory.Count - HoursToCheckTrend;
		if (olderIndex < 0 || olderIndex >= _closeHistory.Count)
		return 0;

		var olderClose = _closeHistory[olderIndex];

		return olderClose > recentClose ? 1 : -1;
	}

	private decimal CalculateOrderVolume(decimal price)
	{
		decimal baseVolume;

		if (FixedVolume > 0m)
		{
			baseVolume = FixedVolume;
		}
		else
		{
			var equity = Portfolio?.CurrentValue ?? 0m;
			if (equity > 0m && MaximumRisk > 0m)
			{
				baseVolume = RoundToOneDecimal(equity * MaximumRisk / 1000m);
			}
			else
			{
				baseVolume = Volume > 0m ? Volume : 1m;
			}
		}

		baseVolume = ApplyLossMultipliers(baseVolume);

		var equityCap = Portfolio?.CurrentValue ?? 0m;
		if (equityCap > 0m)
		{
			var cap = RoundToOneDecimal(equityCap / 1000m);
			if (cap > 0m && baseVolume > cap)
			baseVolume = cap;
		}

		if (baseVolume < MinimumVolume)
		baseVolume = MinimumVolume;
		else if (baseVolume > MaximumVolume)
		baseVolume = MaximumVolume;

		return AdjustVolume(baseVolume);
	}

	private decimal ApplyLossMultipliers(decimal volume)
	{
		if (_closedTradeProfits.Count == 0)
		return volume;

		var multipliers = new[]
		{
			FirstMultiplier,
			SecondMultiplier,
			ThirdMultiplier,
			FourthMultiplier,
			FifthMultiplier,
		};

		var count = _closedTradeProfits.Count;
		for (var i = 0; i < multipliers.Length; i++)
		{
			if (count <= i)
			break;

			var profit = _closedTradeProfits[count - 1 - i];
			if (profit < 0m)
			{
				volume *= multipliers[i];
				break;
			}

			if (profit > 0m)
			break;
		}

		return volume;
	}

	private decimal AdjustVolume(decimal volume)
	{
		var security = Security;
		if (security != null)
		{
			var step = security.VolumeStep;
			if (step is decimal stepValue && stepValue > 0m)
			volume = Math.Round(volume / stepValue, MidpointRounding.AwayFromZero) * stepValue;

			if (volume < 0.01m)
			volume = 0.01m;
		}

		return volume > 0m ? volume : 0m;
	}

	private void UpdateCloseHistory(decimal close)
	{
		if (close <= 0m)
		return;

		_closeHistory.Add(close);

		var maxLength = Math.Max(HoursToCheckTrend + 2, 64);
		while (_closeHistory.Count > maxLength)
		_closeHistory.RemoveAt(0);
	}

	private void AddClosedTradeProfit(decimal profit)
	{
		_closedTradeProfits.Add(profit);
		while (_closedTradeProfits.Count > 5)
		_closedTradeProfits.RemoveAt(0);
	}

	private void ResetEntryState()
	{
		_entrySide = null;
		_entryVolume = 0m;
		_entryPrice = null;
		_entryTime = null;
		_trailingStopPrice = null;
	}

	private void InitializePipSize()
	{
		var security = Security;
		if (security == null)
		{
			_pipSize = 0m;
			return;
		}

		var step = security.PriceStep ?? 0m;
		if (step <= 0m)
		step = 0.0001m;

		if (security.Decimals is int decimals && (decimals == 3 || decimals == 5))
		_pipSize = step * 10m;
		else
		_pipSize = step;
	}

	private decimal EnsurePipSize()
	{
		if (_pipSize <= 0m)
		InitializePipSize();

		return _pipSize;
	}

	private decimal GetTrailingDistance()
	{
		var pip = EnsurePipSize();
		return pip > 0m ? TrailingStopPips * pip : 0m;
	}

	private static decimal RoundToOneDecimal(decimal value)
	{
		return Math.Round(value, 1, MidpointRounding.AwayFromZero);
	}
}