Cluster Search

//------------------------------------------------------------------------------
//
// Индикатор ClusterSearch. 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.Input;
using System.Windows.Media;
using TigerTrade.Chart.Alerts;
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.Dx;
 
namespace TigerTrade.Chart.Indicators.Custom
{
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "ClusterSearchDataType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum ClusterSearchDataType
    {
        [EnumMember(Value = "Volume"), Description("Volume")]
        Volume,
        [EnumMember(Value = "MaxVolume"), Description("Max Volume")]
        MaxVol,
        [EnumMember(Value = "Trades"), Description("Trades")]
        Trades,
        [EnumMember(Value = "Bid"), Description("Bid")]
        Bid,
        [EnumMember(Value = "Ask"), Description("Ask")]
        Ask,
        [EnumMember(Value = "Delta"), Description("Delta")]
        Delta,
        [EnumMember(Value = "DeltaPlus"), Description("Delta+")]
        DeltaPlus,
        [EnumMember(Value = "DeltaMinus"), Description("Delta-")]
        DeltaMinus
    }
 
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "ClusterSearchObjectType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum ClusterSearchObjectType
    {
        [EnumMember(Value = "Rectangle"), Description("Квадрат")]
        Rectangle,
        [EnumMember(Value = "Triangle"), Description("Треугольник")]
        Triangle,
        [EnumMember(Value = "Diamond"), Description("Ромб")]
        Diamond,
        [EnumMember(Value = "Circle"), Description("Круг")]
        Circle,
        [EnumMember(Value = "SelectionOnly"), Description("Только выделение")]
        SelectionOnly
    }
 
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "ClusterSearchBarDirection", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum ClusterSearchBarDirection
    {
        [EnumMember(Value = "Any"), Description("Любое")]
        Any,
        [EnumMember(Value = "Up"), Description("Рост")]
        Up,
        [EnumMember(Value = "Down"), Description("Падение")]
        Down
    }
 
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "ClusterSearchPriceLocation", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum ClusterSearchPriceLocation
    {
        [EnumMember(Value = "Any"), Description("Любое")]
        Any,
        [EnumMember(Value = "High"), Description("High")]
        High,
        [EnumMember(Value = "Low"), Description("Low")]
        Low,
        [EnumMember(Value = "HighLow"), Description("High или Low")]
        HighLow,
        [EnumMember(Value = "Body"), Description("Тело")]
        Body,
        [EnumMember(Value = "Wick"), Description("Тень")]
        Wick,
        [EnumMember(Value = "UpperWick"), Description("Верхняя тень")]
        UpperWick,
        [EnumMember(Value = "LowerWick"), Description("Нижняя тень")]
        LowerWick
    }
 
    [TypeConverter(typeof(EnumDescriptionTypeConverter))]
    [DataContract(Name = "ClusterSearchPriceRangeDirection", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    public enum ClusterSearchPriceRangeDirection
    {
        [EnumMember(Value = "All"), Description("Оба")]
        All,
        [EnumMember(Value = "Downward"), Description("Сверху вниз")]
        Downward,
        [EnumMember(Value = "Upward"), Description("Снизу вверх")]
        Upward
    }
 
    [DataContract(Name = "ClusterSearchIndicator", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
    [Indicator("X_ClusterSearch", "*ClusterSearch", true, Type = typeof(ClusterSearchIndicator))]
    internal sealed class ClusterSearchIndicator : IndicatorBase, IContainsSelection
    {
        private class ClusterSearchItem
        {
            public DateTime Time { get; }
            public long PriceHigh { get; }
            public long PriceLow { get; }
            public long Value { get; }
 
            public ClusterSearchItem(DateTime time, long priceHigh, long priceLow, long value)
            {
                Time = time;
                PriceHigh = priceHigh;
                PriceLow = priceLow;
                Value = value;
            }
        }
 
        private class ClusterSearchRect
        {
            private Rect _rect;
 
            private readonly ClusterSearchItem _item;
 
            public ClusterSearchRect(Rect rect, ClusterSearchItem item)
            {
                _rect = rect;
                _item = item;
            }
 
            public bool Contains(Point p)
            {
                return _rect.Contains(p);
            }
 
            public string GetLabel(ClusterSearchDataType type, IChartDataProvider dp, string name)
            {
                var price = dp.Symbol.FormatRawPrice((_item.PriceHigh + _item.PriceLow) / 2, true);
 
                var val = type == ClusterSearchDataType.Trades
                    ? dp.Symbol.FormatTrades(_item.Value)
                    : dp.Symbol.FormatRawSizeShort(_item.Value);
 
                var time = dp.Symbol.FormatTime(_item.Time, "HH:mm:ss");
 
                return $"{name} {time} {price} x {val}";
            }
        }
 
        private class ClusterSearchBar
        {
            public List<ClusterSearchItem> Items { get; }
            public HashSet<long> Selections { get; }
            public HashSet<long> SingleSelection { get; }
 
            private readonly HashSet<long> _signals;
 
            public ClusterSearchBar()
            {
                Items = new List<ClusterSearchItem>();
                Selections = new HashSet<long>();
                SingleSelection = new HashSet<long>();
 
                _signals = new HashSet<long>();
            }
 
            public void Add(ClusterSearchItem item)
            {
                Items.Add(item);
            }
 
            public bool CheckSignal(long price)
            {
                if (_signals.Contains(price))
                {
                    return false;
                }
 
                _signals.Add(price);
 
                return true;
            }
 
            public void Clear()
            {
                Items.Clear();
                Selections.Clear();
                SingleSelection.Clear();
            }
 
            public void Update()
            {
                ClusterSearchItem maxItem = null;
 
                foreach (var item in Items)
                {
                    if (maxItem == null || maxItem.Value < item.Value)
                    {
                        maxItem = item;
                    }
 
                    for (var price = item.PriceLow; price <= item.PriceHigh; price++)
                    {
                        if (!Selections.Contains(price))
                        {
                            Selections.Add(price);
                        }
                    }
                }
 
                if (maxItem == null)
                {
                    return;
                }
 
                for (var price = maxItem.PriceLow; price <= maxItem.PriceHigh; price++)
                {
                    if (!SingleSelection.Contains(price))
                    {
                        SingleSelection.Add(price);
                    }
                }
            }
        }
 
        private ClusterSearchDataType _type;
 
        [DataMember(Name = "Type"), DefaultValue(ClusterSearchDataType.Volume)]
        [Category("Параметры"), DisplayName("Тип")]
        public ClusterSearchDataType Type
        {
            get => _type;
            set
            {
                if (value == _type)
                {
                    return;
                }
 
                _type = value;
 
                Clear();
 
                OnPropertyChanged();
                OnPropertyChanged(nameof(Title));
            }
        }
 
        private IndicatorIntParam _minimumParam;
 
        [DataMember(Name = "MinimumParam")]
        public IndicatorIntParam MinimumParam
        {
            get => _minimumParam ?? (_minimumParam = new IndicatorIntParam(1000));
            set => _minimumParam = value;
        }
 
        [DefaultValue(1000)]
        [Category("Параметры"), DisplayName("Минимум")]
        public int Minimum
        {
            get => MinimumParam.Get(SettingsLongKey);
            set
            {
                if (!MinimumParam.Set(SettingsLongKey, value, 0))
                {
                    return;
                }
 
                Clear();
 
                OnPropertyChanged();
                OnPropertyChanged(nameof(Title));
            }
        }
 
        private IndicatorNullIntParam _maximumParam;
 
        [DataMember(Name = "MaximumParam")]
        public IndicatorNullIntParam MaximumParam
        {
            get => _maximumParam ?? (_maximumParam = new IndicatorNullIntParam(null));
            set => _maximumParam = value;
        }
 
        [DefaultValue(null)]
        [Category("Параметры"), DisplayName("Максимум")]
        public int? Maximum
        {
            get => MaximumParam.Get(SettingsLongKey);
            set
            {
                if (!MaximumParam.Set(SettingsLongKey, value, 0))
                {
                    return;
                }
 
                Clear();
 
                OnPropertyChanged();
                OnPropertyChanged(nameof(Title));
            }
        }
 
        private ChartAlertSettings _alert;
 
        [DataMember(Name = "Alert"), Browsable(true)]
        [Category("Параметры"), DisplayName("Оповещение")]
        public ChartAlertSettings Alert
        {
            get => _alert ?? (_alert = new ChartAlertSettings());
            set
            {
                if (Equals(value, _alert))
                {
                    return;
                }
 
                _alert = value;
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _selectionBrush;
 
        private XColor _selectionColor;
 
        [DataMember(Name = "SelectionColor")]
        [Category("Стиль"), DisplayName("Цвет выделения")]
        public XColor SelectionColor
        {
            get => _selectionColor;
            set
            {
                if (value == _selectionColor)
                {
                    return;
                }
 
                _selectionColor = value;
 
                _selectionBrush = new XBrush(_selectionColor);
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _objectBackBrush;
 
        private XColor _objectBackColor;
 
        [DataMember(Name = "ObjectColor")]
        [Category("Стиль"), DisplayName("Цвет фона объекта")]
        public XColor ObjectBackColor
        {
            get => _objectBackColor;
            set
            {
                if (value == _objectBackColor)
                {
                    return;
                }
 
                _objectBackColor = value;
 
                _objectBackBrush = new XBrush(_objectBackColor);
 
                OnPropertyChanged();
            }
        }
 
        private XBrush _objectBorderBrush;
 
        private XPen _objectBorderPen;
 
        private XColor _objectBorderColor;
 
        [DataMember(Name = "ObjectBorderColor")]
        [Category("Стиль"), DisplayName("Цвет границы объекта")]
        public XColor ObjectBorderColor
        {
            get => _objectBorderColor;
            set
            {
                if (value == _objectBorderColor)
                {
                    return;
                }
 
                _objectBorderColor = value;
 
                _objectBorderBrush = new XBrush(_objectBorderColor);
                _objectBorderPen = new XPen(_objectBorderBrush, 1);
 
                OnPropertyChanged();
            }
        }
 
        private ClusterSearchObjectType _objectType;
 
        [DataMember(Name = "ObjectType")]
        [Category("Стиль"), DisplayName("Тип фигуры")]
        public ClusterSearchObjectType ObjectType
        {
            get => _objectType;
            set
            {
                if (value == _objectType)
                {
                    return;
                }
 
                _objectType = value;
 
                OnPropertyChanged();
            }
        }
 
        private int _objectMinSize;
 
        [DataMember(Name = "ObjectMinSize")]
        [Category("Стиль"), DisplayName("Мин. размер")]
        public int ObjectMinSize
        {
            get => _objectMinSize;
            set
            {
                value = Math.Max(10, Math.Min(value, 200));
 
                if (value == _objectMinSize)
                {
                    return;
                }
 
                _objectMinSize = value;
 
                OnPropertyChanged();
            }
        }
 
        private int _objectMaxSize;
 
        [DataMember(Name = "ObjectMaxSize")]
        [Category("Стиль"), DisplayName("Макс. размер")]
        public int ObjectMaxSize
        {
            get => _objectMaxSize;
            set
            {
                value = Math.Min(200, Math.Max(value, 10));
 
                if (value == _objectMaxSize)
                {
                    return;
                }
 
                _objectMaxSize = value;
 
                OnPropertyChanged();
            }
        }
 
        private int _barsRange;
 
        [DataMember(Name = "BarsRange"), DefaultValue(1)]
        [Category("Фильтры"), DisplayName("Объедин. баров")]
        public int BarsRange
        {
            get => _barsRange;
            set
            {
                value = Math.Max(1, value);
 
                if (value == _barsRange)
                {
                    return;
                }
 
                _barsRange = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int _priceRange;
 
        [DataMember(Name = "PriceRange"), DefaultValue(1)]
        [Category("Фильтры"), DisplayName("Объедин. цен")]
        public int PriceRange
        {
            get => _priceRange;
            set
            {
                value = Math.Max(1, value);
 
                if (value == _priceRange)
                {
                    return;
                }
 
                _priceRange = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private ClusterSearchPriceRangeDirection _priceRangeDir;
 
        [DataMember(Name = "PriceRangeDir"), DefaultValue(ClusterSearchPriceRangeDirection.All)]
        [Category("Фильтры"), DisplayName("Напр. объедин. цен")]
        public ClusterSearchPriceRangeDirection PriceRangeDir
        {
            get => _priceRangeDir;
            set
            {
                if (value == _priceRangeDir)
                {
                    return;
                }
 
                _priceRangeDir = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private IndicatorNullIntParam _minValueParam;
 
        [DataMember(Name = "MinValueParam")]
        public IndicatorNullIntParam MinValueParam
        {
            get => _minValueParam ?? (_minValueParam = new IndicatorNullIntParam());
            set => _minValueParam = value;
        }
 
        [DefaultValue(null)]
        [Category("Фильтры"), DisplayName("Мин. значение")]
        public int? MinValue
        {
            get => MinValueParam.Get(SettingsLongKey);
            set
            {
                if (!MinValueParam.Set(SettingsLongKey, value, 0))
                {
                    return;
                }
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int? _minDelta;
 
        [DataMember(Name = "MinDelta"), DefaultValue(null)]
        [Category("Фильтры"), DisplayName("Мин. дельта")]
        public int? MinDelta
        {
            get => _minDelta;
            set
            {
                if (value == _minDelta)
                {
                    return;
                }
 
                _minDelta = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int? _bidAskImbalance;
 
        [DataMember(Name = "BidAskImbalance"), DefaultValue(null)]
        [Category("Фильтры"), DisplayName("Перевес BidAsk")]
        public int? BidAskImbalance
        {
            get => _bidAskImbalance;
            set
            {
                if (value == _bidAskImbalance)
                {
                    return;
                }
 
                _bidAskImbalance = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int? _rangeFromHigh;
 
        [DataMember(Name = "RangeFromHigh"), DefaultValue(null)]
        [Category("Фильтры"), DisplayName("Диапазон от High")]
        public int? RangeFromHigh
        {
            get => _rangeFromHigh;
            set
            {
                if (value.HasValue && value.Value < 1 && _rangeFromHigh.HasValue)
                {
                    value = null;
                }
                else if (value.HasValue && value.Value < 1 && !_rangeFromHigh.HasValue)
                {
                    value = 1;
                }
 
                if (value == _rangeFromHigh)
                {
                    return;
                }
 
                _rangeFromHigh = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int? _rangeFromLow;
 
        [DataMember(Name = "RangeFromLow"), DefaultValue(null)]
        [Category("Фильтры"), DisplayName("Диапазон от Low")]
        public int? RangeFromLow
        {
            get => _rangeFromLow;
            set
            {
                if (value.HasValue && value.Value < 0)
                {
                    value = null;
                }
 
                if (value == _rangeFromLow)
                {
                    return;
                }
 
                _rangeFromLow = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int? _minAvgTrade;
 
        [DataMember(Name = "MinAvgTrade"), DefaultValue(null)]
        [Category("Фильтры"), DisplayName("Мин. средний трейд")]
        public int? MinAvgTrade
        {
            get => _minAvgTrade;
            set
            {
                if (value.HasValue && value.Value < 0)
                {
                    value = null;
                }
 
                if (value == _minAvgTrade)
                {
                    return;
                }
 
                _minAvgTrade = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private int? _maxAvgTrade;
 
        [DataMember(Name = "MaxAvgTrade"), DefaultValue(null)]
        [Category("Фильтры"), DisplayName("Макс. средний трейд")]
        public int? MaxAvgTrade
        {
            get => _maxAvgTrade;
            set
            {
                if (value.HasValue && value.Value < 0)
                {
                    value = null;
                }
 
                if (value == _maxAvgTrade)
                {
                    return;
                }
 
                _maxAvgTrade = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private ClusterSearchBarDirection _barDirection;
 
        [DataMember(Name = "BarDirection"), DefaultValue(ClusterSearchBarDirection.Any)]
        [Category("Фильтры"), DisplayName("Направление бара")]
        public ClusterSearchBarDirection BarDirection
        {
            get => _barDirection;
            set
            {
                if (value == _barDirection)
                {
                    return;
                }
 
                _barDirection = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private ClusterSearchPriceLocation _priceLocation;
 
        [DataMember(Name = "PriceLocation"), DefaultValue(ClusterSearchPriceLocation.Any)]
        [Category("Фильтры"), DisplayName("Расположение цены")]
        public ClusterSearchPriceLocation PriceLocation
        {
            get => _priceLocation;
            set
            {
                if (value == _priceLocation)
                {
                    return;
                }
 
                _priceLocation = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private bool _singleSelection;
 
        [DataMember(Name = "SingleSelection"), DefaultValue(false)]
        [Category("Фильтры"), DisplayName("Одно выдел. в баре")]
        public bool SingleSelection
        {
            get => _singleSelection;
            set
            {
                if (value == _singleSelection)
                {
                    return;
                }
 
                _singleSelection = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private bool _useTimeFilter;
 
        [DataMember(Name = "UseTimeFilter")]
        [Category("Фильтр по времени"), DisplayName("Включить фильтр")]
        public bool UseTimeFilter
        {
            get => _useTimeFilter;
            set
            {
                if (value == _useTimeFilter)
                {
                    return;
                }
 
                _useTimeFilter = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private TimeSpan _startTime;
 
        [DataMember(Name = "StartTime")]
        [Category("Фильтр по времени"), DisplayName("Начальное время")]
        public TimeSpan StartTime
        {
            get => _startTime;
            set
            {
                if (value == _startTime)
                {
                    return;
                }
 
                _startTime = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        private TimeSpan _endTime;
 
        [DataMember(Name = "EndTime")]
        [Category("Фильтр по времени"), DisplayName("Конечное время")]
        public TimeSpan EndTime
        {
            get => _endTime;
            set
            {
                if (value == _endTime)
                {
                    return;
                }
 
                _endTime = value;
 
                Clear();
 
                OnPropertyChanged();
            }
        }
 
        [Browsable(false)]
        public override bool ShowIndicatorValues => false;
 
        [Browsable(false)]
        public override bool ShowIndicatorLabels => false;
 
        [Browsable(false)]
        public override IndicatorCalculation Calculation => IndicatorCalculation.OnEachTick;
 
        private long _minValue;
        private long _maxValue;
 
        private int _lastFullID;
 
        private Dictionary<int, ClusterSearchBar> _bars;
        private Dictionary<int, ClusterSearchBar> Bars => _bars ?? (_bars = new Dictionary<int, ClusterSearchBar>());
 
        private List<ClusterSearchRect> _rects;
     
        public ClusterSearchIndicator()
        {
            Type = ClusterSearchDataType.Volume;
 
            SelectionColor = Color.FromArgb(255, 178, 34, 34);
            ObjectBackColor = Color.FromArgb(127, 30, 144, 255);
            ObjectBorderColor = Color.FromArgb(255, 30, 144, 255);
            ObjectType = ClusterSearchObjectType.Diamond;
            ObjectMinSize = 20;
            ObjectMaxSize = 80;
 
            BarsRange = 1;
            PriceRange = 1;
            PriceRangeDir = ClusterSearchPriceRangeDirection.All;
            MinDelta = null;
            BidAskImbalance = null;
            RangeFromHigh = null;
            RangeFromLow = null;
            MaxAvgTrade = null;
            MinAvgTrade = null;
            BarDirection = ClusterSearchBarDirection.Any;
            PriceLocation = ClusterSearchPriceLocation.Any;
            SingleSelection = false;
 
            UseTimeFilter = false;
            StartTime = TimeSpan.Zero;
            EndTime = TimeSpan.Zero;
        }
 
        private void Clear()
        {
            _maxValue = 0;
            _minValue = 0;
 
            _lastFullID = 0;
 
            Bars.Clear();
        }
 
        private bool GetValue(IRawCluster cluster, IRawClusterItem item, long? minFilter, ref long value, ref long bid, ref long ask)
        {
            var itemValue = 0L;
 
            switch (Type)
            {
                case ClusterSearchDataType.Volume:
 
                    itemValue = item.Volume;
 
                    break;
 
                case ClusterSearchDataType.Trades:
 
                    itemValue = item.Trades;
 
                    break;
 
                case ClusterSearchDataType.Bid:
 
                    itemValue = item.Bid;
 
                    break;
 
                case ClusterSearchDataType.Ask:
 
                    itemValue = item.Ask;
 
                    break;
 
                case ClusterSearchDataType.Delta:
                case ClusterSearchDataType.DeltaPlus:
                case ClusterSearchDataType.DeltaMinus:
 
                    itemValue = item.Delta;
 
                    break;
 
                case ClusterSearchDataType.MaxVol:
 
                    var maxValues = cluster.MaxValues;
 
                    itemValue = item.Price == maxValues.Poc ? maxValues.MaxVolume : 0;
 
                    break;
            }
 
            if (minFilter.HasValue && minFilter.Value > Math.Abs(itemValue))
            {
                return false;
            }
 
            value += itemValue;
            bid += item.Bid;
            ask += item.Ask;
 
            return true;
        }
 
        private bool CheckMinMax(long value, long min, long? max)
        {
            switch (Type)
            {
                case ClusterSearchDataType.DeltaPlus:
 
                    if (value < 0)
                    {
                        return false;
                    }
 
                    break;
 
                case ClusterSearchDataType.DeltaMinus:
 
                    if (value > 0)
                    {
                        return false;
                    }
 
                    break;
            }
 
            if (Math.Abs(value) < min)
            {
                return false;
            }
 
            if (max.HasValue && Math.Abs(value) > max.Value)
            {
                return false;
            }
 
            return true;
        }
 
        protected override void Execute()
        {
            if (ClearData)
            {
                Clear();
            }
 
            var symbol = DataProvider.Symbol;
 
            var min = Type == ClusterSearchDataType.Trades ? Minimum : symbol.CorrectSizeFilter(Minimum);
            var max = Type == ClusterSearchDataType.Trades ? Maximum : symbol.CorrectSizeFilter(Maximum);
            var minValue = Type == ClusterSearchDataType.Trades ? MinValue : symbol.CorrectSizeFilter(MinValue);
            var minDelta = symbol.CorrectSizeFilter(MinDelta);
            var bidAskImb = symbol.CorrectSizeFilter(BidAskImbalance);
 
            var maxAvgTrade = symbol.CorrectSizeFilter(MaxAvgTrade);
            var minAvgTrade = symbol.CorrectSizeFilter(MinAvgTrade);
 
            for (var i = _lastFullID; i < DataProvider.Count; i++)
            {
                var cluster = DataProvider.GetRawCluster(i);
 
                if (!Bars.ContainsKey(i))
                {
                    Bars.Add(i, new ClusterSearchBar());
                }
 
                var currBar = Bars[i];
 
                currBar.Clear();
 
                if (UseTimeFilter)
                {
                    var openTime = symbol.ConvertTimeToLocal(cluster.OpenTime);
 
                    if (StartTime <= EndTime)
                    {
                        if (openTime < cluster.OpenTime.Date.Add(StartTime) ||
                            openTime > cluster.OpenTime.Date.Add(EndTime))
                        {
                            continue;
                        }
                    }
                    else
                    {
                        if (openTime < cluster.OpenTime.Date.Add(StartTime) &&
                            openTime > cluster.OpenTime.Date.Add(EndTime))
                        {
                            continue;
                        }
                    }
                }
 
                switch (BarDirection)
                {
                    case ClusterSearchBarDirection.Up:
 
                        if (cluster.Open > cluster.Close)
                        {
                            continue;
                        }
 
                        break;
 
                    case ClusterSearchBarDirection.Down:
 
                        if (cluster.Open < cluster.Close)
                        {
                            continue;
                        }
 
                        break;
                }
 
                for (var price = cluster.High; price >= cluster.Low; price--)
                {
                    var item = cluster.GetItem(price);
 
                    if (item == null)
                    {
                        continue;
                    }
 
                    switch (PriceLocation)
                    {
                        case ClusterSearchPriceLocation.High:
 
                            if (price != cluster.High)
                            {
                                continue;
                            }
 
                            break;
 
                        case ClusterSearchPriceLocation.Low:
 
                            if (price != cluster.Low)
                            {
                                continue;
                            }
 
                            break;
 
                        case ClusterSearchPriceLocation.HighLow:
 
                            if (price != cluster.High && price != cluster.Low)
                            {
                                continue;
                            }
 
                            break;
 
                        case ClusterSearchPriceLocation.Body:
 
                            if (price > Math.Max(cluster.Open, cluster.Close) ||
                                price < Math.Min(cluster.Open, cluster.Close))
                            {
                                continue;
                            }
 
                            break;
 
                        case ClusterSearchPriceLocation.Wick:
 
                            if (price <= Math.Max(cluster.Open, cluster.Close) &&
                                price >= Math.Min(cluster.Open, cluster.Close))
                            {
                                continue;
                            }
 
                            break;
 
                        case ClusterSearchPriceLocation.UpperWick:
 
                            if (price <= Math.Max(cluster.Open, cluster.Close))
                            {
                                continue;
                            }
 
                            break;
 
                        case ClusterSearchPriceLocation.LowerWick:
 
                            if (price >= Math.Min(cluster.Open, cluster.Close))
                            {
                                continue;
                            }
 
                            break;
                    }
 
                    if (RangeFromHigh.HasValue && item.Price < cluster.High - RangeFromHigh.Value)
                    {
                        continue;
                    }
 
                    if (RangeFromLow.HasValue && item.Price > cluster.Low + RangeFromLow.Value)
                    {
                        continue;
                    }
 
                    var avgTrade = item.Volume / (double)item.Trades;
 
                    if (minAvgTrade.HasValue && avgTrade < minAvgTrade.Value)
                    {
                        continue;
                    }
 
                    if (maxAvgTrade.HasValue && avgTrade > maxAvgTrade)
                    {
                        continue;
                    }
 
                    var result = false;
 
                    var totalValue = 0L;
                    var priceHigh = 0L;
                    var priceLow = 0L;
 
                    if (PriceRangeDir == ClusterSearchPriceRangeDirection.Downward ||
                        PriceRangeDir == ClusterSearchPriceRangeDirection.All)
                    {
                        var currentResult = true;
 
                        var currentValue = 0L;
                        var currentBid = 0L;
                        var currentAsk = 0L;
 
                        var results = false;
 
                        var price2 = price;
 
                        for (var j = i - BarsRange + 1; j <= i; j++)
                        {
                            var extCluster = DataProvider.GetRawCluster(j);
 
                            if (extCluster == null)
                            {
                                continue;
                            }
 
                            for (var p = price - PriceRange + 1; p <= price; p++)
                            {
                                var extItem = extCluster.GetItem(p);
 
                                if (extItem == null)
                                {
                                    continue;
                                }
 
                                if (GetValue(cluster, extItem, minValue, ref currentValue, ref currentBid,
                                    ref currentAsk))
                                {
                                    price2 = Math.Min(price2, p);
 
                                    results = true;
                                }
                            }
                        }
 
                        if (!results || !CheckMinMax(currentValue, min, max))
                        {
                            currentResult = false;
                        }
 
                        if (currentResult && minDelta.HasValue)
                        {
                            var currentDelta = currentAsk - currentBid;
 
                            if ((minDelta.Value > 0 && currentDelta < minDelta.Value ||
                                 minDelta.Value < 0 && currentDelta > minDelta.Value))
                            {
                                currentResult = false;
                            }
                        }
 
                        if (currentResult && bidAskImb.HasValue)
                        {
                            var ratio = bidAskImb.Value / 100.0;
 
                            var skip = true;
 
                            if (ratio > 0 && currentAsk > currentBid * ratio)
                            {
                                skip = false;
                            }
                            else if (ratio < 0 && currentBid > currentAsk * -ratio)
                            {
                                skip = false;
                            }
 
                            if (skip)
                            {
                                currentResult = false;
                            }
                        }
 
                        if (currentResult)
                        {
                            totalValue = currentValue;
                            priceHigh = Math.Max(price, price2);
                            priceLow = Math.Min(price, price2);
 
                            result = true;
                        }
                    }
 
                    if (!result && (PriceRangeDir == ClusterSearchPriceRangeDirection.Upward ||
                                    PriceRangeDir == ClusterSearchPriceRangeDirection.All))
                    {
                        var currentResult = true;
 
                        var currentValue = 0L;
                        var currentBid = 0L;
                        var currentAsk = 0L;
 
                        var results = false;
 
                        var price2 = price;
 
                        for (var j = i - BarsRange + 1; j <= i; j++)
                        {
                            var extCluster = DataProvider.GetRawCluster(j);
 
                            if (extCluster == null)
                            {
                                continue;
                            }
 
                            for (var p = price + PriceRange - 1; p >= price; p--)
                            {
                                var extItem = extCluster.GetItem(p);
 
                                if (extItem == null)
                                {
                                    continue;
                                }
 
                                if (GetValue(cluster, extItem, minValue, ref currentValue, ref currentBid,
                                    ref currentAsk))
                                {
                                    price2 = Math.Max(price2, p);
 
                                    results = true;
                                }
                            }
                        }
 
                        if (!results || !CheckMinMax(currentValue, min, max))
                        {
                            currentResult = false;
                        }
 
                        if (currentResult && minDelta.HasValue)
                        {
                            var currentDelta = currentAsk - currentBid;
 
                            if ((minDelta.Value > 0 && currentDelta < minDelta.Value ||
                                 minDelta.Value < 0 && currentDelta > minDelta.Value))
                            {
                                currentResult = false;
                            }
                        }
 
                        if (currentResult && bidAskImb.HasValue)
                        {
                            var ratio = bidAskImb.Value / 100.0;
 
                            var skip = true;
 
                            if (ratio > 0 && currentAsk > currentBid * ratio)
                            {
                                skip = false;
                            }
                            else if (ratio < 0 && currentBid > currentAsk * -ratio)
                            {
                                skip = false;
                            }
 
                            if (skip)
                            {
                                currentResult = false;
                            }
                        }
 
                        if (currentResult)
                        {
                            totalValue = currentValue;
                            priceHigh = Math.Max(price, price2);
                            priceLow = Math.Min(price, price2);
 
                            result = true;
                        }
                    }
 
                    if (!result)
                    {
                        continue;
                    }
 
                    currBar.Add(new ClusterSearchItem(cluster.Time, priceHigh, priceLow, totalValue));
 
                    if (Alert.IsActive && i == DataProvider.Count - 1 &&
                        currBar.CheckSignal(price))
                    {
                        AddAlert(Alert, GetAlertText(price, totalValue));
                    }
 
                    var absValue = Math.Abs(totalValue);
 
                    _maxValue = _maxValue == 0 ? absValue : Math.Max(_maxValue, absValue);
                    _minValue = _minValue == 0 ? absValue : Math.Min(_minValue, absValue);
                }
 
                currBar.Update();
            }
 
            _lastFullID = Math.Max(DataProvider.Count - 2, 0);
        }
 
        private string GetAlertText(long price, long value)
        {
            var text = "Value";
 
            switch (Type)
            {
                case ClusterSearchDataType.Volume:
 
                    text = "Volume";
 
                    break;
 
                case ClusterSearchDataType.MaxVol:
 
                    text = "MaxVol";
 
                    break;
 
                case ClusterSearchDataType.Trades:
 
                    text = "Trades";
 
                    break;
 
                case ClusterSearchDataType.Bid:
 
                    text = "Bid";
 
                    break;
 
                case ClusterSearchDataType.Ask:
 
                    text = "Ask";
 
                    break;
 
                case ClusterSearchDataType.Delta:
 
                    text = "Delta";
 
                    break;
 
                case ClusterSearchDataType.DeltaPlus:
 
                    text = "Delta+";
 
                    break;
 
                case ClusterSearchDataType.DeltaMinus:
 
                    text = "Delta-";
 
                    break;
            }
 
            var val = Type == ClusterSearchDataType.Trades
                ? DataProvider.Symbol.FormatTrades(value)
                : DataProvider.Symbol.FormatRawSizeShort(value);
 
            return $"ClusterSearch: {text}: {val}, Price: {DataProvider.Symbol.FormatRawPrice(price, true)}.";
        }
 
        public override void Render(DxVisualQueue visual)
        {
            base.Render(visual);
 
            if (_rects == null)
            {
                _rects = new List<ClusterSearchRect>();
            }
            else
            {
                _rects.Clear();
            }
 
            var minSize = Math.Max(10, Math.Min(200, ObjectMinSize));
            var maxSize = Math.Min(200, Math.Max(ObjectMaxSize, 10));
 
            if (maxSize < minSize)
            {
                maxSize = minSize;
            }
 
            var baseWidth = (maxSize - minSize) / 9.0;
 
            var range = _maxValue - _minValue;
 
            for (var c = 0; c < Canvas.Count; c++)
            {
                var index = Canvas.GetIndex(c);
 
                if (!Bars.ContainsKey(index))
                {
                    continue;
                }
 
                var items = Bars[index].Items;
 
                for (var i = 0; i < items.Count; i++)
                {
                    var searchItem = items[i];
 
                    if (SingleSelection)
                    {
                        var time = searchItem.Time;
 
                        for (var j = i + 1; j < items.Count; j++)
                        {
                            var nextItem = items[j];
 
                            if (nextItem.Time == time)
                            {
                                if (Math.Abs(nextItem.Value) > Math.Abs(searchItem.Value))
                                {
                                    searchItem = nextItem;
                                }
 
                                i = j;
                            }
                            else
                            {
                                break;
                            }
                        }
                    }
 
                    var step = DataProvider.Step;
 
                    var x = (int)GetX(Canvas.DateToIndex(searchItem.Time, -1));
                    var y1 = (int)GetY((searchItem.PriceHigh + .5) * step);
                    var y2 = (int)GetY((searchItem.PriceLow - .5) * step);
                    var y = (int)((y1 + y2) / 2.0);
 
                    var sizeStep = 9.0;
 
                    if (Math.Abs(searchItem.Value) != _maxValue && range > 0)
                    {
                        sizeStep = (Math.Abs(searchItem.Value) - _minValue) / (range / 9.0);
                    }
 
                    var width = (int)((minSize + baseWidth * sizeStep) / 2.0);
 
                    _rects.Add(
                        new ClusterSearchRect(
                            new Rect(new Point(x - width, y - width), new Point(x + width, y + width)),
                            searchItem));
 
                    switch (ObjectType)
                    {
                        case ClusterSearchObjectType.Rectangle:
                            {
                                var rect = new Rect(new Point(x - width, y - width), new Point(x + width, y + width));
 
                                visual.FillRectangle(_objectBackBrush, rect);
                                visual.DrawRectangle(_objectBorderPen, rect);
 
                                break;
                            }
 
                        case ClusterSearchObjectType.Triangle:
                            {
                                var points = new Point[3];
 
                                points[0] = new Point(x, y - width);
                                points[1] = new Point(x + width, y + width);
                                points[2] = new Point(x - width, y + width);
 
                                visual.FillPolygon(_objectBackBrush, points);
                                visual.DrawPolygon(_objectBorderPen, points);
 
                                break;
                            }
 
                        case ClusterSearchObjectType.Diamond:
                            {
                                var points = new Point[4];
 
                                points[0] = new Point(x - width, y);
                                points[1] = new Point(x, y - width);
                                points[2] = new Point(x + width, y);
                                points[3] = new Point(x, y + width);
 
                                visual.FillPolygon(_objectBackBrush, points);
                                visual.DrawPolygon(_objectBorderPen, points);
 
                                break;
                            }
                        case ClusterSearchObjectType.Circle:
                            {
                                visual.FillEllipse(_objectBackBrush, new Point(x, y), width, width);
                                visual.DrawEllipse(_objectBorderPen, new Point(x, y), width, width);
 
                                break;
                            }
                    }
 
                    if (Canvas.StockType == ChartStockType.Clusters)
                    {
                        continue;
                    }
 
                    var selectionWidth = (int)Math.Max(Canvas.ColumnWidth / 2.0 - 1, 2.0);
 
                    if (Type == ClusterSearchDataType.Bid)
                    {
                        visual.FillRectangle(_selectionBrush,
                            new Rect(new Point(x - selectionWidth, y1), new Point(x + 1, y2)));
                    }
                    else if (Type == ClusterSearchDataType.Ask)
                    {
                        visual.FillRectangle(_selectionBrush,
                            new Rect(new Point(x, y1), new Point(x + selectionWidth + 1, y2)));
                    }
                    else
                    {
                        visual.FillRectangle(_selectionBrush,
                            new Rect(new Point(x - selectionWidth, y1), new Point(x + selectionWidth + 1, y2)));
 
                    }
                }
            }
        }
 
        public override void RenderCursor(DxVisualQueue visual, int cursorPos, Point cursorCenter, ref int topPos)
        {
            if ((Keyboard.Modifiers & ModifierKeys.Control) == 0 || _rects == null || _rects.Count == 0)
            {
                return;
            }
 
            var textLabels = new List<string>();
 
            foreach (var rect in _rects)
            {
                if (rect.Contains(cursorCenter))
                {
                    textLabels.Add(rect.GetLabel(Type, DataProvider, ToString()));
                }
            }
 
            if (textLabels.Count == 0)
            {
                return;
            }
 
            var left = cursorCenter.X + 15;
            var top = cursorCenter.Y + 13 + topPos;
            var width = 0.0;
            var height = 0.0;
 
            var textRects = new List<Tuple<string, Rect>>();
 
            foreach (var textLabel in textLabels)
            {
                var size = Canvas.ChartFont.GetSize(textLabel);
 
                width = Math.Max(width, size.Width);
 
                var textRect = new Rect(left, top + height + 2, width, size.Height + 2);
 
                height += textRect.Height + 2;
 
                textRects.Add(new Tuple<string, Rect>(textLabel, textRect));
            }
 
            var boxX = cursorCenter.X + 10;
            var leftCorrect = 0.0;
 
            if (boxX + width + 10 >= Canvas.Rect.Right)
            {
                boxX -= width + 30;
                leftCorrect = -(width + 30);
            }
 
            var boxY = cursorCenter.Y + 10;
            var topCorrect = 0.0;
 
            if (topPos == 0)
            {
                if (boxY + height + 10 >= Canvas.Rect.Bottom)
                {
                    boxY -= height + 30;
                    topCorrect = -(height + 30);
                }
            }
 
            var boxRect = new Rect(boxX, boxY + topPos,
                width + 10, height + 7);
 
            topPos += (int)boxRect.Height + 5 + (int)topCorrect;
 
            visual.FillRectangle(Canvas.Theme.ChartBackBrush, boxRect);
 
            visual.DrawRectangle(new XPen(new XBrush(Canvas.Theme.ChartAxisColor), 1), boxRect);
 
            foreach (var textRect in textRects)
            {
                visual.DrawString(textRect.Item1, Canvas.ChartFont, Canvas.Theme.ChartFontBrush,
                    new Rect(textRect.Item2.X + leftCorrect, textRect.Item2.Y + topCorrect, textRect.Item2.Width,
                        textRect.Item2.Height));
            }
        }
 
        public override IndicatorTitleInfo GetTitle()
        {
            return new IndicatorTitleInfo(Title, _selectionBrush);
        }
 
        public override void CopyTemplate(IndicatorBase indicator, bool style)
        {
            var i = (ClusterSearchIndicator)indicator;
 
            Type = i.Type;
 
            Alert.Copy(i.Alert, !style);
 
            OnPropertyChanged(nameof(Alert));
 
            MaximumParam.Copy(i.MaximumParam);
            MinimumParam.Copy(i.MinimumParam);
 
            SelectionColor = i.SelectionColor;
            ObjectBackColor = i.ObjectBackColor;
            ObjectBorderColor = i.ObjectBorderColor;
            ObjectType = i.ObjectType;
            ObjectMinSize = i.ObjectMinSize;
            ObjectMaxSize = i.ObjectMaxSize;
 
            BarsRange = i.BarsRange;
            PriceRange = i.PriceRange;
            PriceRangeDir = i.PriceRangeDir;
            MinValueParam.Copy(i.MinValueParam);
            MinDelta = i.MinDelta;
            BidAskImbalance = i.BidAskImbalance;
            RangeFromHigh = i.RangeFromHigh;
            RangeFromLow = i.RangeFromLow;
            MaxAvgTrade = i.MaxAvgTrade;
            MinAvgTrade = i.MinAvgTrade;
            BarDirection = i.BarDirection;
            PriceLocation = i.PriceLocation;
            SingleSelection = i.SingleSelection;
 
            UseTimeFilter = i.UseTimeFilter;
            StartTime = i.StartTime;
            EndTime = i.EndTime;
 
            OnPropertyChanged(nameof(Maximum));
            OnPropertyChanged(nameof(Minimum));
 
            base.CopyTemplate(indicator, style);
        }
 
        public XBrush GetSelection(int index, long price, int type)
        {
            if (!ShowIndicator || !Bars.ContainsKey(index))
            {
                return null;
            }
 
            switch (Type)
            {
                case ClusterSearchDataType.Volume:
                case ClusterSearchDataType.MaxVol:
 
                    if (type == 1)
                    {
                        return null;
                    }
 
                    break;
 
                case ClusterSearchDataType.Trades:
 
                    if (type == 2)
                    {
                        return null;
                    }
 
                    break;
 
                case ClusterSearchDataType.Bid:
 
                    if (type == 3)
                    {
                        return null;
                    }
 
                    break;
 
                case ClusterSearchDataType.Ask:
 
                    if (type == 4)
                    {
                        return null;
                    }
 
                    break;
 
                case ClusterSearchDataType.Delta:
                case ClusterSearchDataType.DeltaPlus:
                case ClusterSearchDataType.DeltaMinus:
 
                    if (type == 5)
                    {
                        return null;
                    }
 
                    break;
            }
 
            return (SingleSelection
                ? Bars[index].SingleSelection.Contains(price)
                : Bars[index].Selections.Contains(price))
                ? _selectionBrush
                : null;
        }
 
        public override string ToString()
        {
            return Maximum == null ? $"*CS({Type}, {Minimum}+)" : $"*CS({Type}, {Minimum}-{Maximum ?? 0})";
        }
    }
}

Last updated