在 GitHub 上查看

自适应 Renko Duplex 策略

概览

Adaptive Renko Duplex StrategyExp_AdaptiveRenko_Duplex.mq5 专家顾问在 StockSharp 平台上的移植版本。新版本保留了原策略的核心思想——为多头和空头分别运行两套自适应 Renko 流,并通过高层 API 暴露全部逻辑。每个流都会根据近期波动率动态调整砖块高度,从而在价格两侧构建支持/阻力轨道;策略监控这些轨道中出现的趋势反转,并允许对多空参数进行独立配置。

不同于基于虚拟砖块运行的传统 Renko 系统,Duplex 方法监听标准 K 线,并在每根完整 K 线结束后重新计算自适应 Renko 缓冲区。只有在 K 线收盘后才会发出信号,以避免重绘并契合 StockSharp 的事件驱动模型。

市场数据与指标

  • K 线订阅:两个 DataType 参数分别指定多头与空头流所使用的 K 线序列,可以选择相同或不同的周期。
  • 自适应 Renko 重构:每个流都内嵌原指标算法。策略在“最小砖块高度(点数)”与 K × 波动率 之间取较大值来决定新的砖块尺寸,并维护上/下包络线以及彩色趋势线(上涨时为支撑、下跌时为阻力)。
  • 波动率来源:可在 AverageTrueRangeStandardDeviation 指标之间切换。两者都在各自的 K 线流上运行,并支持自定义回溯长度。

交易逻辑

  1. 多头检测
    • 多头流按照设定参数构建自适应砖块。
    • 当延迟 LongSignalBarOffset 指定的 K 线上出现 RenkoTrend.Up(上升趋势线)时,策略发出市价买单,订单量为 Volume + |Position|,以便快速从空头翻转到多头。
    • 如果在同样的延迟窗口内检测到 RenkoTrend.DownLongExitsEnabled 为真,则立即平掉所有多头仓位。
  2. 空头检测
    • 空头流采用相同的镜像逻辑:出现 RenkoTrend.Down 时卖出,RenkoTrend.Up 则在 ShortExitsEnabled 为真时平空。
  3. 信号延迟SignalBarOffset 控制信号延迟条数,复刻原 EA 中的“信号延后一根 K 线”行为。设为 0 可在最新收盘 K 线上直接响应。
  4. 仓位规模:策略完全依赖 Strategy.Volume 属性,因此在启动前务必设置目标手数。

风险管理

  • 止损/止盈:距离以“点”为单位设置,并乘以标的 PriceStep(若为空则退回到 MinStep)后得到绝对价格差。由于 StockSharp 不会自动创建服务器端保护单,所有退出都通过市价单完成。止损逻辑在每次订阅的 K 线收盘时执行。
  • 状态跟踪:策略记录最近一次多头或空头建仓时的价格(基于 K 线收盘价),用以计算与止损/止盈的距离。
  • 手工扩展:如需账户级别的风控,可在外部调用 StartProtection() 附加其他保护模块。

参数

参数 默认值 说明
LongCandleType 4 小时 多头信号使用的 K 线类型。
ShortCandleType 4 小时 空头信号使用的 K 线类型。
LongVolatilityMode ATR 多头砖块使用的波动率指标(ATR 或 StandardDeviation)。
ShortVolatilityMode ATR 空头砖块使用的波动率指标。
LongVolatilityPeriod 10 多头波动率指标的回溯长度。
ShortVolatilityPeriod 10 空头波动率指标的回溯长度。
LongSensitivity 1.0 多头砖块在波动率基础上的放大倍数。
ShortSensitivity 1.0 空头砖块在波动率基础上的放大倍数。
LongPriceMode Close 多头流使用的价格类型(HighLowClose)。
ShortPriceMode Close 空头流使用的价格类型。
LongMinimumBrickPoints 2 多头流的最小砖块高度(点数)。
ShortMinimumBrickPoints 2 空头流的最小砖块高度。
LongSignalBarOffset 1 多头信号确认所需延迟的 K 线数量。
ShortSignalBarOffset 1 空头信号确认所需延迟的 K 线数量。
LongEntriesEnabled true 是否允许多头建仓。
LongExitsEnabled true 是否允许基于 Renko 信号的多头平仓。
ShortEntriesEnabled true 是否允许空头建仓。
ShortExitsEnabled true 是否允许基于 Renko 信号的空头平仓。
LongStopLossPoints 1000 多头止损距离(点 × PriceStep)。
LongTakeProfitPoints 2000 多头止盈距离。
ShortStopLossPoints 1000 空头止损距离。
ShortTakeProfitPoints 2000 空头止盈距离。

点值换算:MetaTrader 中的“点”依赖于报价精度。移植后所有距离都会乘以 Security.PriceStep(若不可用则使用 MinStep)来得到实际价格增量。请根据标的的最小跳动调整默认值。

使用建议

  1. 先配置环境:在启动策略前设置 SecurityPortfolioVolume,并确保数据源能够提供所需的全部 K 线周期。
  2. 独立调节多空流:既可以保持对称配置,也可以分别设置不同的周期、波动率源或砖块参数,以实现多空不同的交易风格。
  3. 关注日志:策略在每次进出场时都会通过 LogInfo 输出触发的 Renko 水平,便于校验信号是否符合预期。
  4. 叠加外部模块:可结合会话过滤、资产组合风险控制等模块,通过 StockSharp 高层 API 将其与策略组合使用。
  5. 回测提示:在历史回测中,优先选择能够重建目标周期的 K 线生成器,以保持自适应 Renko 的一致性。

与原 EA 的差异

  • 移除了 MetaTrader 特有功能(魔术号、资金管理模式、滑点控制、消息推送等),仓位规模完全由 Volume 决定。
  • 原 EA 会同时下发止损/止盈挂单。移植版本改为在 K 线收盘时检查距离并通过市价单退出。
  • 所有信号仅在 K 线收盘后评估,避免部分 K 线阶段的重算;对应原 MQL 中的 IsNewBar 检查。
  • 自适应 Renko 重构算法保持不变,但使用 C# 直接实现,避免额外的指标集合,符合 StockSharp 高层 API 的惯例。

推荐拓展

  • 搭配更高层级的行情过滤器(如交易时段、波动率门槛)以规避流动性不足的时段。
  • 通过 StartProtection() 附加跟踪止损或权益保护模块,提升账户层级的风控能力。
  • 将生成的支撑/阻力轨道输出到图表或日志中,辅助人工复核策略表现。
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;

using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Dual-stream adaptive Renko strategy converted from the Exp_AdaptiveRenko_Duplex MQL5 expert advisor.
/// Generates independent long and short signals by projecting the Adaptive Renko indicator onto configurable candle series.
/// </summary>
public class AdaptiveRenkoDuplexStrategy : Strategy
{
	private readonly StrategyParam<DataType> _longCandleType;
	private readonly StrategyParam<DataType> _shortCandleType;
	private readonly StrategyParam<AdaptiveRenkoVolatilityModes> _longVolatilityMode;
	private readonly StrategyParam<AdaptiveRenkoVolatilityModes> _shortVolatilityMode;
	private readonly StrategyParam<int> _longVolatilityPeriod;
	private readonly StrategyParam<int> _shortVolatilityPeriod;
	private readonly StrategyParam<decimal> _longSensitivity;
	private readonly StrategyParam<decimal> _shortSensitivity;
	private readonly StrategyParam<AdaptiveRenkoPriceModes> _longPriceMode;
	private readonly StrategyParam<AdaptiveRenkoPriceModes> _shortPriceMode;
	private readonly StrategyParam<decimal> _longMinimumBrickPoints;
	private readonly StrategyParam<decimal> _shortMinimumBrickPoints;
	private readonly StrategyParam<int> _longSignalBarOffset;
	private readonly StrategyParam<int> _shortSignalBarOffset;
	private readonly StrategyParam<bool> _longEntriesEnabled;
	private readonly StrategyParam<bool> _longExitsEnabled;
	private readonly StrategyParam<bool> _shortEntriesEnabled;
	private readonly StrategyParam<bool> _shortExitsEnabled;
	private readonly StrategyParam<decimal> _longStopLossPoints;
	private readonly StrategyParam<decimal> _longTakeProfitPoints;
	private readonly StrategyParam<decimal> _shortStopLossPoints;
	private readonly StrategyParam<decimal> _shortTakeProfitPoints;

	private readonly AdaptiveRenkoProcessor _longProcessor = new();
	private readonly AdaptiveRenkoProcessor _shortProcessor = new();

	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;

	public AdaptiveRenkoDuplexStrategy()
	{
		_longCandleType = Param(nameof(LongCandleType), TimeSpan.FromDays(1).TimeFrame())
			.SetDisplay("Long Candle Type", "Timeframe used to derive long-side signals", "Long Side");

		_shortCandleType = Param(nameof(ShortCandleType), TimeSpan.FromDays(1).TimeFrame())
			.SetDisplay("Short Candle Type", "Timeframe used to derive short-side signals", "Short Side");

		_longVolatilityMode = Param(nameof(LongVolatilityMode), AdaptiveRenkoVolatilityModes.AverageTrueRange)
			.SetDisplay("Long Volatility Source", "Volatility measure controlling long Renko brick size", "Long Side");

		_shortVolatilityMode = Param(nameof(ShortVolatilityMode), AdaptiveRenkoVolatilityModes.AverageTrueRange)
			.SetDisplay("Short Volatility Source", "Volatility measure controlling short Renko brick size", "Short Side");

		_longVolatilityPeriod = Param(nameof(LongVolatilityPeriod), 10)
			.SetRange(1, 500)
			.SetDisplay("Long Volatility Period", "Lookback period for the volatility calculation", "Long Side")
			;

		_shortVolatilityPeriod = Param(nameof(ShortVolatilityPeriod), 10)
			.SetRange(1, 500)
			.SetDisplay("Short Volatility Period", "Lookback period for the volatility calculation", "Short Side")
			;

		_longSensitivity = Param(nameof(LongSensitivity), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Long Sensitivity", "Multiplier applied to volatility for long bricks", "Long Side")
			;

		_shortSensitivity = Param(nameof(ShortSensitivity), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Short Sensitivity", "Multiplier applied to volatility for short bricks", "Short Side")
			;

		_longPriceMode = Param(nameof(LongPriceMode), AdaptiveRenkoPriceModes.Close)
			.SetDisplay("Long Price Mode", "Price source used when building long bricks", "Long Side");

		_shortPriceMode = Param(nameof(ShortPriceMode), AdaptiveRenkoPriceModes.Close)
			.SetDisplay("Short Price Mode", "Price source used when building short bricks", "Short Side");

		_longMinimumBrickPoints = Param(nameof(LongMinimumBrickPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Long Minimum Brick", "Minimal brick height in points for long bricks", "Long Side");

		_shortMinimumBrickPoints = Param(nameof(ShortMinimumBrickPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Short Minimum Brick", "Minimal brick height in points for short bricks", "Short Side");

		_longSignalBarOffset = Param(nameof(LongSignalBarOffset), 2)
			.SetRange(0, 10)
			.SetDisplay("Long Signal Offset", "Number of closed bars to delay long signals", "Long Side");

		_shortSignalBarOffset = Param(nameof(ShortSignalBarOffset), 2)
			.SetRange(0, 10)
			.SetDisplay("Short Signal Offset", "Number of closed bars to delay short signals", "Short Side");

		_longEntriesEnabled = Param(nameof(LongEntriesEnabled), true)
			.SetDisplay("Enable Long Entries", "Allow long-side market entries", "Long Side");

		_longExitsEnabled = Param(nameof(LongExitsEnabled), true)
			.SetDisplay("Enable Long Exits", "Allow long-side exits triggered by Renko", "Long Side");

		_shortEntriesEnabled = Param(nameof(ShortEntriesEnabled), true)
			.SetDisplay("Enable Short Entries", "Allow short-side market entries", "Short Side");

		_shortExitsEnabled = Param(nameof(ShortExitsEnabled), true)
			.SetDisplay("Enable Short Exits", "Allow short-side exits triggered by Renko", "Short Side");

		_longStopLossPoints = Param(nameof(LongStopLossPoints), 1000m)
			.SetNotNegative()
			.SetDisplay("Long Stop Loss", "Protective stop distance in points for long trades", "Risk");

		_longTakeProfitPoints = Param(nameof(LongTakeProfitPoints), 2000m)
			.SetNotNegative()
			.SetDisplay("Long Take Profit", "Profit target distance in points for long trades", "Risk");

		_shortStopLossPoints = Param(nameof(ShortStopLossPoints), 1000m)
			.SetNotNegative()
			.SetDisplay("Short Stop Loss", "Protective stop distance in points for short trades", "Risk");

		_shortTakeProfitPoints = Param(nameof(ShortTakeProfitPoints), 2000m)
			.SetNotNegative()
			.SetDisplay("Short Take Profit", "Profit target distance in points for short trades", "Risk");
	}

	/// <summary>
	/// Candle stream used to compute long-side Renko structures.
	/// </summary>
	public DataType LongCandleType
	{
		get => _longCandleType.Value;
		set => _longCandleType.Value = value;
	}

	/// <summary>
	/// Candle stream used to compute short-side Renko structures.
	/// </summary>
	public DataType ShortCandleType
	{
		get => _shortCandleType.Value;
		set => _shortCandleType.Value = value;
	}

	/// <summary>
	/// Volatility mode for the long Renko stream.
	/// </summary>
	public AdaptiveRenkoVolatilityModes LongVolatilityMode
	{
		get => _longVolatilityMode.Value;
		set => _longVolatilityMode.Value = value;
	}

	/// <summary>
	/// Volatility mode for the short Renko stream.
	/// </summary>
	public AdaptiveRenkoVolatilityModes ShortVolatilityMode
	{
		get => _shortVolatilityMode.Value;
		set => _shortVolatilityMode.Value = value;
	}

	/// <summary>
	/// Lookback period for the long-side volatility indicator.
	/// </summary>
	public int LongVolatilityPeriod
	{
		get => _longVolatilityPeriod.Value;
		set => _longVolatilityPeriod.Value = value;
	}

	/// <summary>
	/// Lookback period for the short-side volatility indicator.
	/// </summary>
	public int ShortVolatilityPeriod
	{
		get => _shortVolatilityPeriod.Value;
		set => _shortVolatilityPeriod.Value = value;
	}

	/// <summary>
	/// Volatility multiplier that scales long-side bricks.
	/// </summary>
	public decimal LongSensitivity
	{
		get => _longSensitivity.Value;
		set => _longSensitivity.Value = value;
	}

	/// <summary>
	/// Volatility multiplier that scales short-side bricks.
	/// </summary>
	public decimal ShortSensitivity
	{
		get => _shortSensitivity.Value;
		set => _shortSensitivity.Value = value;
	}

	/// <summary>
	/// Price source used while building long bricks.
	/// </summary>
	public AdaptiveRenkoPriceModes LongPriceMode
	{
		get => _longPriceMode.Value;
		set => _longPriceMode.Value = value;
	}

	/// <summary>
	/// Price source used while building short bricks.
	/// </summary>
	public AdaptiveRenkoPriceModes ShortPriceMode
	{
		get => _shortPriceMode.Value;
		set => _shortPriceMode.Value = value;
	}

	/// <summary>
	/// Minimal brick height for the long Renko stream (expressed in points).
	/// </summary>
	public decimal LongMinimumBrickPoints
	{
		get => _longMinimumBrickPoints.Value;
		set => _longMinimumBrickPoints.Value = value;
	}

	/// <summary>
	/// Minimal brick height for the short Renko stream (expressed in points).
	/// </summary>
	public decimal ShortMinimumBrickPoints
	{
		get => _shortMinimumBrickPoints.Value;
		set => _shortMinimumBrickPoints.Value = value;
	}

	/// <summary>
	/// Number of closed bars to wait before using a long-side signal.
	/// </summary>
	public int LongSignalBarOffset
	{
		get => _longSignalBarOffset.Value;
		set => _longSignalBarOffset.Value = value;
	}

	/// <summary>
	/// Number of closed bars to wait before using a short-side signal.
	/// </summary>
	public int ShortSignalBarOffset
	{
		get => _shortSignalBarOffset.Value;
		set => _shortSignalBarOffset.Value = value;
	}

	/// <summary>
	/// Enables long-side entries.
	/// </summary>
	public bool LongEntriesEnabled
	{
		get => _longEntriesEnabled.Value;
		set => _longEntriesEnabled.Value = value;
	}

	/// <summary>
	/// Enables Renko-driven exits for long positions.
	/// </summary>
	public bool LongExitsEnabled
	{
		get => _longExitsEnabled.Value;
		set => _longExitsEnabled.Value = value;
	}

	/// <summary>
	/// Enables short-side entries.
	/// </summary>
	public bool ShortEntriesEnabled
	{
		get => _shortEntriesEnabled.Value;
		set => _shortEntriesEnabled.Value = value;
	}

	/// <summary>
	/// Enables Renko-driven exits for short positions.
	/// </summary>
	public bool ShortExitsEnabled
	{
		get => _shortExitsEnabled.Value;
		set => _shortExitsEnabled.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for long positions expressed in indicator points.
	/// </summary>
	public decimal LongStopLossPoints
	{
		get => _longStopLossPoints.Value;
		set => _longStopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance for long positions expressed in indicator points.
	/// </summary>
	public decimal LongTakeProfitPoints
	{
		get => _longTakeProfitPoints.Value;
		set => _longTakeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance for short positions expressed in indicator points.
	/// </summary>
	public decimal ShortStopLossPoints
	{
		get => _shortStopLossPoints.Value;
		set => _shortStopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance for short positions expressed in indicator points.
	/// </summary>
	public decimal ShortTakeProfitPoints
	{
		get => _shortTakeProfitPoints.Value;
		set => _shortTakeProfitPoints.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security == null)
			yield break;

		yield return (Security, LongCandleType);

		if (ShortCandleType != LongCandleType)
			yield return (Security, ShortCandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_longProcessor.Reset();
		_shortProcessor.Reset();
		_longEntryPrice = null;
		_shortEntryPrice = null;
	}

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

		_longProcessor.Reset();
		_shortProcessor.Reset();
		_longEntryPrice = null;
		_shortEntryPrice = null;

		var longIndicator = CreateVolatilityIndicator(LongVolatilityMode, LongVolatilityPeriod);
		var longSubscription = SubscribeCandles(LongCandleType);
		longSubscription.BindEx(longIndicator, ProcessLongCandle);

		var shortIndicator = CreateVolatilityIndicator(ShortVolatilityMode, ShortVolatilityPeriod);

		if (ShortCandleType == LongCandleType)
		{
			longSubscription.BindEx(shortIndicator, ProcessShortCandle);
			longSubscription.Start();
		}
		else
		{
			longSubscription.Start();
			var shortSubscription = SubscribeCandles(ShortCandleType);
			shortSubscription.BindEx(shortIndicator, ProcessShortCandle);
			shortSubscription.Start();
		}
	}

	private void ProcessLongCandle(ICandleMessage candle, IIndicatorValue volatilityValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		ManageLongRisk(candle);

		if (!volatilityValue.IsFinal)
			return;

		var step = GetPriceStep();
		var volatility = volatilityValue.ToDecimal();
		var snapshot = _longProcessor.Process(candle, volatility, LongSensitivity, LongMinimumBrickPoints, LongPriceMode, LongSignalBarOffset, step);

		if (snapshot == null)
			return;

		var signal = _longProcessor.GetSnapshot(LongSignalBarOffset);
		if (signal == null)
			return;

		if (LongExitsEnabled && Position > 0 && signal.Value.Trend == RenkoTrends.Down)
		{
			TryCloseLong("Adaptive Renko bearish reversal", candle);
		}

		if (LongEntriesEnabled && signal.Value.Trend == RenkoTrends.Up)
		{
			TryOpenLong(candle, signal.Value);
		}
	}

	private void ProcessShortCandle(ICandleMessage candle, IIndicatorValue volatilityValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		ManageShortRisk(candle);

		if (!volatilityValue.IsFinal)
			return;

		var step = GetPriceStep();
		var volatility = volatilityValue.ToDecimal();
		var snapshot = _shortProcessor.Process(candle, volatility, ShortSensitivity, ShortMinimumBrickPoints, ShortPriceMode, ShortSignalBarOffset, step);

		if (snapshot == null)
			return;

		var signal = _shortProcessor.GetSnapshot(ShortSignalBarOffset);
		if (signal == null)
			return;

		if (ShortExitsEnabled && Position < 0 && signal.Value.Trend == RenkoTrends.Up)
		{
			TryCloseShort("Adaptive Renko bullish reversal", candle);
		}

		if (ShortEntriesEnabled && signal.Value.Trend == RenkoTrends.Down)
		{
			TryOpenShort(candle, signal.Value);
		}
	}

	private void ManageLongRisk(ICandleMessage candle)
	{
		if (Position <= 0)
		{
			_longEntryPrice = null;
			return;
		}

		if (_longEntryPrice == null)
			_longEntryPrice = candle.ClosePrice;

		var step = GetPriceStep();

		if (LongStopLossPoints > 0m)
		{
			var stopDistance = LongStopLossPoints * step;
			if (stopDistance > 0m && candle.LowPrice <= _longEntryPrice.Value - stopDistance)
			{
				TryCloseLong("Long stop loss reached", candle);
				return;
			}
		}

		if (LongTakeProfitPoints > 0m)
		{
			var targetDistance = LongTakeProfitPoints * step;
			if (targetDistance > 0m && candle.HighPrice >= _longEntryPrice.Value + targetDistance)
			{
				TryCloseLong("Long take profit reached", candle);
			}
		}
	}

	private void ManageShortRisk(ICandleMessage candle)
	{
		if (Position >= 0)
		{
			_shortEntryPrice = null;
			return;
		}

		if (_shortEntryPrice == null)
			_shortEntryPrice = candle.ClosePrice;

		var step = GetPriceStep();

		if (ShortStopLossPoints > 0m)
		{
			var stopDistance = ShortStopLossPoints * step;
			if (stopDistance > 0m && candle.HighPrice >= _shortEntryPrice.Value + stopDistance)
			{
				TryCloseShort("Short stop loss reached", candle);
				return;
			}
		}

		if (ShortTakeProfitPoints > 0m)
		{
			var targetDistance = ShortTakeProfitPoints * step;
			if (targetDistance > 0m && candle.LowPrice <= _shortEntryPrice.Value - targetDistance)
			{
				TryCloseShort("Short take profit reached", candle);
			}
		}
	}

	private void TryOpenLong(ICandleMessage candle, RenkoSnapshot signal)
	{
		if (Position > 0)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
		{
			LogWarning("Volume must be positive to open a long position.");
			return;
		}

		BuyMarket(volume);
		_longEntryPrice = candle.ClosePrice;
		_shortEntryPrice = null;
		LogInfo($"Long entry triggered. Trend level: {signal.Support?.ToString("F5") ?? "n/a"}.");
	}

	private void TryOpenShort(ICandleMessage candle, RenkoSnapshot signal)
	{
		if (Position < 0)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
		{
			LogWarning("Volume must be positive to open a short position.");
			return;
		}

		SellMarket(volume);
		_shortEntryPrice = candle.ClosePrice;
		_longEntryPrice = null;
		LogInfo($"Short entry triggered. Trend level: {signal.Resistance?.ToString("F5") ?? "n/a"}.");
	}

	private void TryCloseLong(string reason, ICandleMessage candle)
	{
		if (Position <= 0)
		{
			_longEntryPrice = null;
			return;
		}

		SellMarket(Math.Abs(Position));
		_longEntryPrice = null;
		LogInfo($"Long exit: {reason} at {candle.ClosePrice:F5}.");
	}

	private void TryCloseShort(string reason, ICandleMessage candle)
	{
		if (Position >= 0)
		{
			_shortEntryPrice = null;
			return;
		}

		BuyMarket(Math.Abs(Position));
		_shortEntryPrice = null;
		LogInfo($"Short exit: {reason} at {candle.ClosePrice:F5}.");
	}

	private static IIndicator CreateVolatilityIndicator(AdaptiveRenkoVolatilityModes mode, int period)
	{
		return mode switch
		{
			AdaptiveRenkoVolatilityModes.AverageTrueRange => new AverageTrueRange { Length = period },
			AdaptiveRenkoVolatilityModes.StandardDeviation => new StandardDeviation { Length = period },
			_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported volatility mode"),
		};
	}

	private decimal GetPriceStep()
	{
		var security = Security;
		if (security == null)
			return 1m;

		if (security.PriceStep != null && security.PriceStep.Value > 0m)
			return security.PriceStep.Value;

		return 1m;
	}

	private enum RenkoTrends
	{
		None = 0,
		Up = 1,
		Down = -1
	}

	private readonly struct RenkoSnapshot
	{
		public RenkoSnapshot(DateTimeOffset time, RenkoTrends trend, decimal? support, decimal? resistance)
		{
			Time = time;
			Trend = trend;
			Support = support;
			Resistance = resistance;
		}

		public DateTimeOffset Time { get; }

		public RenkoTrends Trend { get; }

		public decimal? Support { get; }

		public decimal? Resistance { get; }
	}

	private sealed class AdaptiveRenkoProcessor : IEquatable<AdaptiveRenkoProcessor>
	{
		private readonly List<RenkoSnapshot> _history = new();
		private bool _initialized;
		private decimal _up;
		private decimal _down;
		private decimal _brick;
		private RenkoTrends _trend;

		public RenkoSnapshot? Process(ICandleMessage candle, decimal volatility, decimal sensitivity, decimal minimumBrickPoints, AdaptiveRenkoPriceModes priceMode, int signalOffset, decimal step)
		{
			var (high, low) = priceMode == AdaptiveRenkoPriceModes.Close
				? (candle.ClosePrice, candle.ClosePrice)
				: (candle.HighPrice, candle.LowPrice);

			var minBrick = Math.Max(minimumBrickPoints * step, 0m);

			if (!_initialized)
			{
				var range = Math.Max(high - low, 0m);
				var initialBrick = Math.Max(sensitivity * range, minBrick);

				_up = high;
				_down = low;
				_brick = initialBrick > 0m ? initialBrick : minBrick;
				_trend = RenkoTrends.None;
				_initialized = true;

				var initialSnapshot = new RenkoSnapshot(GetCandleTime(candle), RenkoTrends.None, null, null);
				AppendSnapshot(initialSnapshot, signalOffset);
				return initialSnapshot;
			}

			var up = _up;
			var down = _down;
			var brick = _brick > 0m ? _brick : minBrick;
			var trend = _trend;

			var adjustedBrick = Math.Max(sensitivity * Math.Abs(volatility), minBrick);
			if (adjustedBrick <= 0m)
				adjustedBrick = minBrick;

			if (brick <= 0m)
				brick = adjustedBrick > 0m ? adjustedBrick : minBrick;

			if (high > up + brick)
			{
				if (brick > 0m)
				{
					var diff = high - up;
					var bricks = Math.Floor(diff / brick);
					if (bricks < 1m)
						bricks = 1m;
					up += bricks * brick;
				}
				else
				{
					up = high;
				}

				brick = adjustedBrick;
				down = up - brick;
			}

			if (low < down - brick)
			{
				if (brick > 0m)
				{
					var diff = down - low;
					var bricks = Math.Floor(diff / brick);
					if (bricks < 1m)
						bricks = 1m;
					down -= bricks * brick;
				}
				else
				{
					down = low;
				}

				brick = adjustedBrick;
				up = down + brick;
			}

			if (_up < up)
				trend = RenkoTrends.Up;

			if (_down > down)
				trend = RenkoTrends.Down;

			_up = up;
			_down = down;
			_brick = brick;
			_trend = trend;

			var support = trend == RenkoTrends.Up ? down - brick : (decimal?)null;
			var resistance = trend == RenkoTrends.Down ? up + brick : (decimal?)null;

			var snapshot = new RenkoSnapshot(GetCandleTime(candle), trend, support, resistance);
			AppendSnapshot(snapshot, signalOffset);
			return snapshot;
		}

		public RenkoSnapshot? GetSnapshot(int shift)
		{
			if (shift < 0)
				shift = 0;

			var index = _history.Count - 1 - shift;
			if (index < 0)
				return null;

			return _history[index];
		}

		public void Reset()
		{
			_history.Clear();
			_initialized = false;
			_up = 0m;
			_down = 0m;
			_brick = 0m;
			_trend = RenkoTrends.None;
		}

		public bool Equals(AdaptiveRenkoProcessor other)
		{
			if (ReferenceEquals(null, other))
				return false;

			if (ReferenceEquals(this, other))
				return true;

			if (_initialized != other._initialized ||
				_up != other._up ||
				_down != other._down ||
				_brick != other._brick ||
				_trend != other._trend ||
				_history.Count != other._history.Count)
				return false;

			for (var i = 0; i < _history.Count; i++)
			{
				if (!_history[i].Equals(other._history[i]))
					return false;
			}

			return true;
		}

		public override bool Equals(object obj)
			=> obj is AdaptiveRenkoProcessor other && Equals(other);

		public override int GetHashCode()
		{
			var hash = HashCode.Combine(_initialized, _up, _down, _brick, _trend, _history.Count);

			foreach (var item in _history)
				hash = HashCode.Combine(hash, item);

			return hash;
		}

		private void AppendSnapshot(RenkoSnapshot snapshot, int signalOffset)
		{
			_history.Add(snapshot);
			var maxHistory = Math.Max(signalOffset + 3, 8);
			var overflow = _history.Count - maxHistory;
			if (overflow > 0)
				_history.RemoveRange(0, overflow);
		}

		private static DateTimeOffset GetCandleTime(ICandleMessage candle)
		{
			if (candle.CloseTime != default)
				return candle.CloseTime;

			return candle.ServerTime;
		}
	}

	public enum AdaptiveRenkoVolatilityModes
	{
		AverageTrueRange,
		StandardDeviation
	}

	public enum AdaptiveRenkoPriceModes
	{
		HighLow,
		Close
	}
}