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