//------------------------------------------------------------------------------
//
// Indicator 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))
{