Code of the Trades Flow indicator
//------------------------------------------------------------------------------ // // TradesFlow. Copyright (c) 2019 Ilya Smirnov. 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); } } }