Cluster Search Indicator Code
//------------------------------------------------------------------------------ // // ClusterSearch. Copyright (c) 2019 Ilya Smirnov. 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})"; } } }