Volume Profiles
//------------------------------------------------------------------------------
//
// Индикатор Volume Profiles. 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.Enums;
using TigerTrade.Chart.Data;
using TigerTrade.Chart.Indicators.Common;
using TigerTrade.Chart.Indicators.Enums;
using TigerTrade.Core.UI.Converters;
using TigerTrade.Core.Utils.Time;
using TigerTrade.Dx;
using TigerTrade.Dx.Enums;
namespace TigerTrade.Chart.Indicators.Custom
{
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
[DataContract(Name = "VolumeProfilesPeriodType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
public enum VolumeProfilesPeriodType
{
[EnumMember(Value = "Minute"), Description("Минута")]
Minute,
[EnumMember(Value = "Hour"), Description("Час")]
Hour,
[EnumMember(Value = "Day"), Description("День")]
Day,
[EnumMember(Value = "Week"), Description("Неделя")]
Week,
[EnumMember(Value = "Month"), Description("Месяц")]
Month
}
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
[DataContract(Name = "VolumeProfilesType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
public enum VolumeProfilesType
{
[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 = "VolumeProfilesMaximumType", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
public enum VolumeProfilesMaximumType
{
[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 = "VolumeProfilesIndicator", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
[Indicator("X_VolumeProfiles", "*VolumeProfiles", true, Type = typeof(VolumeProfilesIndicator))]
internal sealed class VolumeProfilesIndicator : IndicatorBase
{
private List<VolumeProfile> _profiles;
private List<VolumeProfile> Profiles => _profiles ?? (_profiles = new List<VolumeProfile>());
private VolumeProfilesPeriodType _periodType;
[DataMember(Name = "PeriodType")]
[Category("Период"), DisplayName("Интервал")]
public VolumeProfilesPeriodType PeriodType
{
get => _periodType;
set
{
if (value == _periodType)
{
return;
}
_periodType = value;
_periodValue = _periodType == VolumeProfilesPeriodType.Minute ? 15 : 1;
Clear();
OnPropertyChanged();
}
}
private int _periodValue;
[DataMember(Name = "PeriodValue")]
[Category("Период"), DisplayName("Значение")]
public int PeriodValue
{
get => _periodValue;
set
{
value = Math.Max(1, value);
if (value == _periodValue)
{
return;
}
_periodValue = value;
Clear();
OnPropertyChanged();
}
}
private VolumeProfilesType _profileType;
[DataMember(Name = "ProfileType")]
[Category("Профиль"), DisplayName("Тип")]
public VolumeProfilesType ProfileType
{
get => _profileType;
set
{
if (value == _profileType)
{
return;
}
_profileType = value;
OnPropertyChanged();
}
}
private int _profileProportion;
[DataMember(Name = "ProfileProportion")]
[Category("Профиль"), DisplayName("Объём шкалы")]
public int ProfileProportion
{
get => _profileProportion;
set
{
value = Math.Max(0, value);
if (value == _profileProportion)
{
return;
}
_profileProportion = 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 _backBrush;
private XColor _profileBackColor;
[DataMember(Name = "ProfileBackColor")]
[Category("Профиль"), DisplayName("Цвет фона")]
public XColor ProfileBackColor
{
get => _profileBackColor;
set
{
if (value == _profileBackColor)
{
return;
}
_profileBackColor = value;
_backBrush = new XBrush(_profileBackColor);
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 IndicatorIntParam _roundValueParam;
[DataMember(Name = "RoundValueParam")]
public IndicatorIntParam RoundValuesParam
{
get => _roundValueParam ?? (_roundValueParam = new IndicatorIntParam(0));
set => _roundValueParam = value;
}
[DefaultValue(0)]
[Category("Значения"), DisplayName("Округлять")]
public int RoundValues
{
get => RoundValuesParam.Get(SettingsLongKey);
set
{
if (!RoundValuesParam.Set(SettingsLongKey, value, -4, 4))
{
return;
}
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 VolumeProfilesMaximumType _maximumType;
[DataMember(Name = "MaximumType")]
[Category("Максимум"), DisplayName("Тип")]
public VolumeProfilesMaximumType 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 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 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;
Clear();
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 = "FilterValue")]
[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();
}
}
[Browsable(false)]
public override bool ShowIndicatorValues => false;
[Browsable(false)]
public override bool ShowIndicatorLabels => false;
[Browsable(false)]
public override IndicatorCalculation Calculation => IndicatorCalculation.OnEachTick;
public VolumeProfilesIndicator()
{
ShowIndicatorTitle = false;
PeriodType = VolumeProfilesPeriodType.Hour;
PeriodValue = 1;
ProfileType = VolumeProfilesType.Volume;
ProfileProportion = 0;
ProfileColor = Color.FromArgb(127, 70, 130, 180);
ProfileBackColor = Color.FromArgb(30, 70, 130, 180);
ShowValues = false;
MinimizeValues = false;
ValuesColor = Color.FromArgb(255, 255, 255, 255);
MaximumType = VolumeProfilesMaximumType.Volume;
ShowMaximum = true;
MaximumColor = Color.FromArgb(127, 178, 34, 34);
ShowValueArea = true;
ValueAreaPercent = 70;
ValueAreaColor = Color.FromArgb(127, 128, 128, 128);
EnableFilter = false;
FilterMin = 0;
FilterMax = 0;
FilterColor = Color.FromArgb(127, 46, 139, 87);
}
private int _lastFullID;
private void Clear()
{
_lastFullID = 0;
Profiles.Clear();
}
private int GetSequence(DateTime date, double offset)
{
var cycleBase = ChartPeriodType.Hour;
switch (PeriodType)
{
case VolumeProfilesPeriodType.Minute:
cycleBase = ChartPeriodType.Minute;
break;
case VolumeProfilesPeriodType.Hour:
cycleBase = ChartPeriodType.Hour;
break;
case VolumeProfilesPeriodType.Day:
cycleBase = ChartPeriodType.Day;
break;
case VolumeProfilesPeriodType.Week:
cycleBase = ChartPeriodType.Week;
break;
case VolumeProfilesPeriodType.Month:
cycleBase = ChartPeriodType.Month;
break;
}
return DataProvider.Period.GetSequence(cycleBase, PeriodValue, date, offset);
}
protected override void Execute()
{
if (ClearData)
{
Clear();
}
if (Profiles.Count > 0 && !Profiles[Profiles.Count - 1].Completed)
{
Profiles.RemoveAt(Profiles.Count - 1);
}
var timeOffset = TimeHelper.GetSessionOffset(DataProvider.Symbol.Exchange);
var lastSequence = -1;
for (var i = _lastFullID; i < DataProvider.Count; i++)
{
var cluster = DataProvider.GetRawCluster(i);
var currSequence = GetSequence(cluster.Time, timeOffset);
if (Profiles.Count == 0 || currSequence != lastSequence)
{
lastSequence = currSequence;
if (Profiles.Count > 0 && i > _lastFullID)
{
_lastFullID = i;
Profiles[Profiles.Count - 1].Completed = true;
Profiles[Profiles.Count - 1].Cluster.UpdateData();
}
Profiles.Add(new VolumeProfile(new RawCluster(cluster.Time), i));
}
var lastCluster = Profiles[Profiles.Count - 1];
lastCluster.Cluster.AddCluster(cluster);
lastCluster.EndBar = i;
}
if (Profiles.Count > 0 && !Profiles[Profiles.Count - 1].Completed)
{
Profiles[Profiles.Count - 1].Cluster.UpdateData();
}
}
public override void Render(DxVisualQueue visual)
{
if (Profiles.Count == 0)
{
return;
}
var minPrice = (long)(Canvas.MinY / Helper.DataProvider.Step) - 1;
var maxPrice = (long)(Canvas.MaxY / Helper.DataProvider.Step) + 1;
if (maxPrice - minPrice > 20000)
{
return;
}
switch (ProfileType)
{
case VolumeProfilesType.Volume:
RenderVolume(visual);
break;
case VolumeProfilesType.Trades:
RenderTrades(visual);
break;
case VolumeProfilesType.Delta:
RenderDelta(visual);
break;
case VolumeProfilesType.BidAsk:
RenderBidAsk(visual);
break;
}
}
private void RenderVolume(DxVisualQueue visual)
{
var startIndex = Canvas.Stop;
var endIndex = Canvas.Stop + Canvas.Count;
var step = DataProvider.Step;
var symbol = DataProvider.Symbol;
var minFilter = symbol.CorrectSizeFilter(FilterMin);
var maxFilter = symbol.CorrectSizeFilter(FilterMax);
var proportion = symbol.CorrectSizeFilter(ProfileProportion);
var height = GetY(0.0) - GetY(step);
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 columnWidth2 = Canvas.ColumnWidth / 2.0;
var roundValues = RoundValues;
var prevRight = int.MinValue;
foreach (var volumeProfile in Profiles)
{
if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
{
continue;
}
var x1 = Canvas.GetX(volumeProfile.StartBar);
var x2 = Canvas.GetX(volumeProfile.EndBar);
var profile = volumeProfile.Cluster;
var maxValues = profile.MaxValues;
var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
var left = x1 - columnWidth2;
var right = x2 + columnWidth2 - 1;
if (prevRight != int.MinValue)
{
left = prevRight;
}
prevRight = (int)right;
var dist = Math.Max(right - left, 1);
var max = ProfileProportion > 0 ? proportion : maxValues.MaxVolume;
var volStep = dist / Math.Max(max, 1);
var colorRects = new List<Tuple<Rect, XBrush>>();
var colorRects2 = new List<Tuple<Rect, XBrush>>();
var valueRects = new List<Tuple<Rect, string>>();
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 = Math.Min(volStep * item.Volume, dist);
var currX = (int)(left + width);
var currY = (int)GetY((j - .5) * step);
var currHeight = Math.Max(currY - prevY, 1);
if (currY == prevY && points.Count > 2)
{
if (currX > prevX)
{
points[points.Count - 2] = new Point(currX, points[points.Count - 2].Y);
points[points.Count - 1] = new Point(currX, points[points.Count - 1].Y);
prevX = currX;
}
}
else
{
points.Add(new Point(currX, prevY));
points.Add(new Point(currX, currY));
prevX = currX;
}
prevY = currY;
var topY = points[points.Count - 2].Y;
if (ShowMaximum && CheckMaximum(item, maxValues))
{
colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
}
else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
{
colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight),
_valueAreaBrush));
}
else if (EnableFilter && item.Volume >= minFilter &&
(maxFilter == 0 || item.Volume <= maxFilter))
{
if (colorRects.Count > 0)
{
var lastRect = colorRects[colorRects.Count - 1].Item1;
if ((int)lastRect.Y == (int)topY)
{
if (width > lastRect.Width)
{
colorRects[colorRects.Count - 1] =
new Tuple<Rect, XBrush>(new Rect(left, topY, width, lastRect.Height), _filterBrush);
}
}
else
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight),
_filterBrush));
}
}
else
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight), _filterBrush));
}
}
if (ShowValues && height > 7 && item.Volume > 0)
{
valueRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist, height),
symbol.FormatRawSize(item.Volume, roundValues, MinimizeValues)));
}
}
points.Add(new Point(left, prevY));
visual.FillRectangle(_backBrush,
new Rect(new Point(left, points[0].Y), new Point(right, prevY)));
visual.FillPolygon(_profileBrush, points.ToArray());
foreach (var colorRect in colorRects)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var colorRect in colorRects2)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var valueRect in valueRects)
{
visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
}
}
}
private void RenderTrades(DxVisualQueue visual)
{
var startIndex = Canvas.Stop;
var endIndex = Canvas.Stop + Canvas.Count;
var step = DataProvider.Step;
var symbol = DataProvider.Symbol;
var height = GetY(0.0) - GetY(step);
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 columnWidth2 = Canvas.ColumnWidth / 2.0;
var roundValues = RoundValues;
var prevRight = int.MinValue;
foreach (var volumeProfile in Profiles)
{
if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
{
continue;
}
var x1 = Canvas.GetX(volumeProfile.StartBar);
var x2 = Canvas.GetX(volumeProfile.EndBar);
var profile = volumeProfile.Cluster;
var maxValues = profile.MaxValues;
var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
var left = x1 - columnWidth2;
var right = x2 + columnWidth2 - 1;
if (prevRight != int.MinValue)
{
left = prevRight;
}
prevRight = (int)right;
var dist = Math.Max(right - left, 1);
var max = ProfileProportion > 0 ? ProfileProportion : maxValues.MaxTrades;
var volStep = dist / Math.Max(max, 1);
var colorRects = new List<Tuple<Rect, XBrush>>();
var colorRects2 = new List<Tuple<Rect, XBrush>>();
var valueRects = new List<Tuple<Rect, string>>();
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 = Math.Min(volStep * item.Trades, dist);
var currX = (int)(left + width);
var currY = (int)GetY((j - .5) * step);
var currHeight = Math.Max(currY - prevY, 1);
if (currY == prevY && points.Count > 2)
{
if (currX > prevX)
{
points[points.Count - 2] = new Point(currX, points[points.Count - 2].Y);
points[points.Count - 1] = new Point(currX, points[points.Count - 1].Y);
prevX = currX;
}
}
else
{
points.Add(new Point(currX, prevY));
points.Add(new Point(currX, currY));
prevX = currX;
}
prevY = currY;
var topY = points[points.Count - 2].Y;
if (ShowMaximum && CheckMaximum(item, maxValues))
{
colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
}
else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
{
colorRects2.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight),
_valueAreaBrush));
}
else if (EnableFilter && item.Trades >= FilterMin && (FilterMax == 0 || item.Trades <= FilterMax))
{
if (colorRects.Count > 0)
{
var lastRect = colorRects[colorRects.Count - 1].Item1;
if ((int)lastRect.Y == (int)topY)
{
if (width > lastRect.Width)
{
colorRects[colorRects.Count - 1] =
new Tuple<Rect, XBrush>(new Rect(left, topY, width, lastRect.Height), _filterBrush);
}
}
else
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight),
_filterBrush));
}
}
else
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, width, currHeight), _filterBrush));
}
}
if (ShowValues && height > 7 && item.Trades > 0)
{
valueRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist, height),
symbol.FormatTrades(item.Trades, roundValues, MinimizeValues)));
}
}
points.Add(new Point(left, prevY));
visual.FillRectangle(_backBrush,
new Rect(new Point(left, points[0].Y), new Point(right, prevY)));
visual.FillPolygon(_profileBrush, points.ToArray());
foreach (var colorRect in colorRects)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var colorRect in colorRects2)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var valueRect in valueRects)
{
visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
}
}
}
private void RenderDelta(DxVisualQueue visual)
{
var startIndex = Canvas.Stop;
var endIndex = Canvas.Stop + Canvas.Count;
var step = DataProvider.Step;
var symbol = DataProvider.Symbol;
var minFilter = symbol.CorrectSizeFilter(FilterMin);
var maxFilter = symbol.CorrectSizeFilter(FilterMax);
var proportion = symbol.CorrectSizeFilter(ProfileProportion);
var height = GetY(0.0) - GetY(step);
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 columnWidth2 = Canvas.ColumnWidth / 2.0;
var roundValues = RoundValues;
var prevRight = int.MinValue;
foreach (var volumeProfile in Profiles)
{
if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
{
continue;
}
var x1 = Canvas.GetX(volumeProfile.StartBar);
var x2 = Canvas.GetX(volumeProfile.EndBar);
var profile = volumeProfile.Cluster;
var maxValues = profile.MaxValues;
var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
var left = x1 - columnWidth2;
var right = x2 + columnWidth2 - 1;
if (prevRight != int.MinValue)
{
left = prevRight;
}
prevRight = (int)right;
var dist = Math.Max(right - left, 1);
var max = ProfileProportion > 0
? proportion
: Math.Max(Math.Abs(maxValues.MinDelta), Math.Abs(maxValues.MaxDelta));
var volStep = dist / Math.Max(max, 1);
var colorRects = new List<Tuple<Rect, XBrush>>();
var colorRectsLeft = new List<Tuple<Rect, XBrush>>();
var colorRectsRight = new List<Tuple<Rect, XBrush>>();
var valueLeftRects = new List<Tuple<Rect, string>>();
var valueRightRects = new List<Tuple<Rect, string>>();
var center = left + dist / 2.0;
// right part
var prevX = (int)center;
var prevY = (int)GetY((profile.High + .5) * step);
var pointsRight = 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.Delta > 0 ? Math.Min(volStep * Math.Abs(item.Delta), dist) / 2.0 : 0;
var currX = (int)(center + width);
var currY = (int)GetY((j - .5) * step);
var currHeight = Math.Max(currY - prevY, 1);
if (currY <= prevY && pointsRight.Count > 2)
{
if (currX > prevX)
{
pointsRight[pointsRight.Count - 2] = new Point(currX, pointsRight[pointsRight.Count - 2].Y);
pointsRight[pointsRight.Count - 1] = new Point(currX, pointsRight[pointsRight.Count - 1].Y);
prevX = currX;
}
}
else
{
pointsRight.Add(new Point(currX, prevY));
pointsRight.Add(new Point(currX, currY));
prevX = currX;
}
prevY = currY;
var topY = pointsRight[pointsRight.Count - 2].Y;
if (ShowMaximum && CheckMaximum(item, maxValues))
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
}
else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _valueAreaBrush));
}
else if (EnableFilter)
{
if (item.Delta > 0 && item.Delta >= minFilter &&
(maxFilter == 0 || item.Delta <= maxFilter))
{
if (colorRectsRight.Count > 0)
{
var lastRect = colorRectsRight[colorRectsRight.Count - 1].Item1;
if ((int)lastRect.Y == (int)topY)
{
if (width > lastRect.Width)
{
colorRectsRight[colorRectsRight.Count - 1] =
new Tuple<Rect, XBrush>(new Rect(center, topY, width, lastRect.Height), _filterBrush);
}
}
else
{
colorRectsRight.Add(new Tuple<Rect, XBrush>(new Rect(center, topY, width, currHeight),
_filterBrush));
}
}
else
{
colorRectsRight.Add(new Tuple<Rect, XBrush>(new Rect(center, topY, width, currHeight), _filterBrush));
}
}
}
if (ShowValues && height > 7 && item.Delta > 0)
{
valueRightRects.Add(new Tuple<Rect, string>(new Rect(center + 2, topY, dist / 2.0, height),
symbol.FormatRawSize(item.Delta, roundValues, MinimizeValues)));
}
}
pointsRight.Add(new Point(center, prevY));
// left part
prevX = (int)center;
prevY = (int)GetY((profile.High + .5) * step);
var pointsLeft = 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.Delta < 0 ? Math.Min(volStep * Math.Abs(item.Delta), dist) / 2.0 : 0;
var currX = (int)(center - width);
var currY = (int)GetY((j - .5) * step);
var currHeight = Math.Max(currY - prevY, 1);
if (currY <= prevY && pointsLeft.Count > 2)
{
if (currX < prevX)
{
pointsLeft[pointsLeft.Count - 2] = new Point(currX, pointsLeft[pointsLeft.Count - 2].Y);
pointsLeft[pointsLeft.Count - 1] = new Point(currX, pointsLeft[pointsLeft.Count - 1].Y);
prevX = currX;
}
}
else
{
pointsLeft.Add(new Point(currX, prevY));
pointsLeft.Add(new Point(currX, currY));
prevX = currX;
}
prevY = currY;
var topY = pointsLeft[pointsLeft.Count - 2].Y;
if (ShowMaximum && CheckMaximum(item, maxValues))
{
}
else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
{
}
else if (EnableFilter)
{
if (item.Delta < 0 && -item.Delta >= minFilter &&
(maxFilter == 0 || -item.Delta <= maxFilter))
{
if (colorRectsLeft.Count > 0)
{
var lastRect = colorRectsLeft[colorRectsLeft.Count - 1].Item1;
if ((int)lastRect.Y == (int)topY)
{
if (width > lastRect.Width)
{
colorRectsLeft[colorRectsLeft.Count - 1] =
new Tuple<Rect, XBrush>(
new Rect(center - width, topY, width, lastRect.Height),
_filterBrush);
}
}
else
{
colorRectsLeft.Add(new Tuple<Rect, XBrush>(
new Rect(center - width, topY, width, currHeight),
_filterBrush));
}
}
else
{
colorRectsLeft.Add(
new Tuple<Rect, XBrush>(new Rect(center - width, topY, width, currHeight),
_filterBrush));
}
}
}
if (ShowValues && height > 7 && item.Delta < 0)
{
valueLeftRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist / 2.0 - 2, height),
symbol.FormatRawSize(item.Delta, roundValues, MinimizeValues)));
}
}
pointsLeft.Add(new Point(center, prevY));
visual.FillRectangle(_backBrush,
new Rect(new Point(left, pointsLeft[0].Y), new Point(right, prevY)));
visual.FillPolygon(_profileBrush, pointsLeft.ToArray());
visual.FillPolygon(_profileBrush, pointsRight.ToArray());
foreach (var colorRect in colorRectsLeft)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var colorRect in colorRectsRight)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var colorRect in colorRects)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var valueRect in valueLeftRects)
{
visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1, XTextAlignment.Right);
}
foreach (var valueRect in valueRightRects)
{
visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
}
}
}
private void RenderBidAsk(DxVisualQueue visual)
{
var startIndex = Canvas.Stop;
var endIndex = Canvas.Stop + Canvas.Count;
var step = DataProvider.Step;
var symbol = DataProvider.Symbol;
var minFilter = symbol.CorrectSizeFilter(FilterMin);
var maxFilter = symbol.CorrectSizeFilter(FilterMax);
var proportion = symbol.CorrectSizeFilter(ProfileProportion);
var height = GetY(0.0) - GetY(step);
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 columnWidth2 = Canvas.ColumnWidth / 2.0;
var roundValues = RoundValues;
var prevRight = int.MinValue;
foreach (var volumeProfile in Profiles)
{
if (volumeProfile.EndBar < startIndex || volumeProfile.StartBar > endIndex)
{
continue;
}
var x1 = Canvas.GetX(volumeProfile.StartBar);
var x2 = Canvas.GetX(volumeProfile.EndBar);
var profile = volumeProfile.Cluster;
var maxValues = profile.MaxValues;
var valueArea = ShowValueArea ? profile.GetValueArea(ValueAreaPercent) : null;
var left = x1 - columnWidth2;
var right = x2 + columnWidth2 - 1;
if (prevRight != int.MinValue)
{
left = prevRight;
}
prevRight = (int)right;
var dist = Math.Max(right - left, 1);
var max = ProfileProportion > 0
? proportion
: Math.Max(maxValues.MaxBid, maxValues.MaxAsk);
var volStep = dist / Math.Max(max, 1);
var colorRects = new List<Tuple<Rect, XBrush>>();
var colorRectsLeft = new List<Tuple<Rect, XBrush>>();
var colorRectsRight = new List<Tuple<Rect, XBrush>>();
var valueLeftRects = new List<Tuple<Rect, string>>();
var valueRightRects = new List<Tuple<Rect, string>>();
var center = left + dist / 2.0;
// right part - ask
var prevX = (int)center;
var prevY = (int)GetY((profile.High + .5) * step);
var pointsRight = 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 askWidth = item.Ask > 0 ? (int)(Math.Min(volStep * item.Ask, dist) / 2.0) : 0;
var currX = (int)(center + askWidth);
var currY = (int)GetY((j - .5) * step);
var currHeight = Math.Max(currY - prevY, 1);
if (currY <= prevY && pointsRight.Count > 2)
{
if (currX > prevX)
{
pointsRight[pointsRight.Count - 2] = new Point(currX, pointsRight[pointsRight.Count - 2].Y);
pointsRight[pointsRight.Count - 1] = new Point(currX, pointsRight[pointsRight.Count - 1].Y);
prevX = currX;
}
}
else
{
pointsRight.Add(new Point(currX, prevY));
pointsRight.Add(new Point(currX, currY));
prevX = currX;
}
prevY = currY;
var topY = pointsRight[pointsRight.Count - 2].Y;
if (ShowMaximum && CheckMaximum(item, maxValues))
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight), _maximumBrush));
}
else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
{
colorRects.Add(new Tuple<Rect, XBrush>(new Rect(left, topY, dist, currHeight),
_valueAreaBrush));
}
else if (EnableFilter)
{
if (item.Ask >= minFilter && (maxFilter == 0 || item.Ask <= maxFilter))
{
if (colorRectsRight.Count > 0)
{
var lastRect = colorRectsRight[colorRectsRight.Count - 1].Item1;
if ((int)lastRect.Y == (int)topY)
{
if (askWidth > lastRect.Width)
{
colorRectsRight[colorRectsRight.Count - 1] =
new Tuple<Rect, XBrush>(new Rect(center, topY, askWidth, lastRect.Height),
_filterBrush);
}
}
else
{
colorRectsRight.Add(
new Tuple<Rect, XBrush>(new Rect(center, topY, askWidth, currHeight),
_filterBrush));
}
}
else
{
colorRectsRight.Add(new Tuple<Rect, XBrush>(
new Rect(center, topY, askWidth, currHeight),
_filterBrush));
}
}
}
if (ShowValues && height > 7 && item.Ask > 0)
{
valueRightRects.Add(new Tuple<Rect, string>(new Rect(center + 2, topY, dist / 2.0, height),
symbol.FormatRawSize(item.Ask, roundValues, MinimizeValues)));
}
}
pointsRight.Add(new Point(center, prevY));
// left part - bid
prevX = (int)center;
prevY = (int)GetY((profile.High + .5) * step);
var pointsLeft = 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 bidWidth = (int)(Math.Min(volStep * item.Bid, dist) / 2.0);
var currX = (int)(center - bidWidth);
var currY = (int)GetY((j - .5) * step);
var currHeight = Math.Max(currY - prevY, 1);
if (currY <= prevY && pointsLeft.Count > 2)
{
if (currX < prevX)
{
pointsLeft[pointsLeft.Count - 2] = new Point(currX, pointsLeft[pointsLeft.Count - 2].Y);
pointsLeft[pointsLeft.Count - 1] = new Point(currX, pointsLeft[pointsLeft.Count - 1].Y);
prevX = currX;
}
}
else
{
pointsLeft.Add(new Point(currX, prevY));
pointsLeft.Add(new Point(currX, currY));
prevX = currX;
}
prevY = currY;
var topY = pointsLeft[pointsLeft.Count - 2].Y;
if (ShowMaximum && CheckMaximum(item, maxValues))
{
}
else if (valueArea != null && (item.Price == valueArea.Vah || item.Price == valueArea.Val))
{
}
else if (EnableFilter)
{
if (item.Bid >= minFilter && (maxFilter == 0 || item.Bid <= maxFilter))
{
if (colorRectsLeft.Count > 0)
{
var lastRect = colorRectsLeft[colorRectsLeft.Count - 1].Item1;
if ((int)lastRect.Y == (int)topY)
{
if (bidWidth > lastRect.Width)
{
colorRectsLeft[colorRectsLeft.Count - 1] =
new Tuple<Rect, XBrush>(
new Rect(center - bidWidth, topY, bidWidth, lastRect.Height),
_filterBrush);
}
}
else
{
colorRectsLeft.Add(new Tuple<Rect, XBrush>(
new Rect(center - bidWidth, topY, bidWidth, currHeight), _filterBrush));
}
}
else
{
colorRectsLeft.Add(
new Tuple<Rect, XBrush>(new Rect(center - bidWidth, topY, bidWidth, currHeight),
_filterBrush));
}
}
}
if (ShowValues && height > 7 && item.Bid > 0)
{
valueLeftRects.Add(new Tuple<Rect, string>(new Rect(left, topY, dist / 2.0 - 2, height),
symbol.FormatRawSize(item.Bid, roundValues, MinimizeValues)));
}
}
pointsLeft.Add(new Point(center, prevY));
visual.FillRectangle(_backBrush,
new Rect(new Point(left, pointsLeft[0].Y), new Point(right, prevY)));
visual.FillPolygon(_profileBrush, pointsLeft.ToArray());
visual.FillPolygon(_profileBrush, pointsRight.ToArray());
foreach (var colorRect in colorRectsLeft)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var colorRect in colorRectsRight)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var colorRect in colorRects)
{
visual.FillRectangle(colorRect.Item2, colorRect.Item1);
}
foreach (var valueRect in valueLeftRects)
{
visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1, XTextAlignment.Right);
}
foreach (var valueRect in valueRightRects)
{
visual.DrawString(valueRect.Item2, normalFont, _valuesBrush, valueRect.Item1);
}
}
}
private bool CheckMaximum(IRawClusterItem item, IRawClusterMaxValues maxValues)
{
switch (MaximumType)
{
case VolumeProfilesMaximumType.Volume:
return item.Volume == maxValues.MaxVolume;
case VolumeProfilesMaximumType.Trades:
return item.Trades == maxValues.MaxTrades;
case VolumeProfilesMaximumType.Delta:
return Math.Abs(item.Delta) ==
Math.Max(Math.Abs(maxValues.MaxDelta), Math.Abs(maxValues.MinDelta));
case VolumeProfilesMaximumType.DeltaPlus:
return item.Delta > 0 && item.Delta == maxValues.MaxDelta;
case VolumeProfilesMaximumType.DeltaMinus:
return item.Delta < 0 && item.Delta == maxValues.MinDelta;
case VolumeProfilesMaximumType.Bid:
return item.Bid == maxValues.MaxBid;
case VolumeProfilesMaximumType.Ask:
return item.Ask == maxValues.MaxAsk;
}
return false;
}
public override void CopyTemplate(IndicatorBase indicator, bool style)
{
var i = (VolumeProfilesIndicator)indicator;
PeriodType = i.PeriodType;
PeriodValue = i.PeriodValue;
ProfileType = i.ProfileType;
ProfileProportion = i.ProfileProportion;
ProfileColor = i.ProfileColor;
ProfileBackColor = i.ProfileBackColor;
ShowValues = i.ShowValues;
MinimizeValues = i.MinimizeValues;
ValuesColor = i.ValuesColor;
RoundValuesParam.Copy(i.RoundValuesParam);
MaximumType = i.MaximumType;
ShowMaximum = i.ShowMaximum;
MaximumColor = i.MaximumColor;
ShowValueArea = i.ShowValueArea;
ValueAreaPercent = i.ValueAreaPercent;
ValueAreaColor = i.ValueAreaColor;
EnableFilter = i.EnableFilter;
FilterMin = i.FilterMin;
FilterMax = i.FilterMax;
FilterColor = i.FilterColor;
base.CopyTemplate(indicator, style);
}
private class VolumeProfile
{
public readonly int StartBar;
public int EndBar;
public readonly RawCluster Cluster;
public bool Completed;
public VolumeProfile(RawCluster cluster, int startBar)
{
Cluster = cluster;
StartBar = startBar;
}
}
}
}
Last updated