# Volume Profiles

```
//------------------------------------------------------------------------------
//
// Индикатор Volume Profiles. Copyright (c) 2023 Tiger Trade Capital AG. All rights reserved.
//
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Windows;
using System.Windows.Media;
using TigerTrade.Chart.Base.Enums;
using TigerTrade.Chart.Data;
using TigerTrade.Chart.Indicators.Common;
using TigerTrade.Chart.Indicators.Enums;
using TigerTrade.Core.UI.Converters;
using TigerTrade.Core.Utils.Time;
using TigerTrade.Dx;
using TigerTrade.Dx.Enums;
 
namespace TigerTrade.Chart.Indicators.Custom
{
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "VolumeProfilesPeriodType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum VolumeProfilesPeriodType
    {
        [EnumMember(Value = "Minute"), Description("Минута")]
        Minute,
        [EnumMember(Value = "Hour"), Description("Час")]
        Hour,
        [EnumMember(Value = "Day"), Description("День")]
        Day,
        [EnumMember(Value = "Week"), Description("Неделя")]
        Week,
        [EnumMember(Value = "Month"), Description("Месяц")]
        Month
    }
 
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "VolumeProfilesType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum VolumeProfilesType
    {
        [EnumMember(Value = "Volume"), Description("Volume")]
        Volume,
        [EnumMember(Value = "Trades"), Description("Trades")]
        Trades,
        [EnumMember(Value = "Delta"), Description("Delta")]
        Delta,
        [EnumMember(Value = "BidAsk"), Description("Bid x Ask")]
        BidAsk
    }
 
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "VolumeProfilesMaximumType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum VolumeProfilesMaximumType
    {
        [EnumMember(Value = "Volume"), Description("Volume")]
        Volume,
        [EnumMember(Value = "Trades"), Description("Trades")]
        Trades,
        [EnumMember(Value = "Delta"), Description("Delta")]
        Delta,
        [EnumMember(Value = "DeltaPlus"), Description("Delta+")]
        DeltaPlus,
        [EnumMember(Value = "DeltaMinus"), Description("Delta-")]
        DeltaMinus,
        [EnumMember(Value = "Bid"), Description("Bid")]
        Bid,
        [EnumMember(Value = "Ask"), Description("Ask")]
        Ask
    }
 
    [DataContract(Name = "VolumeProfilesIndicator", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    [Indicator("X_VolumeProfiles", "*VolumeProfiles", true, Type = typeof(VolumeProfilesIndicator))]
    internal sealed class VolumeProfilesIndicator : IndicatorBase
    {
        private List<VolumeProfile> _profiles;
 
        private List<VolumeProfile> Profiles => _profiles ?? (_profiles = new List<VolumeProfile>());
 
        private VolumeProfilesPeriodType _periodType;
 
        [DataMember(Name = "PeriodType")]
        [Category("Период"), DisplayName("Интервал")]
        public VolumeProfilesPeriodType PeriodType
        {
            get => _periodType;
            set
            {
                if (value == _periodType)
                {
                    return;
                }
 
                _periodType = value;
 
                _periodValue = _periodType == VolumeProfilesPeriodType.Minute ? 15 : 1;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int _periodValue;
 
        [DataMember(Name = "PeriodValue")]
        [Category("Период"), DisplayName("Значение")]
        public int PeriodValue
        {
            get => _periodValue;
            set
            {
                value = Math.Max(1, value);
 
                if (value == _periodValue)
                {
                    return;
                }
 
                _periodValue = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private VolumeProfilesType _profileType;
 
        [DataMember(Name = "ProfileType")]
        [Category("Профиль"), DisplayName("Тип")]
        public VolumeProfilesType ProfileType
        {
            get => _profileType;
            set
            {
                if (value == _profileType)
                {
                    return;
                }
 
                _profileType = value;
 
                OnPropertyChanged();
            }
        }
 
        private int _profileProportion;
 
        [DataMember(Name = "ProfileProportion")]
        [Category("Профиль"), DisplayName("Объём шкалы")]
        public int ProfileProportion
        {
            get => _profileProportion;
            set
            {
                value = Math.Max(0, value);
 
                if (value == _profileProportion)
                {
                    return;
                }
 
                _profileProportion = value;
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _profileBrush;
 
        private XColor _profileColor;
 
        [DataMember(Name = "ProfileColor")]
        [Category("Профиль"), DisplayName("Цвет профиля")]
        public XColor ProfileColor
        {
            get => _profileColor;
            set
            {
                if (value == _profileColor)
                {
                    return;
                }
 
                _profileColor = value;
 
                _profileBrush = new XBrush(_profileColor);
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _backBrush;
 
        private XColor _profileBackColor;
 
        [DataMember(Name = "ProfileBackColor")]
        [Category("Профиль"), DisplayName("Цвет фона")]
        public XColor ProfileBackColor
        {
            get => _profileBackColor;
            set
            {
                if (value == _profileBackColor)
                {
                    return;
                }
 
                _profileBackColor = value;
 
                _backBrush = new XBrush(_profileBackColor);
 
                OnPropertyChanged();
            }
        }
 
        private bool _showValues;
 
        [DataMember(Name = "ShowValues")]
        [Category("Значения"), DisplayName("Отображать")]
        public bool ShowValues
        {
            get => _showValues;
            set
            {
                if (value == _showValues)
                {
                    return;
                }
 
                _showValues = value;
 
                OnPropertyChanged();
            }
        }
 
        private bool _minimizeValues;
 
        [DataMember(Name = "MinimizeValues")]
        [Category("Значения"), DisplayName("Минимизировать")]
        public bool MinimizeValues
        {
            get => _minimizeValues;
            set
            {
                if (value == _minimizeValues)
                {
                    return;
                }
 
                _minimizeValues = value;
 
                OnPropertyChanged();
            }
        }
 
        private IndicatorIntParam _roundValueParam;
 
        [DataMember(Name = "RoundValueParam")]
        public IndicatorIntParam RoundValuesParam
        {
            get => _roundValueParam ?? (_roundValueParam = new IndicatorIntParam(0));
            set => _roundValueParam = value;
        }
 
        [DefaultValue(0)]
        [Category("Значения"), DisplayName("Округлять")]
        public int RoundValues
        {
            get => RoundValuesParam.Get(SettingsLongKey);
            set
            {
                if (!RoundValuesParam.Set(SettingsLongKey, value, -4, 4))
                {
                    return;
                }
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _valuesBrush;
 
        private XColor _valuesColor;
 
        [DataMember(Name = "ValuesColor")]
        [Category("Значения"), DisplayName("Цвет")]
        public XColor ValuesColor
        {
            get => _valuesColor;
            set
            {
                if (value == _valuesColor)
                {
                    return;
                }
 
                _valuesColor = value;
 
                _valuesBrush = new XBrush(_valuesColor);
 
                OnPropertyChanged();
            }
        }
 
        private VolumeProfilesMaximumType _maximumType;
 
        [DataMember(Name = "MaximumType")]
        [Category("Максимум"), DisplayName("Тип")]
        public VolumeProfilesMaximumType MaximumType
        {
            get => _maximumType;
            set
            {
                if (value == _maximumType)
                {
                    return;
                }
 
                _maximumType = value;
 
                OnPropertyChanged();
            }
        }
 
        private bool _showMaximum;
 
        [DataMember(Name = "ShowMaximum")]
        [Category("Максимум"), DisplayName("Отображать")]
        public bool ShowMaximum
        {
            get => _showMaximum;
            set
            {
                if (value == _showMaximum)
                {
                    return;
                }
 
                _showMaximum = value;
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _maximumBrush;
 
        private XColor _maximumColor;
 
        [DataMember(Name = "MaximumColor")]
        [Category("Максимум"), DisplayName("Цвет")]
        public XColor MaximumColor
        {
            get => _maximumColor;
            set
            {
                if (value == _maximumColor)
                {
                    return;
                }
 
                _maximumColor = value;
 
                _maximumBrush = new XBrush(_maximumColor);
 
                OnPropertyChanged();
            }
        }
 
        private bool _showValueArea;
 
        [DataMember(Name = "ShowValueArea")]
        [Category("Value Area"), DisplayName("Отображать")]
        public bool ShowValueArea
        {
            get => _showValueArea;
            set
            {
                if (value == _showValueArea)
                {
                    return;
                }
 
                _showValueArea = value;
 
                OnPropertyChanged();
            }
        }
 
        private int _valueAreaPercent;
 
        [DataMember(Name = "ValueAreaPercent")]
        [Category("Value Area"), DisplayName("ValueArea %")]
        public int ValueAreaPercent
        {
            get => _valueAreaPercent;
            set
            {
                value = Math.Max(0, Math.Min(100, value));
 
                if (value == 0)
                {
                    value = 70;
                }
 
                if (value == _valueAreaPercent)
                {
                    return;
                }
 
                _valueAreaPercent = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _valueAreaBrush;
 
        private XColor _valueAreaColor;
 
        [DataMember(Name = "ValueAreaColor")]
        [Category("Value Area"), DisplayName("Цвет")]
        public XColor ValueAreaColor
        {
            get => _valueAreaColor;
            set
            {
                if (value == _valueAreaColor)
                {
                    return;
                }
 
                _valueAreaColor = value;
 
                _valueAreaBrush = new XBrush(_valueAreaColor);
 
                OnPropertyChanged();
            }
        }
 
        private bool _enableFilter;
 
        [DataMember(Name = "EnableFilter")]
        [Category("Фильтр"), DisplayName("Включить")]
        public bool EnableFilter
        {
            get => _enableFilter;
            set
            {
                if (value == _enableFilter)
                {
                    return;
                }
 
                _enableFilter = value;
 
                OnPropertyChanged();
            }
        }
 
        private int _filterMin;
 
        [DataMember(Name = "FilterValue")]
        [Category("Фильтр"), DisplayName("Минимум")]
        public int FilterMin
        {
            get => _filterMin;
            set
            {
                value = Math.Max(0, value);
 
                if (value == _filterMin)
                {
                    return;
                }
 
                _filterMin = value;
 
                OnPropertyChanged();
            }
        }
 
        private int _filterMax;
 
        [DataMember(Name = "FilterMax")]
        [Category("Фильтр"), DisplayName("Максимум")]
        public int FilterMax
        {
            get => _filterMax;
            set
            {
                value = Math.Max(0, value);
 
                if (value == _filterMax)
                {
                    return;
                }
 
                _filterMax = value;
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _filterBrush;
 
        private XColor _filterColor;
 
        [DataMember(Name = "FilterColor")]
        [Category("Фильтр"), DisplayName("Цвет")]
        public XColor FilterColor
        {
            get => _filterColor;
            set
            {
                if (value == _filterColor)
                {
                    return;
                }
 
                _filterColor = value;
 
                _filterBrush = new XBrush(_filterColor);
 
                OnPropertyChanged();
            }
        }
 
        [Browsable(false)]
        public override bool ShowIndicatorValues => false;
 
        [Browsable(false)]
        public override bool ShowIndicatorLabels => false;
 
        [Browsable(false)]
        public override IndicatorCalculation Calculation => IndicatorCalculation.OnEachTick;
 
        public VolumeProfilesIndicator()
        {
            ShowIndicatorTitle = false;
 
            PeriodType = VolumeProfilesPeriodType.Hour;
            PeriodValue = 1;
 
            ProfileType = VolumeProfilesType.Volume;
            ProfileProportion = 0;
            ProfileColor = Color.FromArgb(127, 70, 130, 180);
            ProfileBackColor = Color.FromArgb(30, 70, 130, 180);
 
            ShowValues = false;
            MinimizeValues = false;
            ValuesColor = Color.FromArgb(255, 255, 255, 255);
 
            MaximumType = VolumeProfilesMaximumType.Volume;
            ShowMaximum = true;
            MaximumColor = Color.FromArgb(127, 178, 34, 34);
 
            ShowValueArea = true;
            ValueAreaPercent = 70;
            ValueAreaColor = Color.FromArgb(127, 128, 128, 128);
 
            EnableFilter = false;
            FilterMin = 0;
            FilterMax = 0;
            FilterColor = Color.FromArgb(127, 46, 139, 87);
        }
 
        private int _lastFullID;
 
        private void Clear()
        {
            _lastFullID = 0;
 
            Profiles.Clear();
        }
 
        private int GetSequence(DateTime date, double offset)
        {
            var cycleBase = ChartPeriodType.Hour;
 
            switch (PeriodType)
            {
                case VolumeProfilesPeriodType.Minute:
 
                    cycleBase = ChartPeriodType.Minute;
 
                    break;
 
                case VolumeProfilesPeriodType.Hour:
 
                    cycleBase = ChartPeriodType.Hour;
 
                    break;
 
                case VolumeProfilesPeriodType.Day:
 
                    cycleBase = ChartPeriodType.Day;
 
                    break;
 
                case VolumeProfilesPeriodType.Week:
 
                    cycleBase = ChartPeriodType.Week;
 
                    break;
 
                case VolumeProfilesPeriodType.Month:
 
                    cycleBase = ChartPeriodType.Month;
 
                    break;
            }
 
            return DataProvider.Period.GetSequence(cycleBase, PeriodValue, date, offset);
        }
 
        protected override void Execute()
        {
            if (ClearData)
            {
                Clear();
            }
 
            if (Profiles.Count > 0 && !Profiles[Profiles.Count - 1].Completed)
            {
                Profiles.RemoveAt(Profiles.Count - 1);
            }
 
            var timeOffset = TimeHelper.GetSessionOffset(DataProvider.Symbol.Exchange);
 
            var lastSequence = -1;
 
            for (var i = _lastFullID; i < DataProvider.Count; i++)
            {
                var cluster = DataProvider.GetRawCluster(i);
 
                var currSequence = GetSequence(cluster.Time, timeOffset);
 
                if (Profiles.Count == 0 || currSequence != lastSequence)
                {
                    lastSequence = currSequence;
 
                    if (Profiles.Count > 0 && i > _lastFullID)
                    {
                        _lastFullID = i;
 
                        Profiles[Profiles.Count - 1].Completed = true;
 
                        Profiles[Profiles.Count - 1].Cluster.UpdateData();
                    }
 
                    Profiles.Add(new VolumeProfile(new RawCluster(cluster.Time), i));
                }
 
                var lastCluster = Profiles[Profiles.Count - 1];
 
                lastCluster.Cluster.AddCluster(cluster);
 
                lastCluster.EndBar = i;
            }
 
            if (Profiles.Count > 0 && !Profiles[Profiles.Count - 1].Completed)
            {
                Profiles[Profiles.Count - 1].Cluster.UpdateData();
            }
        }
 
        public override void Render(DxVisualQueue visual)
        {
            if (Profiles.Count == 0)
            {
                return;
            }
 
            var minPrice = (long)(Canvas.MinY / Helper.DataProvider.Step) - 1;
            var maxPrice = (long)(Canvas.MaxY / Helper.DataProvider.Step) + 1;
 
            if (maxPrice - minPrice > 20000)
            {
                return;
            }
 
            switch (ProfileType)
            {
                case VolumeProfilesType.Volume:
 
                    RenderVolume(visual);
 
                    break;
 
                case VolumeProfilesType.Trades:
 
                    RenderTrades(visual);
 
                    break;
 
                case VolumeProfilesType.Delta:
 
                    RenderDelta(visual);
 
                    break;
 
                case VolumeProfilesType.BidAsk:
 
                    RenderBidAsk(visual);
 
                    break;
            }
        }
 
        private void RenderVolume(DxVisualQueue visual)
        {
            var startIndex = Canvas.Stop;
            var endIndex = Canvas.Stop + Canvas.Count;
 
            var step = DataProvider.Step;
            var symbol = DataProvider.Symbol;
 
            var minFilter = symbol.CorrectSizeFilter(FilterMin);
            var maxFilter = symbol.CorrectSizeFilter(FilterMax);
 
            var proportion = symbol.CorrectSizeFilter(ProfileProportion);
 
            var height = GetY(0.0) - GetY(step);
 
            var fontSize = Math.Min(height - 2, 18) * 96 / 72;
 
            fontSize = Math.Min(fontSize, Canvas.ChartFont.Size);
 
            var normalFont = new XFont(Canvas.ChartFont.Name, fontSize);
 
            var columnWidth2 = Canvas.ColumnWidth / 2.0;
 
            var roundValues = RoundValues;
 
            var prevRight = int.MinValue;
 
            foreach (var volumeProfile in Profiles)
            {
                if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
                {
                    continue;
                }
 
                var x1 = Canvas.GetX(volumeProfile.StartBar);
                var x2 = Canvas.GetX(volumeProfile.EndBar);
 
                var profile = volumeProfile.Cluster;
 
                var maxValues = profile.MaxValues;
 
                var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
 
                var left = x1 - columnWidth2;
                var right = x2 + columnWidth2 - 1;
 
                if (prevRight != int.MinValue)
                {
                    left = prevRight;
                }
 
                prevRight = (int)right;
 
                var dist = Math.Max(right - left, 1);
 
                var max = ProfileProportion > 0 ? proportion : maxValues.MaxVolume;
 
                var volStep = dist / Math.Max(max, 1);
 
                var colorRects = new List<Tuple<Rect, XBrush>>();
                var colorRects2 = new List<Tuple<Rect, XBrush>>();
                var valueRects = new List<Tuple<Rect, string>>();
 
                var prevX = (int)left;
                var prevY = (int)GetY((profile.High + .5) * step);
 
                var points = new List<Point>
                {
                    new Point(prevX, prevY)
                };
 
                for (var j = profile.High; j >= profile.Low; j--)
                {
                    var item = profile.GetItem(j) ?? new RawClusterItem(j);
 
                    var width = Math.Min(volStep * item.Volume, dist);
 
                    var currX = (int)(left + width);
                    var currY = (int)GetY((j - .5) * step);
 
                    var currHeight = Math.Max(currY - prevY, 1);
 
                    if (currY == prevY && points.Count > 2)
                    {
                        if (currX > prevX)
                        {
                            points[points.Count - 2] = new Point(currX, points[points.Count - 2].Y);
                            points[points.Count - 1] = new Point(currX, points[points.Count - 1].Y);
 
                            prevX = currX;
                        }
                    }
                    else
                    {
                        points.Add(new Point(currX, prevY));
                        points.Add(new Point(currX, currY));
 
                        prevX = currX;
                    }
 
                    prevY = currY;
 
                    var topY = points[points.Count - 2].Y;
 
                    if (ShowMaximum && CheckMaximum(item, maxValues))
                    {
                        colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
                    }
                    else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
                    {
                        colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight),
                            _valueAreaBrush));
                    }
                    else if (EnableFilter && item.Volume >= minFilter &&
                             (maxFilter == 0 || item.Volume <= maxFilter))
                    {
                        if (colorRects.Count > 0)
                        {
                            var lastRect = colorRects[colorRects.Count - 1].Item1;
 
                            if ((int)lastRect.Y == (int)topY)
                            {
                                if (width > lastRect.Width)
                                {
                                    colorRects[colorRects.Count - 1] =
                                        new Tuple<Rect, XBrush>(new Rect(left, topY, width, lastRect.Height), _filterBrush);
                                }
                            }
                            else
                            {
                                colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight),
                                    _filterBrush));
                            }
                        }
                        else
                        {
                            colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight), _filterBrush));
                        }
                    }
 
                    if (ShowValues && height > 7 && item.Volume > 0)
                    {
                        valueRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist, height),
                            symbol.FormatRawSize(item.Volume, roundValues, MinimizeValues)));
                    }
                }
 
                points.Add(new Point(left, prevY));
 
                visual.FillRectangle(_backBrush,
                    new Rect(new Point(left, points[0].Y), new Point(right, prevY)));
 
                visual.FillPolygon(_profileBrush, points.ToArray());
 
                foreach (var colorRect in colorRects)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var colorRect in colorRects2)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var valueRect in valueRects)
                {
                    visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
                }
            }
        }
 
        private void RenderTrades(DxVisualQueue visual)
        {
            var startIndex = Canvas.Stop;
            var endIndex = Canvas.Stop + Canvas.Count;
 
            var step = DataProvider.Step;
            var symbol = DataProvider.Symbol;
 
            var height = GetY(0.0) - GetY(step);
 
            var fontSize = Math.Min(height - 2, 18) * 96 / 72;
 
            fontSize = Math.Min(fontSize, Canvas.ChartFont.Size);
 
            var normalFont = new XFont(Canvas.ChartFont.Name, fontSize);
 
            var columnWidth2 = Canvas.ColumnWidth / 2.0;
 
            var roundValues = RoundValues;
 
            var prevRight = int.MinValue;
 
            foreach (var volumeProfile in Profiles)
            {
                if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
                {
                    continue;
                }
 
                var x1 = Canvas.GetX(volumeProfile.StartBar);
                var x2 = Canvas.GetX(volumeProfile.EndBar);
 
                var profile = volumeProfile.Cluster;
 
                var maxValues = profile.MaxValues;
 
                var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
 
                var left = x1 - columnWidth2;
                var right = x2 + columnWidth2 - 1;
 
                if (prevRight != int.MinValue)
                {
                    left = prevRight;
                }
 
                prevRight = (int)right;
 
                var dist = Math.Max(right - left, 1);
 
                var max = ProfileProportion > 0 ? ProfileProportion : maxValues.MaxTrades;
 
                var volStep = dist / Math.Max(max, 1);
 
                var colorRects = new List<Tuple<Rect, XBrush>>();
                var colorRects2 = new List<Tuple<Rect, XBrush>>();
                var valueRects = new List<Tuple<Rect, string>>();
 
                var prevX = (int)left;
                var prevY = (int)GetY((profile.High + .5) * step);
 
                var points = new List<Point>
                {
                    new Point(prevX, prevY)
                };
 
                for (var j = profile.High; j >= profile.Low; j--)
                {
                    var item = profile.GetItem(j) ?? new RawClusterItem(j);
 
                    var width = Math.Min(volStep * item.Trades, dist);
 
                    var currX = (int)(left + width);
                    var currY = (int)GetY((j - .5) * step);
 
                    var currHeight = Math.Max(currY - prevY, 1);
 
                    if (currY == prevY && points.Count > 2)
                    {
                        if (currX > prevX)
                        {
                            points[points.Count - 2] = new Point(currX, points[points.Count - 2].Y);
                            points[points.Count - 1] = new Point(currX, points[points.Count - 1].Y);
 
                            prevX = currX;
                        }
                    }
                    else
                    {
                        points.Add(new Point(currX, prevY));
                        points.Add(new Point(currX, currY));
 
                        prevX = currX;
                    }
 
                    prevY = currY;
 
                    var topY = points[points.Count - 2].Y;
 
                    if (ShowMaximum && CheckMaximum(item, maxValues))
                    {
                        colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
                    }
                    else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
                    {
                        colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight),
                            _valueAreaBrush));
                    }
                    else if (EnableFilter && item.Trades >= FilterMin && (FilterMax == 0 || item.Trades <= FilterMax))
                    {
                        if (colorRects.Count > 0)
                        {
                            var lastRect = colorRects[colorRects.Count - 1].Item1;
 
                            if ((int)lastRect.Y == (int)topY)
                            {
                                if (width > lastRect.Width)
                                {
                                    colorRects[colorRects.Count - 1] =
                                        new Tuple<Rect, XBrush>(new Rect(left, topY, width, lastRect.Height), _filterBrush);
                                }
                            }
                            else
                            {
                                colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight),
                                    _filterBrush));
                            }
                        }
                        else
                        {
                            colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight), _filterBrush));
                        }
                    }
 
                    if (ShowValues && height > 7 && item.Trades > 0)
                    {
                        valueRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist, height),
                            symbol.FormatTrades(item.Trades, roundValues, MinimizeValues)));
                    }
                }
 
                points.Add(new Point(left, prevY));
 
                visual.FillRectangle(_backBrush,
                    new Rect(new Point(left, points[0].Y), new Point(right, prevY)));
 
                visual.FillPolygon(_profileBrush, points.ToArray());
 
                foreach (var colorRect in colorRects)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var colorRect in colorRects2)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var valueRect in valueRects)
                {
                    visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
                }
            }
        }
 
        private void RenderDelta(DxVisualQueue visual)
        {
            var startIndex = Canvas.Stop;
            var endIndex = Canvas.Stop + Canvas.Count;
 
            var step = DataProvider.Step;
            var symbol = DataProvider.Symbol;
 
            var minFilter = symbol.CorrectSizeFilter(FilterMin);
            var maxFilter = symbol.CorrectSizeFilter(FilterMax);
 
            var proportion = symbol.CorrectSizeFilter(ProfileProportion);
 
            var height = GetY(0.0) - GetY(step);
 
            var fontSize = Math.Min(height - 2, 18) * 96 / 72;
 
            fontSize = Math.Min(fontSize, Canvas.ChartFont.Size);
 
            var normalFont = new XFont(Canvas.ChartFont.Name, fontSize);
 
            var columnWidth2 = Canvas.ColumnWidth / 2.0;
 
            var roundValues = RoundValues;
 
            var prevRight = int.MinValue;
 
            foreach (var volumeProfile in Profiles)
            {
                if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
                {
                    continue;
                }
 
                var x1 = Canvas.GetX(volumeProfile.StartBar);
                var x2 = Canvas.GetX(volumeProfile.EndBar);
 
                var profile = volumeProfile.Cluster;
 
                var maxValues = profile.MaxValues;
 
                var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
 
                var left = x1 - columnWidth2;
                var right = x2 + columnWidth2 - 1;
 
                if (prevRight != int.MinValue)
                {
                    left = prevRight;
                }
 
                prevRight = (int)right;
 
                var dist = Math.Max(right - left, 1);
 
                var max = ProfileProportion > 0
                    ? proportion
                    : Math.Max(Math.Abs(maxValues.MinDelta), Math.Abs(maxValues.MaxDelta));
 
                var volStep = dist / Math.Max(max, 1);
 
                var colorRects = new List<Tuple<Rect, XBrush>>();
                var colorRectsLeft = new List<Tuple<Rect, XBrush>>();
                var colorRectsRight = new List<Tuple<Rect, XBrush>>();
                var valueLeftRects = new List<Tuple<Rect, string>>();
                var valueRightRects = new List<Tuple<Rect, string>>();
 
                var center = left + dist / 2.0;
 
                // right part
 
                var prevX = (int)center;
                var prevY = (int)GetY((profile.High + .5) * step);
 
                var pointsRight = new List<Point>
                {
                    new Point(prevX, prevY)
                };
 
                for (var j = profile.High; j >= profile.Low; j--)
                {
                    var item = profile.GetItem(j) ?? new RawClusterItem(j);
 
                    var width = item.Delta > 0 ? Math.Min(volStep * Math.Abs(item.Delta), dist) / 2.0 : 0;
 
                    var currX = (int)(center + width);
                    var currY = (int)GetY((j - .5) * step);
 
                    var currHeight = Math.Max(currY - prevY, 1);
 
                    if (currY <= prevY && pointsRight.Count > 2)
                    {
                        if (currX > prevX)
                        {
                            pointsRight[pointsRight.Count - 2] = new Point(currX, pointsRight[pointsRight.Count - 2].Y);
                            pointsRight[pointsRight.Count - 1] = new Point(currX, pointsRight[pointsRight.Count - 1].Y);
 
                            prevX = currX;
                        }
                    }
                    else
                    {
                        pointsRight.Add(new Point(currX, prevY));
                        pointsRight.Add(new Point(currX, currY));
 
                        prevX = currX;
                    }
 
                    prevY = currY;
 
                    var topY = pointsRight[pointsRight.Count - 2].Y;
 
                    if (ShowMaximum && CheckMaximum(item, maxValues))
                    {
                        colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
                    }
                    else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
                    {
                        colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _valueAreaBrush));
                    }
                    else if (EnableFilter)
                    {
                        if (item.Delta > 0 && item.Delta >= minFilter &&
                            (maxFilter == 0 || item.Delta <= maxFilter))
                        {
                            if (colorRectsRight.Count > 0)
                            {
                                var lastRect = colorRectsRight[colorRectsRight.Count - 1].Item1;
 
                                if ((int)lastRect.Y == (int)topY)
                                {
                                    if (width > lastRect.Width)
                                    {
                                        colorRectsRight[colorRectsRight.Count - 1] =
                                            new Tuple<Rect, XBrush>(new Rect(center, topY, width, lastRect.Height), _filterBrush);
                                    }
                                }
                                else
                                {
                                    colorRectsRight.Add(new Tuple<Rect, XBrush>(new Rect(center, topY, width, currHeight),
                                        _filterBrush));
                                }
                            }
                            else
                            {
                                colorRectsRight.Add(new Tuple<Rect, XBrush>(new Rect(center, topY, width, currHeight), _filterBrush));
                            }
                        }
                    }
 
                    if (ShowValues && height > 7 && item.Delta > 0)
                    {
                        valueRightRects.Add(new Tuple<Rect, string>(new Rect(center + 2, topY, dist / 2.0, height),
                            symbol.FormatRawSize(item.Delta, roundValues, MinimizeValues)));
                    }
                }
 
                pointsRight.Add(new Point(center, prevY));
 
                // left part
 
                prevX = (int)center;
                prevY = (int)GetY((profile.High + .5) * step);
 
                var pointsLeft = new List<Point>
                {
                    new Point(prevX, prevY)
                };
 
                for (var j = profile.High; j >= profile.Low; j--)
                {
                    var item = profile.GetItem(j) ?? new RawClusterItem(j);
 
                    var width = item.Delta < 0 ? Math.Min(volStep * Math.Abs(item.Delta), dist) / 2.0 : 0;
 
                    var currX = (int)(center - width);
                    var currY = (int)GetY((j - .5) * step);
 
                    var currHeight = Math.Max(currY - prevY, 1);
 
                    if (currY <= prevY && pointsLeft.Count > 2)
                    {
                        if (currX < prevX)
                        {
                            pointsLeft[pointsLeft.Count - 2] = new Point(currX, pointsLeft[pointsLeft.Count - 2].Y);
                            pointsLeft[pointsLeft.Count - 1] = new Point(currX, pointsLeft[pointsLeft.Count - 1].Y);
 
                            prevX = currX;
                        }
                    }
                    else
                    {
                        pointsLeft.Add(new Point(currX, prevY));
                        pointsLeft.Add(new Point(currX, currY));
 
                        prevX = currX;
                    }
 
                    prevY = currY;
 
                    var topY = pointsLeft[pointsLeft.Count - 2].Y;
 
                    if (ShowMaximum && CheckMaximum(item, maxValues))
                    {
                    }
                    else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
                    {
                    }
                    else if (EnableFilter)
                    {
                        if (item.Delta < 0 && -item.Delta >= minFilter &&
                            (maxFilter == 0 || -item.Delta <= maxFilter))
                        {
                            if (colorRectsLeft.Count > 0)
                            {
                                var lastRect = colorRectsLeft[colorRectsLeft.Count - 1].Item1;
 
                                if ((int)lastRect.Y == (int)topY)
                                {
                                    if (width > lastRect.Width)
                                    {
                                        colorRectsLeft[colorRectsLeft.Count - 1] =
                                            new Tuple<Rect, XBrush>(
                                                new Rect(center - width, topY, width, lastRect.Height),
                                                _filterBrush);
                                    }
                                }
                                else
                                {
                                    colorRectsLeft.Add(new Tuple<Rect, XBrush>(
                                        new Rect(center - width, topY, width, currHeight),
                                        _filterBrush));
                                }
                            }
                            else
                            {
                                colorRectsLeft.Add(
                                    new Tuple<Rect, XBrush>(new Rect(center - width, topY, width, currHeight),
                                        _filterBrush));
                            }
                        }
                    }
 
                    if (ShowValues && height > 7 && item.Delta < 0)
                    {
                        valueLeftRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist / 2.0 - 2, height),
                            symbol.FormatRawSize(item.Delta, roundValues, MinimizeValues)));
                    }
                }
 
                pointsLeft.Add(new Point(center, prevY));
 
                visual.FillRectangle(_backBrush,
                    new Rect(new Point(left, pointsLeft[0].Y), new Point(right, prevY)));
 
                visual.FillPolygon(_profileBrush, pointsLeft.ToArray());
                visual.FillPolygon(_profileBrush, pointsRight.ToArray());
 
                foreach (var colorRect in colorRectsLeft)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var colorRect in colorRectsRight)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var colorRect in colorRects)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var valueRect in valueLeftRects)
                {
                    visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1, XTextAlignment.Right);
                }
 
                foreach (var valueRect in valueRightRects)
                {
                    visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
                }
            }
        }
 
        private void RenderBidAsk(DxVisualQueue visual)
        {
            var startIndex = Canvas.Stop;
            var endIndex = Canvas.Stop + Canvas.Count;
 
            var step = DataProvider.Step;
            var symbol = DataProvider.Symbol;
 
            var minFilter = symbol.CorrectSizeFilter(FilterMin);
            var maxFilter = symbol.CorrectSizeFilter(FilterMax);
 
            var proportion = symbol.CorrectSizeFilter(ProfileProportion);
 
            var height = GetY(0.0) - GetY(step);
 
            var fontSize = Math.Min(height - 2, 18) * 96 / 72;
 
            fontSize = Math.Min(fontSize, Canvas.ChartFont.Size);
 
            var normalFont = new XFont(Canvas.ChartFont.Name, fontSize);
 
            var columnWidth2 = Canvas.ColumnWidth / 2.0;
 
            var roundValues = RoundValues;
 
            var prevRight = int.MinValue;
 
            foreach (var volumeProfile in Profiles)
            {
                if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
                {
                    continue;
                }
 
                var x1 = Canvas.GetX(volumeProfile.StartBar);
                var x2 = Canvas.GetX(volumeProfile.EndBar);
 
                var profile = volumeProfile.Cluster;
 
                var maxValues = profile.MaxValues;
 
                var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
 
                var left = x1 - columnWidth2;
                var right = x2 + columnWidth2 - 1;
 
                if (prevRight != int.MinValue)
                {
                    left = prevRight;
                }
 
                prevRight = (int)right;
 
                var dist = Math.Max(right - left, 1);
 
                var max = ProfileProportion > 0
                    ? proportion
                    : Math.Max(maxValues.MaxBid, maxValues.MaxAsk);
 
                var volStep = dist / Math.Max(max, 1);
 
                var colorRects = new List<Tuple<Rect, XBrush>>();
                var colorRectsLeft = new List<Tuple<Rect, XBrush>>();
                var colorRectsRight = new List<Tuple<Rect, XBrush>>();
                var valueLeftRects = new List<Tuple<Rect, string>>();
                var valueRightRects = new List<Tuple<Rect, string>>();
 
                var center = left + dist / 2.0;
 
                // right part - ask
 
                var prevX = (int)center;
                var prevY = (int)GetY((profile.High + .5) * step);
 
                var pointsRight = new List<Point>
                {
                    new Point(prevX, prevY)
                };
 
                for (var j = profile.High; j >= profile.Low; j--)
                {
                    var item = profile.GetItem(j) ?? new RawClusterItem(j);
 
                    var askWidth = item.Ask > 0 ? (int)(Math.Min(volStep * item.Ask, dist) / 2.0) : 0;
 
                    var currX = (int)(center + askWidth);
                    var currY = (int)GetY((j - .5) * step);
 
                    var currHeight = Math.Max(currY - prevY, 1);
 
                    if (currY <= prevY && pointsRight.Count > 2)
                    {
                        if (currX > prevX)
                        {
                            pointsRight[pointsRight.Count - 2] = new Point(currX, pointsRight[pointsRight.Count - 2].Y);
                            pointsRight[pointsRight.Count - 1] = new Point(currX, pointsRight[pointsRight.Count - 1].Y);
 
                            prevX = currX;
                        }
                    }
                    else
                    {
                        pointsRight.Add(new Point(currX, prevY));
                        pointsRight.Add(new Point(currX, currY));
 
                        prevX = currX;
                    }
 
                    prevY = currY;
 
                    var topY = pointsRight[pointsRight.Count - 2].Y;
 
                    if (ShowMaximum && CheckMaximum(item, maxValues))
                    {
                        colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
                    }
                    else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
                    {
                        colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight),
                            _valueAreaBrush));
                    }
                    else if (EnableFilter)
                    {
                        if (item.Ask >= minFilter && (maxFilter == 0 || item.Ask <= maxFilter))
                        {
                            if (colorRectsRight.Count > 0)
                            {
                                var lastRect = colorRectsRight[colorRectsRight.Count - 1].Item1;
 
                                if ((int)lastRect.Y == (int)topY)
                                {
                                    if (askWidth > lastRect.Width)
                                    {
                                        colorRectsRight[colorRectsRight.Count - 1] =
                                            new Tuple<Rect, XBrush>(new Rect(center, topY, askWidth, lastRect.Height),
                                                _filterBrush);
                                    }
                                }
                                else
                                {
                                    colorRectsRight.Add(
                                        new Tuple<Rect, XBrush>(new Rect(center, topY, askWidth, currHeight),
                                            _filterBrush));
                                }
                            }
                            else
                            {
                                colorRectsRight.Add(new Tuple<Rect, XBrush>(
                                    new Rect(center, topY, askWidth, currHeight),
                                    _filterBrush));
                            }
                        }
                    }
 
                    if (ShowValues && height > 7 && item.Ask > 0)
                    {
                        valueRightRects.Add(new Tuple<Rect, string>(new Rect(center + 2, topY, dist / 2.0, height),
                            symbol.FormatRawSize(item.Ask, roundValues, MinimizeValues)));
                    }
                }
 
                pointsRight.Add(new Point(center, prevY));
 
                // left part - bid
 
                prevX = (int)center;
                prevY = (int)GetY((profile.High + .5) * step);
 
                var pointsLeft = new List<Point>
                {
                    new Point(prevX, prevY)
                };
 
                for (var j = profile.High; j >= profile.Low; j--)
                {
                    var item = profile.GetItem(j) ?? new RawClusterItem(j);
 
                    var bidWidth = (int)(Math.Min(volStep * item.Bid, dist) / 2.0);
 
                    var currX = (int)(center - bidWidth);
                    var currY = (int)GetY((j - .5) * step);
 
                    var currHeight = Math.Max(currY - prevY, 1);
 
                    if (currY <= prevY && pointsLeft.Count > 2)
                    {
                        if (currX < prevX)
                        {
                            pointsLeft[pointsLeft.Count - 2] = new Point(currX, pointsLeft[pointsLeft.Count - 2].Y);
                            pointsLeft[pointsLeft.Count - 1] = new Point(currX, pointsLeft[pointsLeft.Count - 1].Y);
 
                            prevX = currX;
                        }
                    }
                    else
                    {
                        pointsLeft.Add(new Point(currX, prevY));
                        pointsLeft.Add(new Point(currX, currY));
 
                        prevX = currX;
                    }
 
                    prevY = currY;
 
                    var topY = pointsLeft[pointsLeft.Count - 2].Y;
 
                    if (ShowMaximum && CheckMaximum(item, maxValues))
                    {
                    }
                    else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
                    {
                    }
                    else if (EnableFilter)
                    {
                        if (item.Bid >= minFilter && (maxFilter == 0 || item.Bid <= maxFilter))
                        {
                            if (colorRectsLeft.Count > 0)
                            {
                                var lastRect = colorRectsLeft[colorRectsLeft.Count - 1].Item1;
 
                                if ((int)lastRect.Y == (int)topY)
                                {
                                    if (bidWidth > lastRect.Width)
                                    {
                                        colorRectsLeft[colorRectsLeft.Count - 1] =
                                            new Tuple<Rect, XBrush>(
                                                new Rect(center - bidWidth, topY, bidWidth, lastRect.Height),
                                                _filterBrush);
                                    }
                                }
                                else
                                {
                                    colorRectsLeft.Add(new Tuple<Rect, XBrush>(
                                        new Rect(center - bidWidth, topY, bidWidth, currHeight), _filterBrush));
                                }
                            }
                            else
                            {
                                colorRectsLeft.Add(
                                    new Tuple<Rect, XBrush>(new Rect(center - bidWidth, topY, bidWidth, currHeight),
                                        _filterBrush));
                            }
                        }
                    }
 
                    if (ShowValues && height > 7 && item.Bid > 0)
                    {
                        valueLeftRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist / 2.0 - 2, height),
                            symbol.FormatRawSize(item.Bid, roundValues, MinimizeValues)));
                    }
                }
 
                pointsLeft.Add(new Point(center, prevY));
 
                visual.FillRectangle(_backBrush,
                    new Rect(new Point(left, pointsLeft[0].Y), new Point(right, prevY)));
 
                visual.FillPolygon(_profileBrush, pointsLeft.ToArray());
                visual.FillPolygon(_profileBrush, pointsRight.ToArray());
 
                foreach (var colorRect in colorRectsLeft)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var colorRect in colorRectsRight)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var colorRect in colorRects)
                {
                    visual.FillRectangle(colorRect.Item2, colorRect.Item1);
                }
 
                foreach (var valueRect in valueLeftRects)
                {
                    visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1, XTextAlignment.Right);
                }
 
                foreach (var valueRect in valueRightRects)
                {
                    visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
                }
            }
        }
 
        private bool CheckMaximum(IRawClusterItem item, IRawClusterMaxValues maxValues)
        {
            switch (MaximumType)
            {
                case VolumeProfilesMaximumType.Volume:
 
                    return item.Volume == maxValues.MaxVolume;
 
                case VolumeProfilesMaximumType.Trades:
 
                    return item.Trades == maxValues.MaxTrades;
 
                case VolumeProfilesMaximumType.Delta:
 
                    return Math.Abs(item.Delta) ==
                                     Math.Max(Math.Abs(maxValues.MaxDelta), Math.Abs(maxValues.MinDelta));
 
                case VolumeProfilesMaximumType.DeltaPlus:
 
                    return item.Delta > 0 && item.Delta == maxValues.MaxDelta;
 
                case VolumeProfilesMaximumType.DeltaMinus:
 
                    return item.Delta < 0 && item.Delta == maxValues.MinDelta;
 
                case VolumeProfilesMaximumType.Bid:
 
                    return item.Bid == maxValues.MaxBid;
 
                case VolumeProfilesMaximumType.Ask:
 
                    return item.Ask == maxValues.MaxAsk;
            }
 
            return false;
        }
 
        public override void CopyTemplate(IndicatorBase indicator, bool style)
        {
            var i = (VolumeProfilesIndicator)indicator;
 
            PeriodType = i.PeriodType;
            PeriodValue = i.PeriodValue;
 
            ProfileType = i.ProfileType;
            ProfileProportion = i.ProfileProportion;
            ProfileColor = i.ProfileColor;
            ProfileBackColor = i.ProfileBackColor;
 
            ShowValues = i.ShowValues;
            MinimizeValues = i.MinimizeValues;
            ValuesColor = i.ValuesColor;
 
            RoundValuesParam.Copy(i.RoundValuesParam);
 
            MaximumType = i.MaximumType;
            ShowMaximum = i.ShowMaximum;
            MaximumColor = i.MaximumColor;
 
            ShowValueArea = i.ShowValueArea;
            ValueAreaPercent = i.ValueAreaPercent;
            ValueAreaColor = i.ValueAreaColor;
 
            EnableFilter = i.EnableFilter;
            FilterMin = i.FilterMin;
            FilterMax = i.FilterMax;
            FilterColor = i.FilterColor;
 
            base.CopyTemplate(indicator, style);
        }
 
        private class VolumeProfile
        {
            public readonly int StartBar;
            public int EndBar;
            public readonly RawCluster Cluster;
            public bool Completed;
 
            public VolumeProfile(RawCluster cluster, int startBar)
            {
                Cluster = cluster;
                StartBar = startBar;
            }
        }
    }
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://support.tiger.com/razrabotka-dlya-tiger.trade-windows/primery-indikatorov/volume-profiles.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
