# 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})";
        }
    }
}
```


---

# Agent Instructions: Querying This Documentation

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

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

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

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

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