//------------------------------------------------------------------------------
//
// Графический объект VolumeProfile. 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.Media;
using TigerTrade.Chart.Base;
using TigerTrade.Chart.Data;
using TigerTrade.Chart.Objects.Common;
using TigerTrade.Core.UI.Converters;
using TigerTrade.Dx;
using TigerTrade.Dx.Enums;
namespace TigerTrade.Chart.Objects.Custom
{
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
[DataContract(Name = "VolumeProfileType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Objects.Custom")]
public enum VolumeProfileType
{
[EnumMember(Value = "Volume"), Description("Volume")]
Volume,
[EnumMember(Value = "Trades"), Description("Trades")]
Trades,
[EnumMember(Value = "Delta"), Description("Delta")]
Delta,
[EnumMember(Value = "BidAsk"), Description("Bid x Ask")]
BidAsk
}
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
[DataContract(Name = "VolumeProfileMaximumType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Objects.Custom")]
public enum VolumeProfileMaximumType
{
[EnumMember(Value = "Volume"), Description("Volume")]
Volume,
[EnumMember(Value = "Trades"), Description("Trades")]
Trades,
[EnumMember(Value = "Delta"), Description("Delta")]
Delta,
[EnumMember(Value = "DeltaPlus"), Description("Delta+")]
DeltaPlus,
[EnumMember(Value = "DeltaMinus"), Description("Delta-")]
DeltaMinus,
[EnumMember(Value = "Bid"), Description("Bid")]
Bid,
[EnumMember(Value = "Ask"), Description("Ask")]
Ask
}
[DataContract(Name = "VolumeProfileObject", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Objects.Custom")]
[ChartObject("X_VolumeProfile", "Профиль объёма", 2, Type = typeof(VolumeProfileObject))]
public sealed class VolumeProfileObject : ObjectBase
{
private VolumeProfileType _profileType;
[DataMember(Name = "ProfileType")]
[Category("Профиль"), DisplayName("Тип")]
public VolumeProfileType ProfileType
{
get => _profileType;
set
{
if (value == _profileType)
{
return;
}
_profileType = value;
OnPropertyChanged();
}
}
private XBrush _profileBrush;
private XColor _profileColor;
[DataMember(Name = "ProfileColor")]
[Category("Профиль"), DisplayName("Цвет")]
public XColor ProfileColor
{
get => _profileColor;
set
{
if (value == _profileColor)
{
return;
}
_profileColor = value;
_profileBrush = new XBrush(_profileColor);
OnPropertyChanged();
}
}
private XBrush _profile2Brush;
private XColor _profile2Color;
[DataMember(Name = "Profile2Color")]
[Category("Профиль"), DisplayName("Цвет 2")]
public XColor Profile2Color
{
get => _profile2Color;
set
{
if (value == _profile2Color)
{
return;
}
_profile2Color = value;
_profile2Brush = new XBrush(_profile2Color);
OnPropertyChanged();
}
}
private bool _extendProfile;
[DataMember(Name = "ExtendProfile")]
[Category("Профиль"), DisplayName("Расширить")]
public bool ExtendProfile
{
get => _extendProfile;
set
{
if (value == _extendProfile)
{
return;
}
_extendProfile = value;
OnPropertyChanged();
}
}
private bool _showCumValue;
[DataMember(Name = "ShowCumValue")]
[Category("Профиль"), DisplayName("Отобр. общ. знач.")]
public bool ShowCumValue
{
get => _showCumValue;
set
{
if (value == _showCumValue)
{
return;
}
_showCumValue = value;
OnPropertyChanged();
}
}
private bool _showValues;
[DataMember(Name = "ShowValues")]
[Category("Значения"), DisplayName("Отображать")]
public bool ShowValues
{
get => _showValues;
set
{
if (value == _showValues)
{
return;
}
_showValues = value;
OnPropertyChanged();
}
}
private bool _minimizeValues;
[DataMember(Name = "MinimizeValues")]
[Category("Значения"), DisplayName("Минимизировать")]
public bool MinimizeValues
{
get => _minimizeValues;
set
{
if (value == _minimizeValues)
{
return;
}
_minimizeValues = value;
OnPropertyChanged();
}
}
private int _roundValues;
[DefaultValue(0)]
[DataMember(Name = "RoundValues")]
[Category("Значения"), DisplayName("Округлять")]
public int RoundValues
{
get => _roundValues;
set
{
value = Math.Max(-4, Math.Min(4, value));
if (value == _roundValues)
{
return;
}
_roundValues = value;
OnPropertyChanged();
}
}
private XBrush _valuesBrush;
private XColor _valuesColor;
[DataMember(Name = "ValuesColor")]
[Category("Значения"), DisplayName("Цвет")]
public XColor ValuesColor
{
get => _valuesColor;
set
{
if (value == _valuesColor)
{
return;
}
_valuesColor = value;
_valuesBrush = new XBrush(_valuesColor);
OnPropertyChanged();
}
}
private VolumeProfileMaximumType _maximumType;
[DataMember(Name = "MaximumType")]
[Category("Максимум"), DisplayName("Тип")]
public VolumeProfileMaximumType MaximumType
{
get => _maximumType;
set
{
if (value == _maximumType)
{
return;
}
_maximumType = value;
OnPropertyChanged();
}
}
private bool _showMaximum;
[DataMember(Name = "ShowMaximum")]
[Category("Максимум"), DisplayName("Отображать")]
public bool ShowMaximum
{
get => _showMaximum;
set
{
if (value == _showMaximum)
{
return;
}
_showMaximum = value;
OnPropertyChanged();
}
}
private bool _showMaximumValue;
[DataMember(Name = "ShowMaximumValue")]
[Category("Максимум"), DisplayName("Значение")]
public bool ShowMaximumValue
{
get => _showMaximumValue;
set
{
if (value == _showMaximumValue)
{
return;
}
_showMaximumValue = value;
OnPropertyChanged();
}
}
private bool _extendMaximum;
[DataMember(Name = "ExtendMaximum")]
[Category("Максимум"), DisplayName("Продлить")]
public bool ExtendMaximum
{
get => _extendMaximum;
set
{
if (value == _extendMaximum)
{
return;
}
_extendMaximum = value;
OnPropertyChanged();
}
}
private XBrush _maximumBrush;
private XColor _maximumColor;
[DataMember(Name = "MaximumColor")]
[Category("Максимум"), DisplayName("Цвет")]
public XColor MaximumColor
{
get => _maximumColor;
set
{
if (value == _maximumColor)
{
return;
}
_maximumColor = value;
_maximumBrush = new XBrush(_maximumColor);
OnPropertyChanged();
}
}
private bool _showValueArea;
[DataMember(Name = "ShowValueArea")]
[Category("Value Area"), DisplayName("Отображать")]
public bool ShowValueArea
{
get => _showValueArea;
set
{
if (value == _showValueArea)
{
return;
}
_showValueArea = value;
OnPropertyChanged();
}
}
private bool _extendValueArea;
[DataMember(Name = "ExtendValueArea")]
[Category("Value Area"), DisplayName("Продлить")]
public bool ExtendValueArea
{
get => _extendValueArea;
set
{
if (value == _extendValueArea)
{
return;
}
_extendValueArea = value;
OnPropertyChanged();
}
}
private int _valueAreaPercent;
[DataMember(Name = "ValueAreaPercent")]
[Category("Value Area"), DisplayName("ValueArea %")]
public int ValueAreaPercent
{
get => _valueAreaPercent;
set
{
value = Math.Max(0, Math.Min(100, value));
if (value == 0)
{
value = 70;
}
if (value == _valueAreaPercent)
{
return;
}
_valueAreaPercent = value;
OnPropertyChanged();
}
}
private XBrush _valueAreaBrush;
private XColor _valueAreaColor;
[DataMember(Name = "ValueAreaColor")]
[Category("Value Area"), DisplayName("Цвет")]
public XColor ValueAreaColor
{
get => _valueAreaColor;
set
{
if (value == _valueAreaColor)
{
return;
}
_valueAreaColor = value;
_valueAreaBrush = new XBrush(_valueAreaColor);
OnPropertyChanged();
}
}
private bool _enableFilter;
[DataMember(Name = "EnableFilter")]
[Category("Фильтр"), DisplayName("Включить")]
public bool EnableFilter
{
get => _enableFilter;
set
{
if (value == _enableFilter)
{
return;
}
_enableFilter = value;
OnPropertyChanged();
}
}
private int _filterMin;
[DataMember(Name = "FilterMin")]
[Category("Фильтр"), DisplayName("Минимум")]
public int FilterMin
{
get => _filterMin;
set
{
value = Math.Max(0, value);
if (value == _filterMin)
{
return;
}
_filterMin = value;
OnPropertyChanged();
}
}
private int _filterMax;
[DataMember(Name = "FilterMax")]
[Category("Фильтр"), DisplayName("Максимум")]
public int FilterMax
{
get => _filterMax;
set
{
value = Math.Max(0, value);
if (value == _filterMax)
{
return;
}
_filterMax = value;
OnPropertyChanged();
}
}
private XBrush _filterBrush;
private XColor _filterColor;
[DataMember(Name = "FilterColor")]
[Category("Фильтр"), DisplayName("Цвет")]
public XColor FilterColor
{
get => _filterColor;
set
{
if (value == _filterColor)
{
return;
}
_filterColor = value;
_filterBrush = new XBrush(_filterColor);
OnPropertyChanged();
}
}
private bool _drawBorder;
[DataMember(Name = "DrawBorder")]
[Category("Граница"), DisplayName("Граница")]
public bool DrawBorder
{
get => _drawBorder;
set
{
if (value == _drawBorder)
{
return;
}
_drawBorder = value;
OnPropertyChanged();
}
}
private XBrush _lineBrush;
private XPen _linePen;
private XColor _lineColor;
[DataMember(Name = "LineColor")]
[Category("Граница"), DisplayName("Цвет линии")]
public XColor LineColor
{
get => _lineColor;
set
{
if (value == _lineColor)
{
return;
}
_lineColor = value;
_lineBrush = new XBrush(_lineColor);
_linePen = new XPen(_lineBrush, LineWidth, LineStyle);
OnPropertyChanged();
}
}
private int _lineWidth;
[DataMember(Name = "LineWidth")]
[Category("Граница"), DisplayName("Толщина линии")]
public int LineWidth
{
get => _lineWidth;
set
{
value = Math.Max(1, Math.Min(10, value));
if (value == _lineWidth)
{
return;
}
_lineWidth = value;
_linePen = new XPen(_lineBrush, _lineWidth, LineStyle);
OnPropertyChanged();
}
}
private XDashStyle _lineStyle;
[DataMember(Name = "LineStyle")]
[Category("Граница"), DisplayName("Стиль линии")]
public XDashStyle LineStyle
{
get => _lineStyle;
set
{
if (value == _lineStyle)
{
return;
}
_lineStyle = value;
_linePen = new XPen(_lineBrush, LineWidth, _lineStyle);
OnPropertyChanged();
}
}
private bool _drawBack;
[DataMember(Name = "DrawBack")]
[Category("Фон"), DisplayName("Фон")]
public bool DrawBack
{
get => _drawBack;
set
{
if (value == _drawBack)
{
return;
}
_drawBack = value;
OnPropertyChanged();
}
}
private XBrush _backBrush;
private XColor _backColor;
[DataMember(Name = "BackColor")]
[Category("Фон"), DisplayName("Цвет фона")]
public XColor BackColor
{
get => _backColor;
set
{
if (value == _backColor)
{
return;
}
_backColor = value;
_backBrush = new XBrush(_backColor);
OnPropertyChanged();
}
}
protected override int PenWidth => LineWidth;
private bool _isObjectInArea;
public class RectangleInfo
{
public Point ControlPoint1;
public Point ControlPoint2;
public Point ExtraPoint1;
public Point ExtraPoint2;
public Rect Rectangle;
}
private RectangleInfo _rectInfo;
public VolumeProfileObject()
{
ProfileType = VolumeProfileType.Volume;
ProfileColor = Color.FromArgb(127, 70, 130, 180);
Profile2Color = Color.FromArgb(127, 178, 34, 34);
ExtendProfile = false;
ShowCumValue = false;
ShowValues = false;
MinimizeValues = false;
ValuesColor = Color.FromArgb(255, 255, 255, 255);
MaximumType = VolumeProfileMaximumType.Volume;
ShowMaximum = true;
ShowMaximumValue = true;
ExtendMaximum = false;
MaximumColor = Color.FromArgb(127, 178, 34, 34);
ShowValueArea = true;
ExtendValueArea = false;
ValueAreaPercent = 70;
ValueAreaColor = Color.FromArgb(127, 128, 128, 128);
EnableFilter = false;
FilterMin = 0;
FilterMax = 0;
FilterColor = Color.FromArgb(127, 46, 139, 87);
DrawBorder = true;
LineColor = Colors.Black;
LineWidth = 1;
LineStyle = XDashStyle.Solid;
DrawBack = true;
BackColor = Color.FromArgb(30, 0, 0, 0);
}
protected override void Prepare()
{
var point1 = ToPoint(ControlPoints[0]);
var point2 = ToPoint(ControlPoints[1]);
var ep1 = ToPoint(ExtraPoints[0]);
var ep2 = ToPoint(ExtraPoints[1]);
var w = Canvas.ColumnWidth / 2.0;
var h = Canvas.StepHeight / 2.0;
if (point1.X > point2.X)
{
point1.X += w;
point2.X -= w;
}
else
{
point1.X -= w;
point2.X += w;
}
if (point1.Y > point2.Y)
{
point1.Y += h;
point2.Y -= h;
}
else
{
point1.Y -= h;
point2.Y += h;
}
if (ep1.X > ep2.X)
{
ep1.X += w;
ep2.X -= w;
}
else
{
ep1.X -= w;
ep2.X += w;
}
if (ep1.Y > ep2.Y)
{
ep1.Y += h;
ep2.Y -= h;
}
else
{
ep1.Y -= h;
ep2.Y += h;
}
_rectInfo = new RectangleInfo
{
ControlPoint1 = point1,
ControlPoint2 = point2,
ExtraPoint1 = ep1,
ExtraPoint2 = ep2,
Rectangle = new Rect(point1, point2)
};
_isObjectInArea = Canvas.Rect.IntersectsWith(_rectInfo.Rectangle);
}
protected override void Draw(DxVisualQueue visual, ref List<ObjectLabelInfo> labels)
{
if (!ExtendProfile)
{
if (DrawBack)
{
visual.FillRectangle(_backBrush, _rectInfo.Rectangle);
}
if (DrawBorder)
{
visual.DrawRectangle(_linePen, _rectInfo.Rectangle);
}
}
if (!Canvas.IsStock)
{
return;
}
var index1 = 0;
var index2 = 1;
if (ControlPoints[0].X > ControlPoints[1].X)
{
index1 = 1;
index2 = 0;
}
var bar1 = Canvas.DateToIndex(ControlPoints[index1].X, 0);
var bar2 = Canvas.DateToIndex(ControlPoints[index2].X, 0);
var step = DataProvider.Step;
var maxPrice = (long)(Math.Max(ControlPoints[0].Y, ControlPoints[1].Y) / step);
var minPrice = (long)(Math.Min(ControlPoints[0].Y, ControlPoints[1].Y) / step);
var profile = new RawCluster(DateTime.MinValue);
if (ExtendProfile)
{
for (var i = bar1; i <= bar2; i++)
{
var cluster = DataProvider.GetRawCluster(i);
if (cluster == null)
{
continue;
}
foreach (var item in cluster.Items)
{
profile.AddItem(item);
}
maxPrice = Math.Max(maxPrice, cluster.High);
minPrice = Math.Min(minPrice, cluster.Low);
}
}
else
{
for (var i = bar1; i <= bar2; i++)
{
var cluster = DataProvider.GetRawCluster(i);
if (cluster == null)
{
continue;
}
foreach (var item in cluster.Items)
{
if (item.Price >= minPrice && item.Price <= maxPrice)
{
profile.AddItem(item);
}
}
}
}
profile.UpdateData();
if (profile.Volume <= 0)
{
return;
}
var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
var p1 = ToPoint(ControlPoints[index1]);
var p2 = ToPoint(ControlPoints[index2]);
switch (ProfileType)
{
case VolumeProfileType.Volume:
DrawVolume(visual, profile, valueArea, p1, p2, ref labels);
break;
case VolumeProfileType.Trades:
DrawTrades(visual, profile, valueArea, p1, p2, ref labels);
break;
case VolumeProfileType.Delta:
DrawDelta(visual, profile, valueArea, p1, p2, ref labels);
break;
case VolumeProfileType.BidAsk:
DrawBidAsk(visual, profile, valueArea, p1, p2, ref labels);
break;
}
if (ShowCumValue)
{
DrawValues(visual, profile);
}
}
private void DrawValues(DxVisualQueue visual, IRawCluster profile)
{
var symbol = DataProvider.Symbol;
var mainRect = _rectInfo.Rectangle;
var height = Canvas.ChartFont.GetHeight();
var rect = new Rect(new Point(mainRect.Left + 2, mainRect.Bottom + 4),
new Point(mainRect.Right - 2, mainRect.Bottom + height + 4));
switch (ProfileType)
{
case VolumeProfileType.Volume:
var volumeText = symbol.FormatRawSize(profile.Volume, RoundValues, MinimizeValues);
visual.DrawString($"V: {volumeText}", Canvas.ChartFont, _valuesBrush, rect, XTextAlignment.Right);
break;
case VolumeProfileType.Trades:
var tradesText = symbol.FormatTrades(profile.Trades, RoundValues, MinimizeValues);
visual.DrawString($"T: {tradesText}", Canvas.ChartFont, _valuesBrush, rect, XTextAlignment.Right);
break;
case VolumeProfileType.Delta:
var deltaText = symbol.FormatRawSize(profile.Delta, RoundValues, MinimizeValues);
visual.DrawString($"D: {deltaText}", Canvas.ChartFont, _valuesBrush, rect, XTextAlignment.Right);
break;
case VolumeProfileType.BidAsk:
var bidText = symbol.FormatRawSize(profile.Bid, RoundValues, MinimizeValues);
var askText = symbol.FormatRawSize(profile.Ask, RoundValues, MinimizeValues);
visual.DrawString($"B: {bidText} A: {askText}", Canvas.ChartFont, _valuesBrush, rect,
XTextAlignment.Right);
break;
}
}
private bool CheckMaximum(IRawClusterItem item, IRawClusterMaxValues maxValues)
{
switch (MaximumType)
{
case VolumeProfileMaximumType.Volume:
return item.Volume == maxValues.MaxVolume;
case VolumeProfileMaximumType.Trades:
return item.Trades == maxValues.MaxTrades;
case VolumeProfileMaximumType.Delta:
return Math.Abs(item.Delta) ==
Math.Max(Math.Abs(maxValues.MaxDelta), Math.Abs(maxValues.MinDelta));
case VolumeProfileMaximumType.DeltaPlus:
return item.Delta > 0 && item.Delta == maxValues.MaxDelta;
case VolumeProfileMaximumType.DeltaMinus:
return item.Delta < 0 && item.Delta == maxValues.MinDelta;
case VolumeProfileMaximumType.Bid:
return item.Bid == maxValues.MaxBid;
case VolumeProfileMaximumType.Ask:
return item.Ask == maxValues.MaxAsk;
}
return false;
}
private string FormatMaximum(IRawClusterItem item)
{
switch (MaximumType)
{
case VolumeProfileMaximumType.Volume:
return DataProvider.Symbol.FormatRawSize(item.Volume, RoundValues, MinimizeValues);
case VolumeProfileMaximumType.Trades:
return DataProvider.Symbol.FormatTrades(item.Trades, RoundValues, MinimizeValues);
case VolumeProfileMaximumType.Delta:
case VolumeProfileMaximumType.DeltaPlus:
case VolumeProfileMaximumType.DeltaMinus:
return DataProvider.Symbol.FormatRawSize(item.Delta, RoundValues, MinimizeValues);
case VolumeProfileMaximumType.Bid:
return DataProvider.Symbol.FormatRawSize(item.Bid, RoundValues, MinimizeValues);
case VolumeProfileMaximumType.Ask:
return DataProvider.Symbol.FormatRawSize(item.Ask, RoundValues, MinimizeValues);
}
return "";
}
private void DrawVolume(DxVisualQueue visual, IRawCluster profile, IRawClusterValueArea valueArea,
Point p1, Point p2, ref List<ObjectLabelInfo> labels)
{
var colorRects = new List<Tuple<Rect, XBrush>>();
var colorRects2 = new List<Tuple<Rect, XBrush>>();
var valueRects = new List<Tuple<Rect, string>>();
var valueRects2 = new List<Tuple<Rect, string>>();
var step = DataProvider.Step;
var symbol = DataProvider.Symbol;
var height = Math.Max(Canvas.StepHeight, 1);
var fontSize = Math.Min(height - 2, 18) * 96 / 72;
fontSize = Math.Min(fontSize, Canvas.ChartFont.Size);
var normalFont = new XFont(Canvas.ChartFont.Name, fontSize);
var dist = Math.Max(p2.X - p1.X + Canvas.ColumnWidth - LineWidth, 0);
var left = p1.X - Canvas.ColumnWidth / 2.0 + Math.Ceiling(LineWidth / 2.0);
if (ExtendProfile)
{
if (DrawBack)
{
visual.FillRectangle(_backBrush,
new Rect(new Point(left, Canvas.Rect.Top), new Point(left + dist, Canvas.Rect.Bottom)));
}
if (DrawBorder)
{
visual.DrawLine(_linePen, new Point(left, Canvas.Rect.Top),
new Point(left, Canvas.Rect.Bottom));
visual.DrawLine(_linePen, new Point(left + dist, Canvas.Rect.Top),
new Point(left + dist, Canvas.Rect.Bottom));
}
}
if (profile.High - profile.Low > 100000)
{
return;
}
var maxValues = profile.MaxValues;
var roundValues = RoundValues;
var prevX = (int)left;
var prevY = (int)GetY((profile.High + .5) * step);
var points = new List<Point>
{
new Point(prevX, prevY)
};
for (var j = profile.High; j >= profile.Low; j--)
{
var item = profile.GetItem(j) ?? new RawClusterItem(j);
var width = item.Volume > 0 ? Math.Min(dist / maxValues.MaxVolume * item.Volume, dist) : 0;
var currX = (int)(left + width);
var currY = (int)GetY((j - .5) * step);
var currHeight = Math.Max(currY - prevY, height);
if (currY <= prevY && points.Count > 2)
{