Trades Flow
//------------------------------------------------------------------------------
//
// Индикатор TradesFlow. Copyright (c) 2023 Tiger Trade Capital AG. All rights reserved.
//
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Windows;
using System.Windows.Media;
using TigerTrade.Chart.Alerts;
using TigerTrade.Chart.Base;
using TigerTrade.Chart.Data;
using TigerTrade.Chart.Indicators.Common;
using TigerTrade.Chart.Indicators.Enums;
using TigerTrade.Dx;
using TigerTrade.Dx.Enums;
namespace TigerTrade.Chart.Indicators.Custom
{
[DataContract(Name = "TradesFlowIndicator", Namespace = "http://schemas.datacontract.org/2004/07/TigerTrade.Chart.Indicators.Custom")]
[Indicator("X_TradesFlow", "*TradesFlow", true, Type = typeof(TradesFlowIndicator))]
internal sealed class TradesFlowIndicator : IndicatorBase
{
internal sealed class TradesFlowItem
{
public DateTime Time { get; set; }
public decimal Price { get; set; }
public decimal Price2 { get; set; }
public decimal Size { get; set; }
public bool IsBuy { get; set; }
public bool Added { get; set; }
public bool Alert { get; set; }
public TradesFlowItem(IChartTick trade)
{
Time = trade.Time;
Price = trade.Price;
Size = trade.Size;
IsBuy = trade.IsBuy;
}
}
internal sealed class TradesFlowList
{
private readonly LinkedList<TradesFlowItem> _ticks = new LinkedList<TradesFlowItem>();
public TradesFlowItem Last { get; private set; }
private bool _aggregate;
private decimal _min;
public void Add(IChartTick tick, bool aggregate, decimal min)
{
if (_aggregate != aggregate || _min != min)
{
_aggregate = aggregate;
_min = min;
Last = null;
_ticks.Clear();
}
if (Last == null)
{
Last = new TradesFlowItem(tick);
}
if (aggregate && Last.Time == tick.Time && Last.IsBuy == tick.IsBuy)
{
Last.Size += tick.Size;
Last.Price2 = tick.Price;
}
else
{
if (Last.Size >= min)
{
if (!Last.Added)
{
_ticks.AddFirst(Last);
}
}
Last = new TradesFlowItem(tick);
}
if (Last != null && Last.Size >= min && !Last.Added)
{
Last.Added = true;
_ticks.AddFirst(Last);
}
if (_ticks.Count > 500)
{
while (_ticks.Count >= 300)
{
_ticks.RemoveLast();
}
}
}
public void Clear()
{
Last = null;
_ticks.Clear();
}
public LinkedList<TradesFlowItem> GetTicks()
{
return _ticks;
}
}
internal sealed class TickData
{
public decimal Size { get; set; }
public bool IsBuy { get; set; }
public bool DrawSize { get; set; }
public bool DrawRect { get; set; }
public double Radius { get; set; }
public Point Center { get; set; }
public Rect Rect { get; set; }
}
private IndicatorDecimalParam _tradesFilterParam;
[DataMember(Name = "TradesFilterParam")]
public IndicatorDecimalParam TradesFilterParam
{
get => _tradesFilterParam ?? (_tradesFilterParam = new IndicatorDecimalParam(0));
set => _tradesFilterParam = value;
}
[DefaultValue(null)]
[Category("Параметры"), DisplayName("Фильтр сделок")]
public decimal TradesFilter
{
get => TradesFilterParam.Get(SettingsShortKey);
set
{
if (!TradesFilterParam.Set(SettingsShortKey, value, 0))
{
return;
}
OnPropertyChanged();
_tradesFlow?.Clear();
}
}
private IndicatorDecimalParam _valuesFilterParam;
[DataMember(Name = "ValuesFilterParam")]
public IndicatorDecimalParam ValuesFilterParam
{
get => _valuesFilterParam ?? (_valuesFilterParam = new IndicatorDecimalParam(5));
set => _valuesFilterParam = value;
}
[DefaultValue(5)]
[Category("Параметры"), DisplayName("Фильтр значений")]
public decimal ValuesFilter
{
get => ValuesFilterParam.Get(SettingsShortKey);
set
{
if (!ValuesFilterParam.Set(SettingsShortKey, value, 0))
{
return;
}
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 bool _сompactMode;
[DataMember(Name = "CompactMode"), DefaultValue(false)]
[Category("Параметры"), DisplayName("Компактный режим")]
public bool CompactMode
{
get => _сompactMode;
set
{
if (value == _сompactMode)
{
return;
}
_сompactMode = value;
OnPropertyChanged();
}
}
private bool _aggregateTicks;
[DataMember(Name = "AggregateTicks"), DefaultValue(false)]
[Category("Параметры"), DisplayName("Агрегировать сделки")]
public bool AggregateTicks
{
get => _aggregateTicks;
set
{
if (value == _aggregateTicks)
{
return;
}
_aggregateTicks = value;
OnPropertyChanged();
_tradesFlow?.Clear();
}
}
private int _width;
[DataMember(Name = "Width"), DefaultValue(30)]
[Category("Параметры"), DisplayName("Ширина (%)")]
public int Width
{
get => _width;
set
{
value = Math.Max(10, Math.Min(100, value));
if (value == _width)
{
return;
}
_width = value;
OnPropertyChanged();
}
}
private int _offset;
[DataMember(Name = "Offset"), DefaultValue(0)]
[Category("Параметры"), DisplayName("Отступ справа")]
public int Offset
{
get => _offset;
set
{
value = Math.Max(0, value);
if (value == _offset)
{
return;
}
_offset = value;
OnPropertyChanged();
}
}
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 IndicatorIntParam _signalFilterParam;
[DataMember(Name = "SignalFilterParam")]
public IndicatorIntParam SignalFilterParam
{
get => _signalFilterParam ?? (_signalFilterParam = new IndicatorIntParam(100));
set => _signalFilterParam = value;
}
[DefaultValue(100)]
[Category("Сигнал"), DisplayName("Фильтр")]
public int SignalFilter
{
get => SignalFilterParam.Get(SettingsShortKey);
set
{
if (!SignalFilterParam.Set(SettingsShortKey, value, 0))
{
return;
}
OnPropertyChanged();
}
}
[Browsable(false)]
public XBrush TickBuyBackBrush { get; private set; }
[Browsable(false)]
public XBrush TickBuyBorderBrush { get; private set; }
[Browsable(false)]
public XPen TickBuyBorderPen { get; private set; }
private XColor _tickBuyBorderColor;
[DataMember(Name = "TickBuyBorderColor")]
[Category("Стиль"), DisplayName("Цвет покупок")]
public XColor TickBuyColor
{
get => _tickBuyBorderColor;
set
{
if (_tickBuyBorderColor == value)
{
return;
}
_tickBuyBorderColor = value;
TickBuyBackBrush = new XBrush(value);
TickBuyBorderBrush = new XBrush(new XColor(255, value));
TickBuyBorderPen = new XPen(TickBuyBorderBrush, 1);
OnPropertyChanged();
}
}
[Browsable(false)]
public XBrush TickSellBackBrush { get; private set; }
[Browsable(false)]
public XBrush TickSellBorderBrush { get; private set; }
[Browsable(false)]
public XPen TickSellBorderPen { get; private set; }
private XColor _tickSellBorderColor;
[DataMember(Name = "TickSellBorderColor")]
[Category("Стиль"), DisplayName("Цвет продаж")]
public XColor TickSellColor
{
get => _tickSellBorderColor;
set
{
if (_tickSellBorderColor == value)
{
return;
}
_tickSellBorderColor = value;
TickSellBackBrush = new XBrush(value);
TickSellBorderBrush = new XBrush(new XColor(255, value));
TickSellBorderPen = new XPen(TickSellBorderBrush, 1);
OnPropertyChanged();
}
}
[Browsable(false)]
public XBrush TicksLineBrush { get; private set; }
private XColor _ticksLineColor;
[DataMember(Name = "TicksLineColor")]
[Category("Стиль"), DisplayName("Цвет линии")]
public XColor TicksLineColor
{
get => _ticksLineColor;
set
{
if (_ticksLineColor == value)
{
return;
}
_ticksLineColor = value;
TicksLineBrush = new XBrush(value);
OnPropertyChanged();
}
}
private int _ticksLineWidth;
[DataMember(Name = "TicksLineWidth"), DefaultValue(1)]
[Category("Стиль"), DisplayName("Толщина линии")]
public int TicksLineWidth
{
get => _ticksLineWidth;
set
{
value = Math.Max(0, Math.Min(10, value));
if (value == _ticksLineWidth)
{
return;
}
_ticksLineWidth = value;
OnPropertyChanged();
}
}
[Browsable(false)]
public override bool ShowIndicatorValues => false;
[Browsable(false)]
public override bool ShowIndicatorLabels => false;
[Browsable(false)]
public override IndicatorCalculation Calculation => IndicatorCalculation.OnEachTick;
private TradesFlowList _tradesFlow;
public TradesFlowIndicator()
{
ShowIndicatorTitle = false;
CompactMode = false;
AggregateTicks = false;
MinimizeValues = false;
Width = 30;
Offset = 0;
TickBuyColor = Color.FromArgb(127, 70, 130, 180);
TickSellColor = Color.FromArgb(127, 178, 34, 34);
TicksLineColor = Color.FromArgb(255, 255, 255, 255);
TicksLineWidth = 1;
}
protected override void Execute()
{
if (_tradesFlow == null)
{
_tradesFlow = new TradesFlowList();
}
if (ClearData)
{
_tradesFlow.Clear();
}
var ticks = DataProvider.GetTicks();
var tradesFilter = TradesFilter;
var signalFilter = SignalFilter;
foreach (var tick in ticks)
{
_tradesFlow.Add(tick, AggregateTicks, tradesFilter);
var last = _tradesFlow.Last;
if (!Alert.IsActive || last.Size < signalFilter || last.Alert)
{
continue;
}
last.Alert = true;
var p = DataProvider.Symbol.FormatPrice(last.Price);
AddAlert(Alert, $"TradesFlow: {(last.IsBuy ? "Buy" : "Sell")} {p} x {last.Size}.");
}
}
public override void Render(DxVisualQueue visual)
{
if (_tradesFlow == null)
{
return;
}
var ticksList = _tradesFlow.GetTicks();
if (ticksList.Count == 0)
{
return;
}
var width = Canvas.Rect.Width / 100.0 * Width;
if (width < 20)
{
return;
}
var start = Canvas.Rect.Right - Offset - 5;
if (start < 20)
{
return;
}
var x = start;
var i = 0;
var tradesListData = new LinkedList<TickData>();
var tradePoints = new Point[ticksList.Count];
var symbol = DataProvider.Symbol;
var valuesFilter = ValuesFilter;
var roundValues = RoundValues;
var minimizeValues = MinimizeValues;
foreach (var tick in ticksList)
{
if (x < start - width)
{
continue;
}
var radius = 3.0;
var drawVolume = false;
if (tick.Size >= valuesFilter)
{
var textSize = Canvas.ChartFont.GetSize(symbol.FormatSize(tick.Size, roundValues, minimizeValues));
if (textSize.Width > textSize.Height)
{
radius = textSize.Width / 2.0;
}
else
{
radius = textSize.Height / 2.0;
}
radius += 4;
drawVolume = true;
}
radius += TicksLineWidth / 2.0;
if (x - 2 * radius - 4 < start - width)
{
x -= radius;
continue;
}
var moveSize = tick.Size >= valuesFilter
? radius + 1
: 1;
x -= moveSize + 1;
var y = GetY(Canvas.IsStock ? (double)tick.Price : 0.5);
var tradeDrawData = new TickData
{
Size = tick.Size,
IsBuy = tick.IsBuy,
DrawSize = drawVolume,
Radius = radius,
Center = new Point(x, y),
Rect = new Rect(x - radius, y - radius, radius * 2, radius * 2),
};
if (tick.Price2 > 0)
{
var top = GetY(Canvas.IsStock ? (double)Math.Max(tick.Price, tick.Price2) : 0.5) - 10;
var bottom = GetY(Canvas.IsStock ? (double)Math.Min(tick.Price, tick.Price2) : 0.5) + 10;
tradeDrawData.Rect = new Rect(x - radius, top, radius * 2, bottom - top);
tradeDrawData.DrawRect = true;
}
tradesListData.AddFirst(tradeDrawData);
tradePoints[i++] = new Point(x, y);
if (!CompactMode)
{
x -= moveSize + TicksLineWidth / 2.0 + 1;
}
}
if (TicksLineWidth > 0 && i > 1)
{
visual.DrawLines(new XPen(TicksLineBrush, TicksLineWidth), tradePoints.Take(i).ToArray());
}
foreach (var trade in tradesListData)
{
if (!trade.DrawRect)
{
visual.FillEllipse(Canvas.Theme.ChartBackBrush, trade.Center, trade.Radius, trade.Radius);
visual.FillEllipse(
trade.IsBuy
? TickBuyBackBrush
: TickSellBackBrush, trade.Center,
trade.Radius, trade.Radius);
visual.DrawEllipse(trade.IsBuy
? TickBuyBorderPen
: TickSellBorderPen,
trade.Center, trade.Radius, trade.Radius
);
}
else
{
visual.FillRectangle(Canvas.Theme.ChartBackBrush, trade.Rect);
visual.FillRectangle(trade.IsBuy ? TickBuyBackBrush : TickSellBackBrush, trade.Rect);
visual.DrawRectangle(trade.IsBuy ? TickBuyBorderPen : TickSellBorderPen, trade.Rect);
}
if (trade.DrawSize)
{
visual.DrawString(symbol.FormatSize(trade.Size, roundValues, minimizeValues), Canvas.ChartFont,
Canvas.Theme.ChartFontBrush, trade.Rect, XTextAlignment.Center);
}
}
}
public override void ApplyColors(IChartTheme theme)
{
TickBuyColor = new XColor(127, theme.PaletteColor6);
TickSellColor = new XColor(127, theme.PaletteColor7);
base.ApplyColors(theme);
}
public override void CopyTemplate(IndicatorBase indicator, bool style)
{
var i = (TradesFlowIndicator)indicator;
CompactMode = i.CompactMode;
AggregateTicks = i.AggregateTicks;
Alert.Copy(i.Alert, !style);
OnPropertyChanged(nameof(Alert));
TradesFilterParam.Copy(i.TradesFilterParam);
ValuesFilterParam.Copy(i.ValuesFilterParam);
RoundValuesParam.Copy(i.RoundValuesParam);
SignalFilterParam.Copy(i.SignalFilterParam);
Width = i.Width;
Offset = i.Offset;
TickBuyColor = i.TickBuyColor;
TickSellColor = i.TickSellColor;
TicksLineColor = i.TicksLineColor;
TicksLineWidth = i.TicksLineWidth;
OnPropertyChanged(nameof(TradesFilter));
OnPropertyChanged(nameof(ValuesFilter));
OnPropertyChanged(nameof(SignalFilter));
base.CopyTemplate(indicator, style);
}
}
}
Last updated