Volume Profiles

//------------------------------------------------------------------------------
//
// Indicator 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;
            }
        }
    }
}

Last updated