diff --git a/Indicators/IndicatorATRTrailingStopWithFibRetracements.cs b/Indicators/IndicatorATRTrailingStopWithFibRetracements.cs new file mode 100644 index 0000000..a33152a --- /dev/null +++ b/Indicators/IndicatorATRTrailingStopWithFibRetracements.cs @@ -0,0 +1,127 @@ +// Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. + +using System; +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace Volatility; + +public class IndicatorATRTrailingStopWithFibRetracements : Indicator +{ + [InputParameter("ATR Period", 0, 1, 9999)] + public int Period = 5; + [InputParameter("ATR Factor", 0, 0.1, 999, 0.1, 1)] + public double ATRFactor = 3.5; + [InputParameter("Fib1", 0, 0.1, 999, 0.1, 1)] + public double Fib1Lvl = 61.8; + [InputParameter("Fib2", 0, 0.1, 999, 0.1, 1)] + public double Fib2Lvl = 78.6; + [InputParameter("Fib3", 0, 0.1, 999, 0.1, 1)] + public double Fib3Lvl = 88.6; + [InputParameter("Type of Moving Average", 3, variants: new object[] { + "Simple", MaMode.SMA, + "Exponential", MaMode.EMA, + "Smoothed Modified", MaMode.SMMA, + "Linear Weighted", MaMode.LWMA})] + public MaMode MaType = MaMode.SMA; + + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorATRTrailingStopWithFibRetracements.cs"; + + + public double fibLvl1 = 1.618, fibLvl2 = 2.618, fibLvl3 = 4.23, fib1, fib2, fib3, fibTgt1, fibTgt2, fibTgt3; + public double trailStop, trend, trendUp, trendDown, ex, currClose, switchVal, diff, loss; + public bool trendChange; + private Indicator atr; + public override string ShortName => $"ATRTrailStop ({this.Period})"; + public IndicatorATRTrailingStopWithFibRetracements() + : base() + { + // Defines indicator's name and description. + Name = "ATR Trailing w/Fib Targets"; + Description = "Fib Targets on the ATR trailing stop"; + + // Defines line on demand with particular parameters. + AddLineSeries("TrailStop", Color.White, 1, LineStyle.Solid); + AddLineSeries("fibTgtDown1", Color.Red, 1, LineStyle.Solid); + AddLineSeries("fibTgtDown2", Color.Red, 1, LineStyle.Solid); + AddLineSeries("fibTgtDown3", Color.Red, 1, LineStyle.Solid); + AddLineSeries("fibTgtUp1", Color.Green, 1, LineStyle.Solid); + AddLineSeries("fibTgtUp2", Color.Green, 1, LineStyle.Solid); + AddLineSeries("fibTgtUp3", Color.Green, 1, LineStyle.Solid); + AddLineSeries("fib1", Color.Gray, 1, LineStyle.Solid); + AddLineSeries("fib2", Color.Gray, 1, LineStyle.Solid); + AddLineSeries("fib3", Color.Gray, 1, LineStyle.Solid); + // By default indicator will be applied on main window of the chart + SeparateWindow = false; + } + + protected override void OnInit() + { + this.atr = Core.Indicators.BuiltIn.ATR(this.Period, this.MaType, Indicator.DEFAULT_CALCULATION_TYPE); + this.AddIndicator(this.atr); + } + + protected override void OnUpdate(UpdateArgs args) + { + double prevClose = currClose; + + currClose = GetPrice(PriceType.Close); + double currHigh = GetPrice(PriceType.High); + double currLow = GetPrice(PriceType.Low); + + loss = ATRFactor * atr.GetValue(); + double up = currClose - loss; + double down = currClose + loss; + + double prevTrend = trend; + double prevTrendUp = trendUp; + double prevTrendDown = trendDown; + + trendUp = prevClose > prevTrendUp ? Math.Max(up, prevTrendUp) : up; + trendDown = prevClose < prevTrendDown ? Math.Min(down, prevTrendDown) : down; + trend = currClose > prevTrendDown ? 1 : currClose < prevTrendUp ? -1 : prevTrend; + + trailStop = trend == 1 ? trendUp : trendDown; + + double prevEx = ex; + ex = (trend > 0 && prevTrend < 0) ? currHigh : (trend < 0 && prevTrend > 0) ? currLow : trend == 1 ? Math.Max(prevEx, currHigh) : trend == -1 ? Math.Min(prevEx, currLow) : prevEx; + + fib1 = ex + (trailStop - ex) * Fib1Lvl / 100; + fib2 = ex + (trailStop - ex) * Fib2Lvl / 100; + fib3 = ex + (trailStop - ex) * Fib3Lvl / 100; + + double prevSwitchVal = switchVal; + double prevDiff = diff; + trendChange = trend == prevTrend ? false : true; + switchVal = trendChange ? trailStop : prevSwitchVal; + diff = trendChange ? currClose - switchVal : prevDiff; + + fibTgt1 = switchVal + (diff * fibLvl1); + fibTgt2 = switchVal + (diff * fibLvl2); + fibTgt3 = switchVal + (diff * fibLvl3); + double prevfibTgt1 = fibTgt1; + + SetValue(trailStop); + if (trend == 1 && fibTgt1 == prevfibTgt1) + { + if (fibTgt1 > trailStop) + SetValue(fibTgt1, 4); + if (fibTgt2 > trailStop) + SetValue(fibTgt2, 5); + if (fibTgt3 > trailStop) + SetValue(fibTgt3, 6); + } + if (trend == -1 && fibTgt1 == prevfibTgt1) + { + if (fibTgt1 < trailStop) + SetValue(fibTgt1, 1); + if (fibTgt2 < trailStop) + SetValue(fibTgt2, 2); + if (fibTgt3 < trailStop) + SetValue(fibTgt3, 3); + } + SetValue(fib1, 7); + SetValue(fib2, 8); + SetValue(fib3, 9); + } +} \ No newline at end of file diff --git a/Indicators/IndicatorAutoTrendLine.cs b/Indicators/IndicatorAutoTrendLine.cs new file mode 100644 index 0000000..61f59a7 --- /dev/null +++ b/Indicators/IndicatorAutoTrendLine.cs @@ -0,0 +1,209 @@ +// Copyright QUANTOWER LLC. © 2017-2023. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Numerics; +using TradingPlatform.BusinessLayer; +using TradingPlatform.BusinessLayer.Chart; +using TradingPlatform.BusinessLayer.Utils; + +namespace Trend +{ + public class IndicatorAutoTrendLine : Indicator + { + public int Period = 20; + public int linesCount = 4; + + public LineOptions TopOptions + { + get => CreateLineOptions(this.TopPen); + set => ApplyLineOptions(this.TopPen, value); + } + + public LineOptions BottomOptions + { + get => CreateLineOptions(this.BottomPen); + set => ApplyLineOptions(this.BottomPen, value); + } + + private readonly Pen TopPen = new Pen(Color.Red); + private readonly Pen BottomPen = new Pen(Color.Green); + + private readonly List pivots = new(); + + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorAutoTrendLine.cs"; + public IndicatorAutoTrendLine() : base() + { + this.Name = "Auto Trend Line"; + this.SeparateWindow = false; + } + + protected override void OnInit() => this.pivots.Clear(); + + protected override void OnUpdate(UpdateArgs args) + { + if (this.Count < this.Period * 2 + 1) return; + + int currBarIndex = this.Count - this.Period - 1; + IndicatorAutoTrendLineMaxOrMin maxOrMin = this.LocalMaxOrMin(currBarIndex); + + if (maxOrMin != IndicatorAutoTrendLineMaxOrMin.Nothing && (this.pivots.Count == 0 || this.pivots[0].index != currBarIndex)) + { + var pivot = new IndicatorAutoTrendLinePivotPoint(maxOrMin, currBarIndex); + var bar = (HistoryItemBar)this.HistoricalData[currBarIndex, SeekOriginHistory.Begin]; + pivot.value = (maxOrMin == IndicatorAutoTrendLineMaxOrMin.LocalMaximum) ? bar.High : bar.Low; + this.pivots.Insert(0, pivot); + } + } + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + if (this.CurrentChart == null || this.HistoricalData.Aggregation is HistoryAggregationTick) return; + + var gr = args.Graphics; + var currWindow = this.CurrentChart.Windows[args.WindowIndex]; + var prevClip = gr.ClipBounds; + gr.SetClip(args.Rectangle); + + try + { + int drawnLines = 0; + + for (int i = 0; i < this.pivots.Count - 1 && drawnLines < this.linesCount; i++) + { + var endPivot = this.pivots[i]; + var endPoint = this.GetPoint(currWindow, endPivot); + + for (int j = i + 1; j < this.pivots.Count; j++) + { + var startPivot = this.pivots[j]; + if (endPivot.MaxOrMin != startPivot.MaxOrMin || + (endPivot.MaxOrMin == IndicatorAutoTrendLineMaxOrMin.LocalMaximum && startPivot.value <= endPivot.value) || + (endPivot.MaxOrMin == IndicatorAutoTrendLineMaxOrMin.LocalMinimum && startPivot.value >= endPivot.value)) + continue; + + var startPoint = this.GetPoint(currWindow, startPivot); + Pen pen = endPivot.MaxOrMin == IndicatorAutoTrendLineMaxOrMin.LocalMaximum ? this.TopPen : this.BottomPen; + + PointF extendedEnd = this.ExtendLine(endPoint, startPoint, currWindow.ClientRectangle); + gr.DrawLine(pen, startPoint, extendedEnd); + drawnLines++; + break; + } + } + } + finally + { + gr.SetClip(prevClip); + } + } + + private PointF GetPoint(IChartWindow window, IndicatorAutoTrendLinePivotPoint pivot) + { + float x = (float)window.CoordinatesConverter.GetChartX(this.HistoricalData[pivot.index, SeekOriginHistory.Begin].TimeLeft) + this.CurrentChart.BarsWidth / 2; + float y = (float)window.CoordinatesConverter.GetChartY(pivot.value); + return new PointF(x, y); + } + + private PointF ExtendLine(PointF start, PointF end, RectangleF bounds) + { + PointF result = new PointF(bounds.Width, end.Y); + float k = (end.Y - start.Y) / (end.X - start.X); + float b = start.Y - k * start.X; + result.Y = k * result.X + b; + return result; + } + + private IndicatorAutoTrendLineMaxOrMin LocalMaxOrMin(int index) + { + IndicatorAutoTrendLineMaxOrMin result = IndicatorAutoTrendLineMaxOrMin.Nothing; + var current = (HistoryItemBar)this.HistoricalData[index, SeekOriginHistory.Begin]; + + for (int i = 1; i <= this.Period; i++) + { + var left = (HistoryItemBar)this.HistoricalData[index - i, SeekOriginHistory.Begin]; + var right = (HistoryItemBar)this.HistoricalData[index + i, SeekOriginHistory.Begin]; + + if (left.High <= current.High && right.High <= current.High) + result = result == IndicatorAutoTrendLineMaxOrMin.LocalMinimum ? IndicatorAutoTrendLineMaxOrMin.Nothing : IndicatorAutoTrendLineMaxOrMin.LocalMaximum; + else if (left.Low >= current.Low && right.Low >= current.Low) + result = result == IndicatorAutoTrendLineMaxOrMin.LocalMaximum ? IndicatorAutoTrendLineMaxOrMin.Nothing : IndicatorAutoTrendLineMaxOrMin.LocalMinimum; + else + return IndicatorAutoTrendLineMaxOrMin.Nothing; + } + + return result; + } + + public override IList Settings + { + get + { + var settings = base.Settings; + settings.Add(new SettingItemInteger("Period", this.Period) { Text = "Period", SortIndex = 1, Minimum = 2 }); + settings.Add(new SettingItemInteger("linesCount", this.linesCount) { Text = "Max Lines Count", SortIndex = 1, Minimum = 2 }); + settings.Add(new SettingItemLineOptions("TopOptions", this.TopOptions) + { + Text = "Resistance Line Style", + SortIndex = 2, + ExcludedStyles = new[] { LineStyle.Histogramm, LineStyle.Points, LineStyle.Columns, LineStyle.StepLine }, + UseEnabilityToggler = false + }); + settings.Add(new SettingItemLineOptions("BottomOptions", this.BottomOptions) + { + Text = "Support Line Style", + SortIndex = 2, + ExcludedStyles = new[] { LineStyle.Histogramm, LineStyle.Points, LineStyle.Columns, LineStyle.StepLine }, + UseEnabilityToggler = false + }); + return settings; + } + set + { + base.Settings = value; + if (value.TryGetValue("Period", out int p)) this.Period = p; + if (value.TryGetValue("linesCount", out int l)) this.linesCount = l; + if (value.TryGetValue("TopOptions", out LineOptions t)) this.TopOptions = t; + if (value.TryGetValue("BottomOptions", out LineOptions b)) this.BottomOptions = b; + this.OnSettingsUpdated(); + } + } + + private static LineOptions CreateLineOptions(Pen pen) => new() + { + Color = pen.Color, + Width = (int)pen.Width, + LineStyle = (LineStyle)pen.DashStyle, + WithCheckBox = false + }; + + private static void ApplyLineOptions(Pen pen, LineOptions options) + { + pen.Color = options.Color; + pen.Width = options.Width; + pen.DashStyle = (DashStyle)options.LineStyle; + } + } + + internal enum IndicatorAutoTrendLineMaxOrMin + { + LocalMaximum, + LocalMinimum, + Nothing + } + + internal class IndicatorAutoTrendLinePivotPoint + { + public IndicatorAutoTrendLineMaxOrMin MaxOrMin { get; set; } + public int index { get; set; } + public double value { get; set; } + public IndicatorAutoTrendLinePivotPoint(IndicatorAutoTrendLineMaxOrMin maxOrMin, int index) + { + this.MaxOrMin = maxOrMin; + this.index = index; + } + } +} \ No newline at end of file diff --git a/Indicators/IndicatorBarCounter.cs b/Indicators/IndicatorBarCounter.cs index 559ee8b..3f2bdc4 100644 --- a/Indicators/IndicatorBarCounter.cs +++ b/Indicators/IndicatorBarCounter.cs @@ -52,14 +52,8 @@ public override void OnPaintChart(PaintChartEventArgs args) return; } var mainWindow = this.CurrentChart.MainWindow; - DateTime leftBorderTime = mainWindow.CoordinatesConverter.GetTime(0); - DateTime rightBorderTime = mainWindow.CoordinatesConverter.GetTime(mainWindow.ClientRectangle.Width); - int leftIndex = (int)mainWindow.CoordinatesConverter.GetBarIndex(leftBorderTime); - int rightIndex = (int)mainWindow.CoordinatesConverter.GetBarIndex(rightBorderTime); - if (leftIndex < 0) - leftIndex = 0; - if (rightIndex >= this.HistoricalData.Count) - rightIndex = this.HistoricalData.Count - 1; + int leftIndex = args.LeftVisibleBarIndex; + int rightIndex = args.RightVisibleBarIndex; Brush labelBrush = new SolidBrush(labelColor); leftIndex = this.HistoricalData.Count - leftIndex - 1; rightIndex = this.HistoricalData.Count - rightIndex - 1; diff --git a/Indicators/IndicatorCommodityChannelIndex.cs b/Indicators/IndicatorCommodityChannelIndex.cs index 3dabb95..c20a0c4 100644 --- a/Indicators/IndicatorCommodityChannelIndex.cs +++ b/Indicators/IndicatorCommodityChannelIndex.cs @@ -24,7 +24,7 @@ public sealed class IndicatorCommodityChannelIndex : Indicator, IWatchlistIndica "Volume", PriceType.Volume, "Open interest", PriceType.OpenInterest })] - public PriceType SourcePrice = PriceType.Close; + public PriceType SourcePrice = PriceType.Typical; // Displays Input Parameter as dropdown list. [InputParameter("Type of Moving Average", 30, variants: new object[] { @@ -93,14 +93,19 @@ protected override void OnUpdate(UpdateArgs args) if (this.Count < this.MinHistoryDepths) return; - double val = this.MA.GetValue(); - double d = 0; - // Processes calculating loop. + double meanDeviation = 0; + double sma = this.MA.GetValue(0); for (int i = 0; i < this.Period; i++) - d += Math.Abs(this.GetPrice(this.SourcePrice, i) - val); + { + double tp = this.GetPrice(this.SourcePrice, i); + meanDeviation += Math.Abs(tp - sma); + } - d = 0.015 * (d / this.Period); - // Setting the value - this.SetValue((this.GetPrice(this.SourcePrice) - val) / d); + meanDeviation = 0.015 * (meanDeviation / this.Period); + + double currentTP = this.GetPrice(this.SourcePrice); + double currentSMA = this.MA.GetValue(); + + this.SetValue((currentTP - currentSMA) / meanDeviation); } } \ No newline at end of file diff --git a/Indicators/IndicatorCumulativeDelta.cs b/Indicators/IndicatorCumulativeDelta.cs index 066e85d..c34a006 100644 --- a/Indicators/IndicatorCumulativeDelta.cs +++ b/Indicators/IndicatorCumulativeDelta.cs @@ -30,6 +30,11 @@ public class IndicatorCumulativeDelta : IndicatorCandleDrawBase, IVolumeAnalysis private const string SPECIFIED_SESSION_TYPE = "Specified session"; private const string CUSTOM_RANGE_SESSION_TYPE = "Custom range"; + private const string LINE_COLORS_SI = "ColorLines"; + private const string CLOSE_LINE_COLOR_BY_SI = "CloseLineColorBy"; + private const string MA_LINE_COLORS_SI = "MAColorLines"; + private const string MA_LINE_COLOR_BY_SI = "MALineColorBy"; + #endregion Consts #region Parameters @@ -50,6 +55,21 @@ public class IndicatorCumulativeDelta : IndicatorCandleDrawBase, IVolumeAnalysis })] public CumulativeDeltaSessionMode SessionMode; + [InputParameter("Period of Moving Average", 31, 1, 9999, 1, 1)] + public int MAPeriod = 20; + + [InputParameter("Average Type", 32, variants: new object[]{ + "Simple Moving Average", MaMode.SMA, + "Exponential Moving Average", MaMode.EMA, + "Smoothed Moving Average", MaMode.SMMA, + "Linearly Weighted Moving Average", MaMode.LWMA, + })] + public MaMode MaType = MaMode.SMA; + + private MALineColorOption maLineColorOption; + + private CloseLineColorOption closeLineСoloringOption; + public ISessionsContainer SessionContainer { get @@ -130,16 +150,31 @@ public override string ShortName private AreaBuilder currentAreaBuider; + private IntervalGenerator intervalGenerator; + + private Color upLineColor; + private Color downLineColor; + + private Color maUpLineColor; + private Color maDownLineColor; + + private Indicator ma; #endregion Parameters public IndicatorCumulativeDelta() : base() { + this.AddLineSeries("MA", Color.Red, 2, LineStyle.Solid); + + this.upLineColor = Color.FromArgb(0, 178, 89); + this.downLineColor = Color.FromArgb(251, 87, 87); + + this.maUpLineColor = Color.Green; + this.maDownLineColor = Color.Red; + this.maLineColorOption = MALineColorOption.PriceCross; + this.closeLineСoloringOption = CloseLineColorOption.Delta; + this.Name = "Cumulative delta"; - this.LinesSeries[0].Name = "Cumulative open"; - this.LinesSeries[1].Name = "Cumulative high"; - this.LinesSeries[2].Name = "Cumulative low"; - this.LinesSeries[3].Name = "Cumulative close"; this.AddLineLevel(0d, "Zero line", Color.Gray, 1, LineStyle.DashDot); @@ -177,7 +212,11 @@ protected override void OnInit() } } base.OnInit(); + + this.ma = Core.Indicators.BuiltIn.MA(this.MAPeriod, PriceType.Close, this.MaType); + this.CandleHistoricalData.AddIndicator(this.ma); } + protected override void OnUpdate(UpdateArgs args) { if (this.IsLoading) @@ -234,6 +273,25 @@ public override IList Settings var separ = settings.FirstOrDefault()?.SeparatorGroup; + var lineRelationVisibility = new SettingItemRelationVisibility("VisualStyle", new SelectItem("", (int)CandleDrawIndicatorVisualMode.Lines)); + var closeLineColorOptions = new List() + { + new SelectItem(loc._("By Delta"), CloseLineColorOption.Delta), + new SelectItem(loc._("By Sign"), CloseLineColorOption.Sign), + }; + settings.Add(new SettingItemSelectorLocalized(CLOSE_LINE_COLOR_BY_SI, closeLineColorOptions.GetItemByValue(this.closeLineСoloringOption), closeLineColorOptions, 20) + { + Text = loc._("Coloring mode"), + SeparatorGroup = separ, + Relation = lineRelationVisibility + }); + settings.Add(new SettingItemPairColor(LINE_COLORS_SI, new PairColor(this.upLineColor, this.downLineColor, loc._("Up"), loc._("Down")), 20) + { + Text = loc._("Lines"), + SeparatorGroup = separ, + Relation = lineRelationVisibility + }); + // // // @@ -279,6 +337,25 @@ public override IList Settings Relation = new SettingItemRelationVisibility(RESET_TYPE_NAME_SI, new SelectItem("", (int)CumulativeDeltaSessionMode.ByPeriod)) }); + var lineColorOptions = new List() + { + new SelectItem(loc._("Price Cross"), MALineColorOption.PriceCross), + new SelectItem(loc._("Value Change (Up/Down)"), MALineColorOption.ValueChange), + new SelectItem(loc._("Solid Color"), MALineColorOption.SolidColor) + }; + settings.Add(new SettingItemSelectorLocalized(MA_LINE_COLOR_BY_SI, lineColorOptions.GetItemByValue(this.maLineColorOption), lineColorOptions, 40) + { + Text = loc._("Color by"), + SeparatorGroup = separ + }); + + settings.Add(new SettingItemPairColor(MA_LINE_COLORS_SI, new PairColor(this.maUpLineColor, this.maDownLineColor, loc._("Up"), loc._("Down")), 40) + { + Text = loc._("Lines"), + SeparatorGroup = separ, + Relation = new SettingItemRelationVisibility(MA_LINE_COLOR_BY_SI, new object[] { lineColorOptions[0], lineColorOptions[1] }) + }); + return settings; } set @@ -333,6 +410,52 @@ public override IList Settings } } + if (holder.TryGetValue(LINE_COLORS_SI, out item)) + { + var newValue = item.GetValue(); + + if (this.upLineColor != newValue.Color1 || this.downLineColor != newValue.Color2) + { + this.upLineColor = newValue.Color1; + this.downLineColor = newValue.Color2; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + + if (holder.TryGetValue(MA_LINE_COLOR_BY_SI, out item)) + { + var newValue = (MALineColorOption)((SelectItem)item.Value).Value; + + if (this.maLineColorOption != newValue) + { + this.maLineColorOption = newValue; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + + if (holder.TryGetValue(MA_LINE_COLORS_SI, out item)) + { + var newValue = item.GetValue(); + + if (this.upLineColor != newValue.Color1 || this.downLineColor != newValue.Color2) + { + this.maUpLineColor = newValue.Color1; + this.maDownLineColor = newValue.Color2; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + + if (holder.TryGetValue(CLOSE_LINE_COLOR_BY_SI, out item)) + { + var newValue = (CloseLineColorOption)((SelectItem)item.Value).Value; + + if (this.closeLineСoloringOption != newValue) + { + this.closeLineСoloringOption = newValue; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + // if (needRefresh) this.Refresh(); @@ -357,7 +480,7 @@ private void CalculateIndicatorByOffset(int offset, bool isNewBar, bool createAf // if (this.SessionMode == CumulativeDeltaSessionMode.SpecifiedSession || this.SessionMode == CumulativeDeltaSessionMode.CustomRange) { - if (!this.SessionContainer.ContainsDate(time.Ticks)) + if (!this.SessionContainer.ContainsDate(time)) { this.currentAreaBuider?.Reset(index); return; @@ -370,19 +493,27 @@ private void CalculateIndicatorByOffset(int offset, bool isNewBar, bool createAf if (this.currentAreaBuider == null) { var period = this.GetStepPeriod(); - var range = this.SessionMode != CumulativeDeltaSessionMode.FullHistory - ? HistoryStepsCalculator.GetSteps(time, time.AddTicks(period.Ticks), period.BasePeriod, period.PeriodMultiplier, this.SessionContainer, this.GetTimeZone()).FirstOrDefault() - : new Interval(time, DateTime.MaxValue); + + Interval range; + + if (this.SessionMode == CumulativeDeltaSessionMode.FullHistory) + range = new Interval(time, DateTime.MaxValue); + else + { + this.intervalGenerator = new IntervalGenerator(time, period, this.SessionContainer, this.GetTimeZone()); + range = this.intervalGenerator.Current; + } if (range.IsEmpty) return; this.currentAreaBuider = this.CreateAreaBuilder(range); } - else if (!this.currentAreaBuider.Contains(time)) + else if (this.intervalGenerator != null && !this.currentAreaBuider.Contains(time)) { - var period = this.GetStepPeriod(); - var range = HistoryStepsCalculator.GetNextStep(this.currentAreaBuider.Range.To, period.BasePeriod, period.PeriodMultiplier); + this.intervalGenerator.MoveUntil(time); + + var range = this.intervalGenerator.Current; if (range.IsEmpty) return; @@ -405,10 +536,44 @@ private void CalculateIndicatorByOffset(int offset, bool isNewBar, bool createAf this.SetValues(this.currentAreaBuider.Bar.Open, this.currentAreaBuider.Bar.High, this.currentAreaBuider.Bar.Low, this.currentAreaBuider.Bar.Close, offset); + bool isUpColor = this.closeLineСoloringOption switch + { + CloseLineColorOption.Sign => this.LinesSeries[1].GetValue(offset) > 0, + CloseLineColorOption.Delta => (this.DeltaSourceType == CumulativeDeltaSourceType.Volume && currentItem.Total.Delta > 0) || + (this.DeltaSourceType == CumulativeDeltaSourceType.Trades && (currentItem.Total.BuyTrades - currentItem.Total.SellTrades) > 0), + + _ => true, + }; + this.LinesSeries[1].SetMarker(offset, isUpColor ? this.upLineColor : this.downLineColor); + + if (this.Count > offset && this.Count > this.MAPeriod) + { + switch (this.maLineColorOption) + { + case MALineColorOption.PriceCross: + this.LinesSeries[2].SetMarker(offset, this.LinesSeries[1].GetValue(offset) > this.LinesSeries[2].GetValue(offset) ? this.maUpLineColor : this.maDownLineColor); + break; + case MALineColorOption.ValueChange: + this.LinesSeries[2].SetMarker(offset, this.LinesSeries[2].GetValue(offset) > this.LinesSeries[2].GetValue(offset + 1) ? this.maUpLineColor : this.maDownLineColor); + break; + } + } + if (isNewBar && createAfterUpdate) this.currentAreaBuider.StartNew(++index); } + protected override void SetValues(double open, double high, double low, double close, int offset) + { + if (!IsValidPrice(open) || !IsValidPrice(close)) + return; + + base.SetValues(open, high, low, close, offset); + + if (this.Count > offset && this.Count > this.MAPeriod) + this.SetValue(this.ma.GetValue(offset), 2, offset); + } + private Interval GetFullDayTimeInterval(TradingPlatform.BusinessLayer.TimeZone timeZone) { var openTime = new DateTime(DateTime.UtcNow.Date.Ticks, DateTimeKind.Unspecified); @@ -438,7 +603,6 @@ private CustomSession CreateCustomSession(TimeSpan open, TimeSpan close, TimeZon Days = Enum.GetValues(typeof(DayOfWeek)).Cast().ToArray(), Type = SessionType.Main }; - session.RecalculateOpenCloseTime(info); return session; } private AreaBuilder CreateAreaBuilder(Interval range) @@ -458,14 +622,7 @@ private AreaBuilder CreateAreaBuilder(Interval range) bool IVolumeAnalysisIndicator.IsRequirePriceLevelsCalculation => false; public void VolumeAnalysisData_Loaded() { - if (this.currentAreaBuider != null) - { - this.currentAreaBuider.Dispose(); - this.currentAreaBuider = null; - } - - for (int i = this.Count - 1; i >= 0; i--) - this.CalculateIndicatorByOffset(i, true); + this.Refresh(); this.IsLoading = false; } @@ -487,6 +644,19 @@ public enum CumulativeDeltaSourceType Trades } + public enum MALineColorOption + { + PriceCross, + ValueChange, + SolidColor + } + + public enum CloseLineColorOption + { + Delta, + Sign + } + abstract class AreaBuilder : IDisposable { internal Interval Range { get; } @@ -513,16 +683,7 @@ internal void StartNew(int barIndex) this.Bar.Open = prevClose; this.BarIndex = barIndex; } - internal bool Contains(DateTime dt) - { - if (dt.CompareTo(this.Range.Min) < 0) - return false; - - if (dt.CompareTo(this.Range.Max) >= 0) - return false; - - return true; - } + internal bool Contains(DateTime dt) => this.Range.Contains(dt); internal void Reset(int barIndex) { this.Bar.Clear(); diff --git a/Indicators/IndicatorDailyOHLC.cs b/Indicators/IndicatorDailyOHLC.cs index b5d57cc..8e09c02 100644 --- a/Indicators/IndicatorDailyOHLC.cs +++ b/Indicators/IndicatorDailyOHLC.cs @@ -73,6 +73,8 @@ public DateTime ExtendRangeEndTime public NativeAlignment LabelAlignment { get; set; } public int labelFormat { get; set; } public int labelPosition { get; set; } + public int LabelHorizontalMode { get; set; } = 0; + public int LastLabelsCount { get; set; } = 1; public bool ShowLabel { get; private set; } public LineOptions OpenLineOptions { @@ -425,6 +427,13 @@ public override IList Settings var belowTL = new SelectItem("Below the line", 0); var aboveTL = new SelectItem("Above the line", 1); + var centerTL = new SelectItem("Center on the line", 2); + + var labRight = new SelectItem("Right", 0); + var labLeft = new SelectItem("Left", 1); + var labCentered = new SelectItem("Centered", 2); + var labRightEdge = new SelectItem("Right Edge", 3); + var labPriceScale = new SelectItem("Price Scale", 4); var formatPrice = new SelectItem("Price", 0); var formatTextPrice = new SelectItem("Text and Price", 1); @@ -513,16 +522,28 @@ public override IList Settings Text = loc._("Font"), Relation = new SettingItemRelationVisibility("ShowLabel", true) }); - settings.Add(new SettingItemAlignment("Label alignment", this.LabelAlignment, 60) + settings.Add(new SettingItemSelectorLocalized( + "Label alignment", // имя ключа остаётся прежним + new SelectItem("Label alignment", this.LabelHorizontalMode), + new List { labRight, labLeft, labCentered, labRightEdge, labPriceScale }) { - Text = loc._("Label alignment"), SeparatorGroup = defaultSeparator, + Text = loc._("Label alignment"), + SortIndex = 60, Relation = new SettingItemRelationVisibility("ShowLabel", true) }); + settings.Add(new SettingItemInteger("Last labels count", this.LastLabelsCount, 61) + { + SeparatorGroup = defaultSeparator, + Text = loc._("Last labels count"), + // показываем только когда выбран Right Edge или Price Scale + Relation = new SettingItemRelationVisibility("Label alignment", labRightEdge, labPriceScale) + }); settings.Add(new SettingItemSelectorLocalized("Label position", new SelectItem("Label position", this.labelPosition), new List { belowTL, - aboveTL + aboveTL, + centerTL }) { SeparatorGroup = defaultSeparator, @@ -755,12 +776,14 @@ public override IList Settings } if (holder.TryGetValue("Font", out item) && item.Value is Font font) this.CurrentFont = font; - if (holder.TryGetValue("Label alignment", out item) && item.Value is NativeAlignment labelAlignment) - this.LabelAlignment = labelAlignment; + if (holder.TryGetValue("Label alignment", out var laItem) && laItem.GetValue() != this.LabelHorizontalMode) + this.LabelHorizontalMode = laItem.GetValue(); if (holder.TryGetValue("ShowLabel", out item) && item.Value is bool showLabel) this.ShowLabel = showLabel; if (holder.TryGetValue("Label position", out var lpitem)&& lpitem.GetValue() != this.labelPosition) this.labelPosition = lpitem.GetValue(); + if (holder.TryGetValue("Last labels count", out item) && item.Value is int lastLabels) + this.LastLabelsCount = lastLabels; if (holder.TryGetValue("Format", out var lfitem)&& lfitem.GetValue() != this.labelFormat) this.labelFormat = lfitem.GetValue(); if (needRefresh) @@ -834,7 +857,9 @@ public override void OnPaintChart(PaintChartEventArgs args) // get previous date if (needUsePreviosRange) range = this.rangeCache[prevDailyRangeOffset]; - + bool restrictLabels = this.LabelHorizontalMode == 3 || this.LabelHorizontalMode == 4; + int maxLabelDays = restrictLabels ? Math.Max(0, Math.Min(this.LastLabelsCount, this.DaysCount)) : int.MaxValue; + bool allowLabelForThisDay = i < maxLabelDays; if (this.HighLineOptions.Enabled || this.HighExtendLineOptions.Enabled) { float highY = (float)currentWindow.CoordinatesConverter.GetChartY(range.High); @@ -845,7 +870,10 @@ public override void OnPaintChart(PaintChartEventArgs args) gr.DrawLine(this.highLinePen, leftX, highY, rightX, highY); if (this.ShowHighLineLabel) - this.DrawBillet(gr, range.High, ref leftX, ref rightX, ref highY, this.CurrentFont, this.highLineOptions, this.highLinePen, this.centerNearSF, this.LabelAlignment, HighCustomText); + if (this.ShowHighLineLabel) + this.DrawBillet(gr, range.High, ref leftX, ref rightX, ref highY, + this.CurrentFont, this.highLineOptions, this.highLinePen, this.centerNearSF, args.Rectangle, HighCustomText); + } if (needDrawExtendLines && this.HighExtendLineOptions.Enabled) @@ -862,8 +890,10 @@ public override void OnPaintChart(PaintChartEventArgs args) { gr.DrawLine(this.lowLinePen, leftX, lowY, rightX, lowY); - if (this.ShowLowLineLabel) - this.DrawBillet(gr, range.Low, ref leftX, ref rightX, ref lowY, this.CurrentFont, this.lowLineOptions, this.lowLinePen, this.centerNearSF, this.LabelAlignment, LowCustomText); + if (allowLabelForThisDay && this.ShowLowLineLabel) + this.DrawBillet(gr, range.Low, ref leftX, ref rightX, ref lowY, + this.CurrentFont, this.lowLineOptions, this.lowLinePen, this.centerNearSF, args.Rectangle, LowCustomText); + } if (needDrawExtendLines && this.LowExtendLineOptions.Enabled) @@ -880,8 +910,10 @@ public override void OnPaintChart(PaintChartEventArgs args) { gr.DrawLine(this.openLinePen, leftX, openY, rightX, openY); - if (this.ShowOpenLineLabel) - this.DrawBillet(gr, range.Open, ref leftX, ref rightX, ref openY, this.CurrentFont, this.openLineOptions, this.openLinePen, this.centerNearSF, this.LabelAlignment, OpenCustomText); + if (allowLabelForThisDay && this.ShowOpenLineLabel) + this.DrawBillet(gr, range.Open, ref leftX, ref rightX, ref openY, + this.CurrentFont, this.openLineOptions, this.openLinePen, this.centerNearSF, args.Rectangle, OpenCustomText); + } if (needDrawExtendLines && this.OpenExtendLineOptions.Enabled) @@ -898,8 +930,10 @@ public override void OnPaintChart(PaintChartEventArgs args) { gr.DrawLine(this.closeLinePen, leftX, closeY, rightX, closeY); - if (this.ShowCloseLineLabel) - this.DrawBillet(gr, range.Close, ref leftX, ref rightX, ref closeY, this.CurrentFont, this.closeLineOptions, this.closeLinePen, this.centerNearSF, this.LabelAlignment, CloseCustomText); + if (allowLabelForThisDay && this.ShowCloseLineLabel) + this.DrawBillet(gr, range.Close, ref leftX, ref rightX, ref closeY, + this.CurrentFont, this.closeLineOptions, this.closeLinePen, this.centerNearSF, args.Rectangle, CloseCustomText); + } if (needDrawExtendLines && this.CloseExtendLineOptions.Enabled) @@ -916,8 +950,10 @@ public override void OnPaintChart(PaintChartEventArgs args) { gr.DrawLine(this.middleLinePen, leftX, middleY, rightX, middleY); - if (this.ShowMiddleLineLabel) - this.DrawBillet(gr, range.MiddlePrice, ref leftX, ref rightX, ref middleY, this.CurrentFont, this.middleLineOptions, this.middleLinePen, this.centerNearSF, this.LabelAlignment, MiddleCustomText); + if (allowLabelForThisDay && this.ShowMiddleLineLabel) + this.DrawBillet(gr, range.MiddlePrice, ref leftX, ref rightX, ref middleY, + this.CurrentFont, this.middleLineOptions, this.middleLinePen, this.centerNearSF, args.Rectangle, MiddleCustomText); + } if (needDrawExtendLines && this.MiddleExtendLineOptions.Enabled) @@ -993,43 +1029,84 @@ private void CurrentChartOnSettingsChanged(object sender, ChartEventArgs e) } } - private void DrawBillet(Graphics gr, double price, ref float leftX, ref float rightX, ref float priceY, Font font, LineOptions lineOptions, Pen pen, StringFormat stringFormat, NativeAlignment nativeAlignment, string prefix) + private void DrawBillet(Graphics gr, double price, ref float leftX, ref float rightX, ref float priceY, + Font font, LineOptions lineOptions, Pen pen, StringFormat stringFormat, + Rectangle chartRect, string prefix) { string label = ""; - if (ShowLabel==true) - label = labelFormat==1 ? prefix + this.Symbol.FormatPrice(price) : labelFormat == 0 ? this.Symbol.FormatPrice(price) : prefix; + if (ShowLabel) + label = labelFormat == 1 ? prefix + this.Symbol.FormatPrice(price) + : labelFormat == 0 ? this.Symbol.FormatPrice(price) + : prefix; + var labelSize = gr.MeasureString(label, font); var rect = new RectangleF() { Height = labelSize.Height, - Width = labelSize.Width + 5, - Y = labelPosition == 1 ? priceY - labelSize.Height - lineOptions.Width : priceY - lineOptions.Width + 1 + Width = labelSize.Width + 5 + // Y выставим ниже }; - switch (nativeAlignment) + // === Вертикальное позиционирование: + // 0 — ниже линии; 1 — выше линии; 2 — по центру линии (добавлено ранее) + if (this.labelPosition == 1) // Above + rect.Y = priceY - labelSize.Height - lineOptions.Width; + else if (this.labelPosition == 2) // Center on the line + rect.Y = priceY - labelSize.Height / 2f; + else // Below (0) + rect.Y = priceY - lineOptions.Width + 1; + + // === Горизонтальное позиционирование (5 вариантов): + switch (this.LabelHorizontalMode) { - case NativeAlignment.Center: - { - rect.X = (rightX - leftX) / 2f + leftX - rect.Width / 2f; - break; - } - case NativeAlignment.Right: + case 2: + rect.X = (rightX - leftX) / 2f + leftX - rect.Width / 2f; + break; + + case 1: + rect.X = leftX; + break; + + case 0: + rect.X = rightX - rect.Width; + break; + + case 3: + // Приклеиваемся к правому краю видимой области окна графика + rect.X = chartRect.Right - rect.Width; + break; + + case 4: + // Рисуем в области правой ценовой шкалы: выходим из клипа графика, + // ставим X чуть правее правой границы области чарта и возвращаем клип назад. { - rect.X = rightX - rect.Width; - break; + var savedClip = gr.Clip; // Region + try + { + gr.ResetClip(); // разрешаем рисовать вне области args.Rectangle + rect.X = chartRect.Right + 4; // сместились внутрь области шкалы (обычно сразу справа от графика) + gr.FillRectangle(pen.Brush, rect); + gr.DrawString(label, font, Brushes.White, rect, stringFormat); + return; // уже всё нарисовали + } + finally + { + // вернуть предыдущий клип + gr.SetClip(savedClip, System.Drawing.Drawing2D.CombineMode.Replace); + } } - case NativeAlignment.Left: + default: - { - rect.X = leftX; - break; - } + rect.X = rightX - rect.Width; // безопасный дефолт — как Right + break; } gr.FillRectangle(pen.Brush, rect); gr.DrawString(label, font, Brushes.White, rect, stringFormat); } + + private Session CreateDefaultSession() { // 00:00 diff --git a/Indicators/IndicatorDemandIndex.cs b/Indicators/IndicatorDemandIndex.cs new file mode 100644 index 0000000..fba2757 --- /dev/null +++ b/Indicators/IndicatorDemandIndex.cs @@ -0,0 +1,150 @@ +// Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using TradingPlatform.BusinessLayer; +using TradingPlatform.BusinessLayer.Utils; + +namespace IndicatorDemandIndex; + +public class IndicatorDemandIndex : Indicator +{ + private int Period = 20; + private int maPeriod = 20; + private int diMaPeriod = 20; + private double controlLine = 0; + + private Indicator rangeEMA; + private Indicator volumeEMA; + private Indicator bpEMA; + private Indicator spEMA; + private Indicator diSMA; + private HistoricalDataCustom SmoothingSource; + + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorDemandIndex.cs"; + + public IndicatorDemandIndex() + : base() + { + this.Name = "Demand Index"; + this.AddLineSeries("Main Line", Color.CadetBlue, 1, LineStyle.Solid); + this.AddLineSeries("Smoothed Line", Color.IndianRed, 1, LineStyle.Solid); + this.AddLineSeries("Control Line", Color.White, 1, LineStyle.Solid); + this.SeparateWindow = true; + } + + protected override void OnInit() + { + PriceType volumeEMASourcePrice = PriceType.Volume; + if (this.Symbol.VolumeType == SymbolVolumeType.Volume) + volumeEMASourcePrice = PriceType.Volume; + else + volumeEMASourcePrice = PriceType.Ticks; + this.volumeEMA = Core.Instance.Indicators.BuiltIn.EMA(Period, volumeEMASourcePrice); + this.rangeEMA = Core.Instance.Indicators.BuiltIn.EMA(Period, PriceType.Close); + this.bpEMA = Core.Instance.Indicators.BuiltIn.EMA(maPeriod, PriceType.High); + this.spEMA = Core.Instance.Indicators.BuiltIn.EMA(maPeriod, PriceType.Low); + this.diSMA = Core.Instance.Indicators.BuiltIn.SMA(diMaPeriod, PriceType.Open); + this.SmoothingSource = new HistoricalDataCustom(this); + this.SmoothingSource.AddIndicator(rangeEMA); + this.SmoothingSource.AddIndicator(bpEMA); + this.SmoothingSource.AddIndicator(spEMA); + this.SmoothingSource.AddIndicator(diSMA); + this.AddIndicator(volumeEMA); + } + protected override void OnUpdate(UpdateArgs args) + { + if (this.Count < 2) + return; + double currentHigh = this.High(); + double currentLow = this.Low(); + double currentClose = this.Close(); + double currentVolume = this.Volume(); + if (this.Symbol.VolumeType == SymbolVolumeType.Volume) + currentVolume = this.Volume(); + else + currentVolume = this.Ticks(); + double previousHigh = this.High(1); + double previousLow = this.Low(1); + double previousClose = this.Close(1); + double currentP = currentHigh + currentLow + 2 * currentClose; + double previousP = previousHigh + previousLow + 2 * previousClose; + double range = Math.Max(currentHigh, previousHigh) - Math.Min(currentLow, previousLow); + this.SmoothingSource.SetValue(0d, 0d, 0d, range); + double BP = 0; + if (currentP < previousP) + BP = (currentVolume / this.volumeEMA.GetValue()) / Math.Exp(0.375 * ((currentP + previousP) / (currentHigh - currentLow)) * ((previousP - currentP) / (currentP))); + else + BP = (currentVolume / this.volumeEMA.GetValue()); + double SP = 0; + if (currentP <= previousP) + SP = (currentVolume / this.volumeEMA.GetValue()); + else + SP = (currentVolume / this.volumeEMA.GetValue()) / Math.Exp(0.375 * ((currentP + previousP) / (currentHigh - currentLow)) * ((currentP - previousP) / (previousP))); + this.SmoothingSource.SetValue(0d, BP, SP, range); + double Q = 0; + double currentSPema = spEMA.GetValue(); + double currentBPema = bpEMA.GetValue(); + if (currentBPema > currentSPema) + Q = currentSPema / currentBPema; + else if (currentBPema < currentSPema) + Q = currentBPema / currentSPema; + else + Q = 1; + double DI = 0; + if (currentSPema <= currentBPema) + DI = 100 * (1 - Q); + else + DI = 100*(Q - 1); + this.SmoothingSource.SetValue(DI, BP, SP, range); + this.SetValue(DI); + this.SetValue(this.diSMA.GetValue(), 1); + this.SetValue(this.controlLine, 2); + } + public override IList Settings + { + get + { + var settings = base.Settings; + settings.Add(new SettingItemInteger("Period", this.Period) + { + Text = "Period", + SortIndex = 1, + }); + settings.Add(new SettingItemInteger("maPeriod", this.maPeriod) + { + Text = "Moving Average Period", + SortIndex = 1, + }); + settings.Add(new SettingItemInteger("diMaPeriod", this.diMaPeriod) + { + Text = "Demand Index MA Period", + SortIndex = 1, + }); + + settings.Add(new SettingItemDouble("controlLine", this.controlLine) + { + Text = "Control Line Position", + SortIndex = 2, + DecimalPlaces = 2, + Increment = 0.01, + }); + return settings; + } + set + { + base.Settings = value; + if (value.TryGetValue("Period", out int Period)) + this.Period = Period; + if (value.TryGetValue("maPeriod", out int maPeriod)) + this.maPeriod = maPeriod; + if (value.TryGetValue("diMaPeriod", out int diMaPeriod)) + this.diMaPeriod = diMaPeriod; + if (value.TryGetValue("controlLine", out double controlLine)) + this.controlLine = controlLine; + this.OnSettingsUpdated(); + } + } +} diff --git a/Indicators/IndicatorFairValueGap.cs b/Indicators/IndicatorFairValueGap.cs new file mode 100644 index 0000000..c90cc73 --- /dev/null +++ b/Indicators/IndicatorFairValueGap.cs @@ -0,0 +1,653 @@ +// Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using TradingPlatform.BusinessLayer; +using TradingPlatform.BusinessLayer.Utils; + +namespace ChanneIsIndicators; + +public sealed class IndicatorFairValueGap : Indicator +{ + private bool closeGaps = false; + private bool halfLine = false; + private bool quarterLine = false; + private bool threeQuarterLine = false; + private bool drawBorder = true; + private bool expandGap = false; + private bool direction = false; + private bool showShrink = true; + private bool hideClosed = false; + private bool continueToEnd = false; + private bool lastContinue = false; + + private ShrinkType shrinkType = ShrinkType.Shrink; + private DirectionType directionType = DirectionType.UpAndDown; + + public LineOptions upBorderOptions { get; set; } + public LineOptions downBorderOptions { get; set; } + public LineOptions upHalfLineOptions { get; set; } + public LineOptions downHalfLineOptions { get; set; } + + private int gapLength = 200; + private int gapsNumber = 15; + private int percentToClose = 50; + private int lastGapsCount = 5; + private double minimalDeviation = 0; + private double maxDeviation = 100; + + public Color UpColor + { + get => this.upBrush.Color; + set => this.upBrush.Color = value; + } + private readonly SolidBrush upBrush; + + public Color DownColor + { + get => this.downBrush.Color; + set => this.downBrush.Color = value; + } + private readonly SolidBrush downBrush; + + private readonly Pen upPen; + private readonly Pen downPen; + private readonly Pen upHalfPen; + private readonly Pen downHalfPen; + + List gaps; + private bool gapAdded = false; + + public override string ShortName => $"FVG"; + public override string HelpLink => "https://help.quantower.com/quantower/analytics-panels/chart/technical-indicators/channels/fair-value-gap-fvg"; + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorFairValueGap.cs"; + + public IndicatorFairValueGap() + : base() + { + Name = "Fair Value Gap"; + SeparateWindow = false; + this.upBorderOptions = new LineOptions(); + this.downBorderOptions = new LineOptions(); + this.upHalfLineOptions = new LineOptions(); + this.downHalfLineOptions = new LineOptions(); + + this.upBorderOptions.Color = Color.Green; + this.downBorderOptions.Color = Color.Red; + this.upHalfLineOptions.Color = Color.Green; + this.downHalfLineOptions.Color = Color.Red; + + this.upBorderOptions.Width = 1; + this.downBorderOptions.Width = 1; + this.upHalfLineOptions.Width = 1; + this.downHalfLineOptions.Width = 1; + + this.upBorderOptions.WithCheckBox = false; + this.downBorderOptions.WithCheckBox = false; + this.upHalfLineOptions.WithCheckBox = false; + this.downHalfLineOptions.WithCheckBox = false; + + this.upBrush = new SolidBrush(Color.FromArgb(25, Color.Green)); + this.downBrush = new SolidBrush(Color.FromArgb(25, Color.Red)); + this.upPen = new Pen(Color.Green, 1); + this.downPen = new Pen(Color.Red, 1); + this.upHalfPen = new Pen(Color.Green, 1); + this.downHalfPen = new Pen(Color.Red, 1); + } + protected override void OnInit() + { + this.gaps = new List(); + } + protected override void OnUpdate(UpdateArgs args) + { + if (this.Count < 4) + return; + double currentHigh = this.High(1); + double currentLow = this.Low(1); + double previousHigh = this.High(3); + double previousLow = this.Low(3); + double currentClose = this.Close(1); + double currentOpen = this.Open(1); + double previousClose = this.Close(3); + double previousOpen = this.Open(3); + double middleClose = this.Close(2); + double middleOpen = this.Open(2); + //Checking the conditions for creating a new gap + if (args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar) + if ((currentClose < currentOpen && previousClose < previousOpen && middleClose < middleOpen) || (currentClose > currentOpen && previousClose > previousOpen && middleClose > middleOpen) || !direction) + { + int gapStart = expandGap ? this.Count - 4 : this.Count - 2; + //Checking if a gap is falling or rising + bool upGap = currentLow > previousHigh && 100 * (currentLow - previousHigh) / previousHigh >= minimalDeviation && 100 * (currentLow - previousHigh) / previousHigh <= maxDeviation && (this.directionType == DirectionType.OnlyUp || this.directionType == DirectionType.UpAndDown); + bool downGap = currentHigh < previousLow && 100 * (previousLow - currentHigh) / currentHigh >= minimalDeviation && 100 * (previousLow - currentHigh) / currentHigh <= maxDeviation && (this.directionType == DirectionType.OnlyDown || this.directionType == DirectionType.UpAndDown); + //Creating a new gap + if (upGap) + this.gaps.Insert(0, new IndicatorFairValueGapGap(gapStart, previousHigh, currentLow)); + else if (downGap) + this.gaps.Insert(0, new IndicatorFairValueGapGap(gapStart, previousLow, currentHigh)); + } + //New values for current High and Low to update the gap in real time + currentHigh = this.High(0); + currentLow = this.Low(0); + //Update the state of the gap from the intersection with the current candle + for (int i = 0; i <= gaps.Count - 1; i++) + { + var currGap = gaps[i]; + if (!currGap.IsEnded) + { + //Assigning the latest gap high and low price values + double currentDownPrice = currGap.downPoints[currGap.downPoints.Count - 1].Price; + double currentUpPrice = currGap.upPoints[currGap.upPoints.Count - 1].Price; + //Checking if a point on that candle has already been added in current gap + bool downPointAdded = currGap.downPoints[currGap.downPoints.Count - 1].BarNumber == this.Count - 1; + bool upPointAdded = currGap.upPoints[currGap.upPoints.Count - 1].BarNumber == this.Count - 1; + //Checking whether a gap intersects with a candle + if (currentHigh > currentDownPrice && currentHigh < currentUpPrice && this.shrinkType != ShrinkType.NoShrink) + { + switch (this.shrinkType) + { + case ShrinkType.Shrink: + if (!downPointAdded) // Adding new point + currGap.AddDownPoint(this.Count - 1, currentHigh); + else if (downPointAdded) //Changing the position of the current point + currGap.downPoints[currGap.downPoints.Count - 1].Price = currentHigh; + break; + case ShrinkType.Close: + if (!downPointAdded) + currGap.AddDownPoint(this.Count - 1, currentDownPrice); + else + currGap.downPoints[currGap.downPoints.Count - 1].Price = currentDownPrice; + if (this.Count - 1 != currGap.downPoints[0].BarNumber) //Closing the candle when it crosses, if this is not the first gap point + currGap.IsEnded = true; + break; + case ShrinkType.CloseOnValue: + //Closing a gap if more than a specified percentage is closed + if (100 * ((currentUpPrice - currentHigh) / (currGap.upPoints[0].Price - currGap.downPoints[0].Price)) <= 100 - percentToClose) + currGap.IsEnded = true; + if (!downPointAdded) + currGap.AddDownPoint(this.Count - 1, currentHigh); + else + currGap.downPoints[currGap.downPoints.Count - 1].Price = currentHigh; + break; + default: + if (!downPointAdded) + currGap.AddDownPoint(this.Count - 1, currentHigh); + else + currGap.downPoints[currGap.downPoints.Count - 1].Price = currentHigh; + break; + } + } + else if (!downPointAdded) // Adding a new point if there is no intersection + currGap.AddDownPoint(this.Count - 1, currentDownPrice); + if (currentLow < currentUpPrice && currentLow > currentDownPrice && this.shrinkType != ShrinkType.NoShrink) + { + switch (this.shrinkType) + { + case ShrinkType.Shrink: + if (!upPointAdded) //Adding new point + currGap.AddUpPoint(this.Count - 1, currentLow); + else + currGap.upPoints[currGap.downPoints.Count - 1].Price = currentLow; //Changing the position of the current point + break; + case ShrinkType.Close: + if (!upPointAdded) + currGap.AddUpPoint(this.Count - 1, currentUpPrice); + else + currGap.upPoints[currGap.upPoints.Count - 1].Price = currentUpPrice; + if (this.Count - 1 != currGap.upPoints[0].BarNumber) //Closing the candle when it crosses, if this is not the first gap point + currGap.IsEnded = true; + break; + case ShrinkType.CloseOnValue: + //Closing a gap if more than a specified percentage is closed + if (100 * ((currentLow - currentDownPrice) / (currGap.upPoints[0].Price - currGap.downPoints[0].Price)) <= 100 - percentToClose) + currGap.IsEnded = true; + if (!upPointAdded) + currGap.AddUpPoint(this.Count - 1, currentLow); + else + currGap.upPoints[currGap.upPoints.Count - 1].Price = currentLow; + break; + default: + if (!upPointAdded) + currGap.AddUpPoint(this.Count - 1, currentLow); + else + currGap.upPoints[currGap.upPoints.Count - 1].Price = currentLow; + break; + } + } + else if (!upPointAdded) //Adding a new point if there is no intersection + currGap.AddUpPoint(this.Count - 1, currentUpPrice); + // Checking whether the current candle has completely covered the gap + if ((currentHigh >= currentUpPrice && currentLow <= currentUpPrice) && (currentHigh >= currentDownPrice && currentLow <= currentDownPrice) && !closeGaps) + { + currGap.IsEnded = true; + currGap.upPoints[currGap.upPoints.Count - 1].Price = currGap.upPoints[currGap.upPoints.Count - 2].Price; + currGap.downPoints[currGap.downPoints.Count - 1].Price = currGap.downPoints[currGap.downPoints.Count - 2].Price; + } + if (closeGaps && (currGap.upPoints[currGap.upPoints.Count - 1].BarNumber - currGap.upPoints[0].BarNumber >= gapLength || currGap.downPoints[currGap.downPoints.Count - 1].BarNumber - currGap.downPoints[0].BarNumber >= gapLength)) + currGap.IsEnded = true; + } + } + while (this.gaps.Count > gapsNumber + 1) + this.gaps.RemoveAt(gaps.Count-1); + } + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + + if (this.CurrentChart == null) + return; + + var gr = args.Graphics; + var currWindow = this.CurrentChart.Windows[args.WindowIndex]; + RectangleF prevClipRectangle = gr.ClipBounds; + gr.SetClip(args.Rectangle); + try + { + int continuedCount = 0; + for (int i = 0; i < gaps.Count; i++) + { + var currGap = gaps[i]; + if (!(currGap.IsEnded && hideClosed)) + { + List Points = new List(); + // Selecting a color depending on the type of gap + var brush = currGap.GapType == IndicatorFairValueGapType.Up ? this.upBrush : this.downBrush; + var pen = currGap.GapType == IndicatorFairValueGapType.Up ? this.upPen : this.downPen; + var halfPen = currGap.GapType == IndicatorFairValueGapType.Up ? this.upHalfPen : this.downHalfPen; + // Painting + if (showShrink) //Case where shrink display is required + { + for (int j = 0; j < currGap.upPoints.Count; j++) + { + DateTime barTime = this.HistoricalData[currGap.upPoints[j].BarNumber, SeekOriginHistory.Begin].TimeLeft; + int x = (int)currWindow.CoordinatesConverter.GetChartX(barTime) + CurrentChart.BarsWidth / 2; + int y = (int)currWindow.CoordinatesConverter.GetChartY(currGap.upPoints[j].Price); + Points.Add(new Point(x, y)); + } + for (int j = currGap.downPoints.Count - 1; j >= 0; j--) + { + DateTime barTime = this.HistoricalData[currGap.downPoints[j].BarNumber, SeekOriginHistory.Begin].TimeLeft; + int x = (int)currWindow.CoordinatesConverter.GetChartX(barTime) + CurrentChart.BarsWidth / 2; + int y = (int)currWindow.CoordinatesConverter.GetChartY(currGap.downPoints[j].Price); + Points.Add(new Point(x, y)); + } + if (!currGap.IsEnded && this.continueToEnd && (!this.lastContinue || (this.lastContinue && i < this.lastGapsCount)) && continuedCount <= this.lastGapsCount) + { + Points.Insert(currGap.upPoints.Count, new Point(currWindow.ClientRectangle.Width, Points[currGap.upPoints.Count - 1].Y)); + Points.Insert(currGap.upPoints.Count+1, new Point(currWindow.ClientRectangle.Width, Points[currGap.upPoints.Count + 1].Y)); + continuedCount++; + } + } + else //Case where shrink display is not required + { + int x1 = (int)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currGap.upPoints[0].BarNumber, SeekOriginHistory.Begin].TimeLeft) + CurrentChart.BarsWidth / 2; + int y1 = (int)currWindow.CoordinatesConverter.GetChartY(currGap.upPoints[0].Price); + int width = (currGap.downPoints[currGap.downPoints.Count - 1].BarNumber - currGap.upPoints[0].BarNumber) * CurrentChart.BarsWidth; + if (currGap.upPoints[currGap.upPoints.Count-1].BarNumber == this.HistoricalData.Count - 1 && !currGap.IsEnded && this.continueToEnd && (!this.lastContinue || (this.lastContinue && i < this.lastGapsCount)) && continuedCount <= lastGapsCount) + { + width = currWindow.ClientRectangle.Width - x1; + } + int height = (int)currWindow.CoordinatesConverter.GetChartY(currGap.downPoints[0].Price) - y1; + Points.Add(new Point(x1, y1)); + Points.Add(new Point(x1 + width, y1)); + Points.Add(new Point(x1 + width, y1 + height)); + Points.Add(new Point(x1, y1 + height)); + } + //Drawing a gap + gr.FillPolygon(brush, Points.ToArray()); + if (this.drawBorder) + gr.DrawPolygon(pen, Points.ToArray()); + if (this.halfLine) //Drawing a half line + { + int halfLineY = (int)currWindow.CoordinatesConverter.GetChartY((currGap.upPoints[0].Price + currGap.downPoints[0].Price) / 2); + int halfLineStart = (int)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currGap.upPoints[0].BarNumber, SeekOriginHistory.Begin].TimeLeft) + CurrentChart.BarsWidth / 2; + int halfLineEnd = (int)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currGap.upPoints[currGap.upPoints.Count - 1].BarNumber, SeekOriginHistory.Begin].TimeLeft) + CurrentChart.BarsWidth / 2; + if (!currGap.IsEnded && this.continueToEnd && (!this.lastContinue || (this.lastContinue && i < this.lastGapsCount)) && continuedCount <= lastGapsCount) + halfLineEnd = currWindow.ClientRectangle.Width; + gr.DrawLine(halfPen, halfLineEnd, halfLineY, halfLineStart, halfLineY); + } + if (this.quarterLine) //Drawing a quarter line + { + int quarterLineY = currGap.GapType == IndicatorFairValueGapType.Up ? (int)currWindow.CoordinatesConverter.GetChartY((currGap.upPoints[0].Price + currGap.downPoints[0].Price*3) / 4) : (int)currWindow.CoordinatesConverter.GetChartY((currGap.upPoints[0].Price * 3 + currGap.downPoints[0].Price) / 4); + int quarterLineStart = (int)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currGap.upPoints[0].BarNumber, SeekOriginHistory.Begin].TimeLeft) + CurrentChart.BarsWidth / 2; + int quarterLineEnd = (int)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currGap.upPoints[currGap.upPoints.Count - 1].BarNumber, SeekOriginHistory.Begin].TimeLeft) + CurrentChart.BarsWidth / 2; + if (!currGap.IsEnded && this.continueToEnd && (!this.lastContinue || (this.lastContinue && i < this.lastGapsCount)) && continuedCount <= lastGapsCount) + quarterLineEnd = currWindow.ClientRectangle.Width; + gr.DrawLine(halfPen, quarterLineEnd, quarterLineY, quarterLineStart, quarterLineY); + } + if (this.threeQuarterLine) //Drawing a three-quarters line + { + int threeQuarterLineY = currGap.GapType == IndicatorFairValueGapType.Up ? (int)currWindow.CoordinatesConverter.GetChartY((currGap.upPoints[0].Price*3 + currGap.downPoints[0].Price) / 4) : (int)currWindow.CoordinatesConverter.GetChartY((currGap.upPoints[0].Price + currGap.downPoints[0].Price*3) / 4); + int threeQuarterLineStart = (int)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currGap.upPoints[0].BarNumber, SeekOriginHistory.Begin].TimeLeft) + CurrentChart.BarsWidth / 2; + int threeQuarterLineEnd = (int)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currGap.upPoints[currGap.upPoints.Count - 1].BarNumber, SeekOriginHistory.Begin].TimeLeft) + CurrentChart.BarsWidth / 2; + if (!currGap.IsEnded && this.continueToEnd && (!this.lastContinue || (this.lastContinue && i < this.lastGapsCount)) && continuedCount <= lastGapsCount) + threeQuarterLineEnd = currWindow.ClientRectangle.Width; + gr.DrawLine(halfPen, threeQuarterLineEnd, threeQuarterLineY, threeQuarterLineStart, threeQuarterLineY); + } + } + } + } + finally + { + gr.SetClip(prevClipRectangle); + } + } + public override IList Settings + { + get + { + var settings = base.Settings; + settings.Add(new SettingItemBoolean("Partially", this.closeGaps) + { + Text = "Close gaps partially", + SortIndex = 1, + }); + SettingItemRelationVisibility visibleRelationPartially = new SettingItemRelationVisibility("Partially", true); + settings.Add(new SettingItemInteger("GapsLength", this.gapLength) + { + Text = "Max gaps trail length", + SortIndex = 2, + Dimension = "Bars", + Relation = visibleRelationPartially, + Minimum = 1 + }); + + settings.Add(new SettingItemInteger("MaxNumber", this.gapsNumber) + { + Text = "Max number of gaps", + SortIndex = 3, + Minimum = 1 + }); + settings.Add(new SettingItemDouble("minimalDeviation", this.minimalDeviation) + { + Text = "Minimal Deviation", + SortIndex = 3, + Dimension = "%", + Minimum = 0, + Maximum = 100, + DecimalPlaces = 3, + Increment = 0.001 + }); + settings.Add(new SettingItemDouble("MaxDeviation", this.maxDeviation) + { + Text = "Maximal Deviation", + SortIndex = 3, + Dimension = "%", + Minimum = 0.001, + Maximum = 100, + DecimalPlaces = 3, + Increment = 0.001 + }); + settings.Add(new SettingItemColor("upColor", this.UpColor) + { + Text = "Up Color", + SortIndex = 4, + }); + settings.Add(new SettingItemColor("downColor", this.DownColor) + { + Text = "Down Color", + SortIndex = 5, + }); + settings.Add(new SettingItemBoolean("Border", this.drawBorder) + { + Text = "Draw Borders", + SortIndex = 6, + }); + SettingItemRelationVisibility visibleRelationBorder = new SettingItemRelationVisibility("Border", true); + settings.Add(new SettingItemLineOptions("UpBorder", this.upBorderOptions) + { + Text = "Up Border Style", + SortIndex = 6, + Relation = visibleRelationBorder, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true + }); + settings.Add(new SettingItemLineOptions("DownBorder", this.downBorderOptions) + { + Text = "Down Border Style", + SortIndex = 6, + Relation = visibleRelationBorder, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true, + }); + settings.Add(new SettingItemBoolean("HalfLine", this.halfLine) + { + Text = "Draw Half Line", + SortIndex = 7, + }); + settings.Add(new SettingItemBoolean("quarterLine", this.quarterLine) + { + Text = "Draw Quarter Line", + SortIndex = 7, + }); + settings.Add(new SettingItemBoolean("threeQuarterLine", this.threeQuarterLine) + { + Text = "Draw Three-quarters Line", + SortIndex = 7, + }); + SettingItemRelationVisibility visibleRelationHalfLine = new SettingItemRelationVisibility("HalfLine", true); + settings.Add(new SettingItemLineOptions("UpHalf", this.upHalfLineOptions) + { + Text = "Up Half Line Style", + SortIndex = 7, + Relation = visibleRelationHalfLine, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true, + }); + settings.Add(new SettingItemLineOptions("DownHalf", this.downHalfLineOptions) + { + Text = "Down Half Line Style", + SortIndex = 7, + Relation = visibleRelationHalfLine, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true, + }); + settings.Add(new SettingItemSelectorLocalized("Shrink", this.shrinkType, new List { new SelectItem("Shrink Always", ShrinkType.Shrink), new SelectItem("Close on percent", ShrinkType.CloseOnValue), new SelectItem("Close Always", ShrinkType.Close), new SelectItem("No Shrink", ShrinkType.NoShrink) }) + { + Text = "Shrink Type", + SortIndex = 8 + }); + SettingItemRelationVisibility visibleRelationClosePercent = new SettingItemRelationVisibility("Shrink", new SelectItem("Close on percent", ShrinkType.CloseOnValue)); + settings.Add(new SettingItemInteger("Percent", this.percentToClose) + { + Text = "Percent to close", + SortIndex = 8, + Minimum = 1, + Dimension = "%", + Maximum = 100, + Relation = visibleRelationClosePercent + }); + settings.Add(new SettingItemBoolean("HideClosed", this.hideClosed) + { + Text = "Hide Closed", + SortIndex = 8, + }); + settings.Add(new SettingItemBoolean("ExpandGap", this.expandGap) + { + Text = "Expand Gap", + SortIndex = 1, + }); + settings.Add(new SettingItemBoolean("Direction", this.direction) + { + Text = "Same Direction Requested", + SortIndex = 1, + }); + settings.Add(new SettingItemSelectorLocalized("DirectionType", this.directionType, new List { new SelectItem("Up and Down", DirectionType.UpAndDown), new SelectItem("Only Up", DirectionType.OnlyUp), new SelectItem("Only Down", DirectionType.OnlyDown) }) + { + Text = "Direction Type", + SortIndex = 1 + }); + SettingItemRelationVisibility visibleRelationDirection = new SettingItemRelationVisibility("Direction", true); + SettingItemRelationVisibility showShrinkVisibleRelation = new SettingItemRelationVisibility("Shrink", [new SelectItem("Close on percent", ShrinkType.CloseOnValue), new SelectItem("Shrink Always", ShrinkType.Shrink)]); + settings.Add(new SettingItemBoolean("ShowShrink", this.showShrink) + { + Text = "Show Shrink", + SortIndex = 8, + Relation = showShrinkVisibleRelation, + }); + settings.Add(new SettingItemBoolean("continueToEnd", this.continueToEnd) + { + Text = "Continue unclosed gaps to end", + SortIndex = 9, + }); + SettingItemRelationVisibility visibleRelationContinueToEnd = new SettingItemRelationVisibility("continueToEnd", true); + settings.Add(new SettingItemBoolean("lastContinue", this.lastContinue) + { + Text = "Continue only several last gaps", + SortIndex = 9, + Relation = visibleRelationContinueToEnd, + }); + SettingItemRelationVisibility visibleRelationLastContinue = new SettingItemRelationVisibility("lastContinue", true); + settings.Add(new SettingItemInteger("lastGapsCount", this.lastGapsCount) + { + Text = "Last gaps count", + SortIndex = 9, + Relation = visibleRelationLastContinue, + Minimum = 1 + }); + return settings; + + } + set + { + base.Settings = value; + if (value.TryGetValue("Partially", out bool partially)) + this.closeGaps = partially; + if (value.TryGetValue("GapsLength", out int gapLength)) + this.gapLength = gapLength; + if (value.TryGetValue("MaxNumber", out int MaxNumber)) + this.gapsNumber = MaxNumber; + if (value.TryGetValue("lastGapsCount", out int lastGapsCount)) + this.lastGapsCount = lastGapsCount; + if (value.TryGetValue("minimalDeviation", out double minimalDeviation)) + this.minimalDeviation = minimalDeviation; + if (value.TryGetValue("MaxDeviation", out double maxDeviation)) + this.maxDeviation = maxDeviation; + if (value.TryGetValue("upColor", out Color upColor)) + this.UpColor = upColor; + if (value.TryGetValue("downColor", out Color downColor)) + this.DownColor = downColor; + if (value.TryGetValue("Border", out bool drawBorder)) + this.drawBorder = drawBorder; + if (value.TryGetValue("continueToEnd", out bool continueToEnd)) + this.continueToEnd = continueToEnd; + if (value.TryGetValue("lastContinue", out bool lastContinue)) + this.lastContinue = lastContinue; + if (value.TryGetValue("UpBorder", out LineOptions UpBorder)) + { + this.upBorderOptions = UpBorder; + this.upPen.Width = UpBorder.Width; + this.upPen.Color = UpBorder.Color; + this.upPen.DashStyle = (DashStyle)UpBorder.LineStyle; + } + if (value.TryGetValue("DownBorder", out LineOptions DownBorder)) + { + this.downBorderOptions = DownBorder; + this.downPen.Width = DownBorder.Width; + this.downPen.Color = DownBorder.Color; + this.downPen.DashStyle = (DashStyle)DownBorder.LineStyle; + } + if (value.TryGetValue("HalfLine", out bool halfLine)) + this.halfLine = halfLine; + if (value.TryGetValue("quarterLine", out bool quarterLine)) + this.quarterLine = quarterLine; + if (value.TryGetValue("threeQuarterLine", out bool threeQuarterLine)) + this.threeQuarterLine = threeQuarterLine; + if (value.TryGetValue("UpHalf", out LineOptions UpHalf)) + { + this.upHalfLineOptions = UpHalf; + this.upHalfPen.Width = UpHalf.Width; + this.upHalfPen.Color = UpHalf.Color; + this.upHalfPen.DashStyle = (DashStyle)UpHalf.LineStyle; + } + if (value.TryGetValue("DownHalf", out LineOptions DownHalf)) + { + this.downHalfLineOptions = DownHalf; + this.downHalfPen.Width = DownHalf.Width; + this.downHalfPen.Color = DownHalf.Color; + this.downHalfPen.DashStyle = (DashStyle)DownHalf.LineStyle; + } + if (value.TryGetValue("Shrink", out ShrinkType Shrink)) + this.shrinkType = Shrink; + if (value.TryGetValue("Percent", out int Percent)) + this.percentToClose = Percent; + if (value.TryGetValue("ExpandGap", out bool expandGap)) + this.expandGap = expandGap; + if (value.TryGetValue("Direction", out bool direction)) + this.direction = direction; + if (value.TryGetValue("ShowShrink", out bool showShrink)) + this.showShrink = showShrink; + if (value.TryGetValue("HideClosed", out bool hideClosed)) + this.hideClosed = hideClosed; + if (value.TryGetValue("DirectionType", out DirectionType directionType)) + this.directionType = directionType; + this.OnSettingsUpdated(); + } + } +} +internal sealed class IndicatorFairValueGapGap +{ + public IndicatorFairValueGapType GapType { get; private set; } + public List upPoints { get; private set; } + public List downPoints { get; private set; } + + public bool IsEnded { get; set; } + + public IndicatorFairValueGapGap(int startBar, double startPrice, double endPrice) + { + this.IsEnded = false; + + upPoints = new List(); + downPoints = new List(); + if (endPrice > startPrice) + { + this.GapType = IndicatorFairValueGapType.Up; + upPoints.Add(new PricePoint(startBar, endPrice)); + downPoints.Add(new PricePoint(startBar, startPrice)); + } + if (endPrice < startPrice) + { + this.GapType = IndicatorFairValueGapType.Down; + upPoints.Add(new PricePoint(startBar, startPrice)); + downPoints.Add(new PricePoint(startBar, endPrice)); + } + } + public void AddUpPoint(int barNumber, double price) + { + upPoints.Add(new PricePoint(barNumber, price)); + } + public void AddDownPoint(int barNumber, double price) + { + downPoints.Add(new PricePoint(barNumber, price)); + } +} +internal sealed class PricePoint +{ + public int BarNumber { get; private set; } + public double Price { get; set; } + public PricePoint(int barNumber, double price) + { + this.BarNumber = barNumber; + this.Price = price; + } +} +public enum IndicatorFairValueGapType +{ + Up, + Down +} +public enum ShrinkType +{ + Shrink, + CloseOnValue, + Close, + NoShrink +} +public enum DirectionType +{ + OnlyUp, + OnlyDown, + UpAndDown +} \ No newline at end of file diff --git a/Indicators/IndicatorFractals.cs b/Indicators/IndicatorFractals.cs index 0b58bca..4982a23 100644 --- a/Indicators/IndicatorFractals.cs +++ b/Indicators/IndicatorFractals.cs @@ -8,7 +8,7 @@ namespace Fractals; public class IndicatorFractals : Indicator { - [InputParameter("Period", 10, 2)] + [InputParameter("Period", 10, 1)] public int period = 3; [InputParameter("Local Maximum Color", 20)] @@ -24,6 +24,7 @@ public class IndicatorFractals : Indicator "Arrow", IndicatorLineMarkerIconType.UpArrow, "Flag", IndicatorLineMarkerIconType.Flag, "Circle", IndicatorLineMarkerIconType.FillCircle, + "Pointer", IndicatorLineMarkerIconType.UpPointer, })] public IndicatorLineMarkerIconType localMaxIconType = IndicatorLineMarkerIconType.UpArrow; @@ -31,6 +32,7 @@ public class IndicatorFractals : Indicator "Arrow", IndicatorLineMarkerIconType.DownArrow, "Flag", IndicatorLineMarkerIconType.Flag, "Circle", IndicatorLineMarkerIconType.FillCircle, + "Pointer", IndicatorLineMarkerIconType.DownPointer, })] public IndicatorLineMarkerIconType localMinIconType = IndicatorLineMarkerIconType.DownArrow; @@ -58,49 +60,35 @@ protected override void OnUpdate(UpdateArgs args) double baseHigh = High(period); double baseLow = Low(period); - double currentHigh; - double currentLow; int minTrendValue = 0; int maxTrendValue = 0; SetValue(High(), 0); SetValue(Low(), 1); - for (int i = 0; i <= period; i++) + for (int i = 1; i <= period; i++) { - currentHigh = High(period + i); - currentLow = Low(period + i); - - if (baseHigh > currentHigh) + double leftHigh = High(period + i); + double leftLow = Low(period + i); + double rightHigh = High(period - i); + double rightLow = Low(period - i); + if (baseHigh >= leftHigh && baseHigh > rightHigh) maxTrendValue++; - if (baseLow < currentLow) + if (baseLow <= leftLow && baseLow < rightLow) minTrendValue++; - } - for (int i = 0; i <= period; i++) - { - - currentHigh = High(period - i); - currentLow = Low(period - i); - - if (baseHigh > currentHigh) - maxTrendValue++; - if (baseLow < currentLow) - minTrendValue++; } - if (maxTrendValue == period * 2) + if (maxTrendValue == period) LinesSeries[0].SetMarker(period, new IndicatorLineMarker(this.maximumColor, upperIcon: this.localMaxIconType)); - if (minTrendValue == period * 2) + if (minTrendValue == period) LinesSeries[1].SetMarker(period, new IndicatorLineMarker(this.minimumColor, bottomIcon: this.localMinIconType)); - if (maxTrendValue != period * 2 && minTrendValue != period * 2) - { + if (maxTrendValue != period) LinesSeries[0].RemoveMarker(period); + if (minTrendValue != period) LinesSeries[1].RemoveMarker(period); - } } - #region Draw lines until intersection public override void OnPaintChart(PaintChartEventArgs args) diff --git a/Indicators/IndicatorGaps.cs b/Indicators/IndicatorGaps.cs new file mode 100644 index 0000000..34a87c9 --- /dev/null +++ b/Indicators/IndicatorGaps.cs @@ -0,0 +1,210 @@ +// Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace ChanneIsIndicators; + +public sealed class IndicatorGaps : Indicator +{ + private bool closeGaps = false; + + private int gapLength = 200; + private int gapsNumber = 15; + private double minimalDeviation = 0; + + public Color UpColor + { + get => this.upBrush.Color; + set => this.upBrush.Color = value; + } + private readonly SolidBrush upBrush; + + public Color DownColor + { + get => this.downBrush.Color; + set => this.downBrush.Color = value; + } + private readonly SolidBrush downBrush; + + List gaps; + + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorGaps.cs"; + + public IndicatorGaps() + : base() + { + Name = "Gaps"; + SeparateWindow = false; + + this.upBrush = new SolidBrush(Color.Green); + this.downBrush = new SolidBrush(Color.Red); + } + protected override void OnInit() + { + this.gaps = new List(); + } + protected override void OnUpdate(UpdateArgs args) + { + if (this.Count < 2) + return; + double currentClose = this.Close(); + double currentOpen = this.Open(); + double currentHigh = this.High(); + double currentLow = this.Low(); + double previousHigh = this.High(1); + double previousLow = this.Low(1); + + if (currentLow > previousHigh && 100 * (currentLow - previousHigh) / previousHigh >= minimalDeviation) + this.gaps.Add(new Gap(this.Count - 2, previousHigh, this.Count - 1, currentLow)); + else if (currentHigh < previousLow && 100 * (previousLow - currentHigh) / currentHigh >= minimalDeviation) + this.gaps.Add(new Gap(this.Count - 2, previousLow, this.Count - 1, currentHigh)); + + for (int i = 0; i <= gaps.Count - 1; i++) + { + var currGap = gaps[i]; + if (!currGap.IsEnded) + { + currGap.EndBar++; + if ((currentClose >= currGap.StartPrice && currentOpen <= currGap.StartPrice) || (currentClose > currGap.EndPrice && currentOpen < currGap.EndPrice)) + currGap.IsEnded = true; + else if ((currentOpen > currGap.StartPrice && currentClose < currGap.StartPrice) || (currentOpen > currGap.EndPrice && currentClose < currGap.EndPrice)) + currGap.IsEnded = true; + } + if (closeGaps && this.gaps[i].EndBar - this.gaps[i].StartBar >= gapLength) + this.gaps[i].EndBar = this.gaps[i].StartBar + gapLength; + if (this.gaps[i].EndBar >= this.Count) + this.gaps[i].EndBar = this.Count - 1; + } + while (this.gaps.Count > gapsNumber) + this.gaps.RemoveAt(0); + } + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + + if (this.CurrentChart == null) + return; + + var gr = args.Graphics; + var currWindow = this.CurrentChart.Windows[args.WindowIndex]; + RectangleF prevClipRectangle = gr.ClipBounds; + gr.SetClip(args.Rectangle); + + for (int i = 0; i <= gaps.Count - 1; i++) + { + DateTime startTime = this.HistoricalData[this.gaps[i].StartBar, SeekOriginHistory.Begin].TimeLeft; + DateTime endTime = this.HistoricalData[this.gaps[i].EndBar, SeekOriginHistory.Begin].TimeLeft; + int x = (int)currWindow.CoordinatesConverter.GetChartX(startTime); + int width = (int)currWindow.CoordinatesConverter.GetChartX(endTime) - x; + int y = this.gaps[i].GapType == GapType.Down ? (int)currWindow.CoordinatesConverter.GetChartY(this.gaps[i].StartPrice) : (int)currWindow.CoordinatesConverter.GetChartY(this.gaps[i].EndPrice); + int height = this.gaps[i].GapType == GapType.Down ? Math.Abs(y - (int)currWindow.CoordinatesConverter.GetChartY(this.gaps[i].EndPrice)) : Math.Abs(y - (int)currWindow.CoordinatesConverter.GetChartY(this.gaps[i].StartPrice)); + var rect = new RectangleF(x + CurrentChart.BarsWidth / 2, y, width + CurrentChart.BarsWidth / 2, height); + + var brush = this.gaps[i].GapType == GapType.Up ? this.upBrush : this.downBrush; + + gr.FillRectangle(brush, rect); + } + gr.SetClip(prevClipRectangle); + } + public override IList Settings + { + get + { + var settings = base.Settings; + settings.Add(new SettingItemBoolean("Partially", this.closeGaps) + { + Text = "Close gaps partially", + SortIndex = 1, + }); + SettingItemRelationVisibility visibleRelation = new SettingItemRelationVisibility("Partially", true); + settings.Add(new SettingItemInteger("GapsLength", this.gapLength) + { + Text = "Max gaps trail length", + SortIndex = 2, + Dimension = "Bars", + Relation = visibleRelation, + Minimum = 1 + }); + + settings.Add(new SettingItemInteger("MaxNumber", this.gapsNumber) + { + Text = "Max number of gaps", + SortIndex = 3, + Minimum = 1 + }); + settings.Add(new SettingItemDouble("minimalDeviation", this.minimalDeviation) + { + Text = "Minimal Deviation", + SortIndex = 3, + Dimension = "%", + Minimum = 0, + DecimalPlaces = 3, + Increment = 0.001 + }); + settings.Add(new SettingItemColor("upColor", this.UpColor) + { + Text = "Up Color", + SortIndex = 4, + }); + settings.Add(new SettingItemColor("downColor", this.DownColor) + { + Text = "Down Color", + SortIndex = 5, + }); + return settings; + + } + set + { + base.Settings = value; + if (value.TryGetValue("Partially", out bool partially)) + this.closeGaps = partially; + if (value.TryGetValue("GapsLength", out int gapLength)) + this.gapLength = gapLength; + if (value.TryGetValue("MaxNumber", out int MaxNumber)) + this.gapsNumber = MaxNumber; + if (value.TryGetValue("minimalDeviation", out double minimalDeviation)) + this.minimalDeviation = minimalDeviation; + if (value.TryGetValue("upColor", out Color upColor)) + this.UpColor = upColor; + if (value.TryGetValue("downColor", out Color downColor)) + this.DownColor = downColor; + this.OnSettingsUpdated(); + } + } + +} +internal sealed class Gap +{ + public int StartBar { get; private set; } + public double StartPrice { get; private set; } + public GapType GapType { get; private set; } + + public int EndBar { get; set; } + public double EndPrice { get; set; } + public bool IsEnded { get; set; } + + public Gap(int startBar, double startPrice, int endBar, double endPrice) + { + this.IsEnded = false; + + this.StartBar = startBar; + this.StartPrice = startPrice; + this.EndBar = endBar; + this.EndPrice = endPrice; + + if (endPrice > startPrice) + this.GapType = GapType.Up; + if (endPrice < startPrice) + this.GapType = GapType.Down; + } +} + +internal enum GapType +{ + Up, + Down +} \ No newline at end of file diff --git a/Indicators/IndicatorMarketStructureCHoCHBOS.cs b/Indicators/IndicatorMarketStructureCHoCHBOS.cs index e70b075..a068de9 100644 --- a/Indicators/IndicatorMarketStructureCHoCHBOS.cs +++ b/Indicators/IndicatorMarketStructureCHoCHBOS.cs @@ -4,132 +4,154 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; +using System.IO; +using System.Numerics; using TradingPlatform.BusinessLayer; using TradingPlatform.BusinessLayer.Utils; -namespace TrendIndicators.IndicatorMarketStructureCHoCHBOS; - -public class IndicatorMarketStructureCHoCHBOS : Indicator +namespace IndicatorMarketStructureCHoCHBOS { - private int Period = 10; - private readonly Pen BearishPen; - private readonly Pen BullishPen; - private readonly Pen BullishResistPen; - private readonly Pen BearishResistPen; - private bool showResist = false; - private bool showLabel = false; - public LineOptions BullishOptions { get; set; } - public LineOptions BearishOptions { get; set; } - public LineOptions BullishResistOptions { get; set; } - public LineOptions BearishResistOptions { get; set; } - private LabelLocation labelLocation = LabelLocation.Start; - private Font labelFont = new Font("Arial", 12); - - public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorMarketStructureCHoCHBOS.cs"; - - List pivots = new List(); - public IndicatorMarketStructureCHoCHBOS() - : base() - { - Name = "Market Structure CHoCH/BOS (Fractal)"; - SeparateWindow = false; - this.BearishPen = new Pen(Color.Red); - this.BullishPen = new Pen(Color.Green); - this.BearishResistPen = new Pen(Color.Red); - this.BullishResistPen = new Pen(Color.Green); - this.BullishResistPen.DashStyle = DashStyle.Dash; - this.BearishResistPen.DashStyle = DashStyle.Dash; - this.BullishOptions = new LineOptions(); - this.BearishOptions = new LineOptions(); - this.BullishResistOptions = new LineOptions(); - this.BearishResistOptions = new LineOptions(); - this.BullishOptions.WithCheckBox = false; - this.BearishOptions.WithCheckBox = false; - this.BullishResistOptions.WithCheckBox = false; - this.BearishResistOptions.WithCheckBox = false; - this.BullishOptions.Color = Color.Green; - this.BearishOptions.Color = Color.Red; - this.BullishResistOptions.Color = Color.Green; - this.BearishResistOptions.Color = Color.Red; - this.BearishResistOptions.LineStyle = LineStyle.Dash; - this.BullishResistOptions.LineStyle = LineStyle.Dash; - } - protected override void OnInit() - { - pivots.Clear(); - } - protected override void OnUpdate(UpdateArgs args) + public class IndicatorMarketStructureCHoCHBOS : Indicator { - if (this.Count < Period * 2 + 1) - return; - int currBarIndex = this.Count - Period - 1; - MaxOrMin maxOrMin = LocalMaxOrMin(currBarIndex); - if (maxOrMin != MaxOrMin.Nothing && (pivots.Count < 1 || pivots[pivots.Count - 1].index != currBarIndex)) + private int Period = 10; + private readonly Pen BearishPen; + private readonly Pen BullishPen; + private readonly Pen BullishResistPen; + private readonly Pen BearishResistPen; + private bool showResist = false; + private bool showLabel = false; + public LineOptions BullishOptions { - this.pivots.Add(new PivotPoint(maxOrMin, currBarIndex)); - if (this.pivots[this.pivots.Count - 1].MaxOrMin == MaxOrMin.LocalMaximum) - { - this.pivots[this.pivots.Count - 1].value = ((HistoryItemBar)this.HistoricalData[this.Count - this.Period - 1, SeekOriginHistory.Begin]).High; - } - else - this.pivots[this.pivots.Count - 1].value = ((HistoryItemBar)this.HistoricalData[this.Count - this.Period - 1, SeekOriginHistory.Begin]).Low; + get => CreateLineOptions(this.BullishPen); + set => ApplyLineOptions(this.BullishPen, value); } - } - public override void OnPaintChart(PaintChartEventArgs args) - { - base.OnPaintChart(args); + public LineOptions BearishOptions + { + get => CreateLineOptions(this.BearishPen); + set => ApplyLineOptions(this.BearishPen, value); + } + public LineOptions BullishResistOptions + { + get => CreateLineOptions(this.BullishResistPen); + set => ApplyLineOptions(this.BullishResistPen, value); + } + public LineOptions BearishResistOptions + { + get => CreateLineOptions(this.BearishResistPen); + set => ApplyLineOptions(this.BearishResistPen, value); + } + private LabelLocation labelLocation = LabelLocation.Start; + private Font labelFont = new Font("Arial", 12); + + List pivots = new List(); + public IndicatorMarketStructureCHoCHBOS() + : base() + { + Name = "Market Structure CHoCH/BOS (Fractal)"; + SeparateWindow = false; - if (this.CurrentChart == null) - return; - var gr = args.Graphics; - Period currentPeriod = this.HistoricalData.Aggregation.GetPeriod; - if (currentPeriod.BasePeriod == BasePeriod.Tick) + this.BearishPen = new Pen(Color.Red); + this.BullishPen = new Pen(Color.Green); + this.BearishResistPen = new Pen(Color.Red); + this.BullishResistPen = new Pen(Color.Green); + this.BullishResistPen.DashStyle = DashStyle.Dash; + this.BearishResistPen.DashStyle = DashStyle.Dash; + } + protected override void OnInit() { - gr.DrawString("Indicator does not work on tick aggregation", new Font("Arial", 20), Brushes.Red, 20, 50); - return; + pivots.Clear(); } - var currWindow = this.CurrentChart.Windows[args.WindowIndex]; - RectangleF prevClipRectangle = gr.ClipBounds; - gr.SetClip(args.Rectangle); - try + protected override void OnUpdate(UpdateArgs args) { - PointF startPoint = new PointF(); - PointF endPoint = new PointF(); - PointF prevStartPoint = new PointF(); - PointF prevEndPoint = new PointF(); - PointF labelPoint = new PointF(); - PointF prevlabelPoint = new PointF(); - Pen currPen = null; - Pen resistPen = null; - string labelText = ""; - MaxOrMin currTrend = MaxOrMin.Nothing; - bool changed = false; - Brush labelBrush = new SolidBrush(Color.White); - for (int i = 1; i < pivots.Count; i++) + if (this.Count < Period * 2 + 1) + return; + int currBarIndex = this.Count - Period - 1; + MaxOrMin maxOrMin = LocalMaxOrMin(currBarIndex); + if (maxOrMin != MaxOrMin.Nothing && (pivots.Count < 1 || pivots[pivots.Count - 1].index != currBarIndex)) { - for (int j = 1; j <=2; j++) + this.pivots.Add(new PivotPoint(maxOrMin, currBarIndex)); + if (this.pivots[this.pivots.Count - 1].MaxOrMin == MaxOrMin.LocalMaximum) { - if (i + j >= pivots.Count-1) - break; - var p1 = pivots[i]; - var p2 = pivots[i+j]; - if (p1.MaxOrMin == p2.MaxOrMin) + this.pivots[this.pivots.Count - 1].value = ((HistoryItemBar)this.HistoricalData[this.Count - this.Period - 1, SeekOriginHistory.Begin]).High; + } + else + this.pivots[this.pivots.Count - 1].value = ((HistoryItemBar)this.HistoricalData[this.Count - this.Period - 1, SeekOriginHistory.Begin]).Low; + } + } + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + + if (this.CurrentChart == null) + return; + var gr = args.Graphics; + if (this.HistoricalData.Aggregation is HistoryAggregationTick) + { + gr.DrawString("Indicator does not work on one tick aggregation", new Font("Arial", 20), Brushes.Red, 0, 0); + return; + } + Period currentPeriod; + if (this.HistoricalData.Aggregation is HistoryAggregationTime historyAggregationTime) + { + currentPeriod = ((HistoryAggregationTime)this.HistoricalData.Aggregation).Period; + } + if (this.HistoricalData.Aggregation is HistoryAggregationTickBars historyAggregationTickBars) + { + currentPeriod = ((HistoryAggregationTickBars)this.HistoricalData.Aggregation).DefaultRange; + } + var currWindow = this.CurrentChart.Windows[args.WindowIndex]; + RectangleF prevClipRectangle = gr.ClipBounds; + gr.SetClip(args.Rectangle); + try + { + PointF startPoint = new PointF(); + PointF endPoint = new PointF(); + PointF prevStartPoint = new PointF(); + PointF prevEndPoint = new PointF(); + PointF labelPoint = new PointF(); + PointF prevlabelPoint = new PointF(); + Pen currPen = null; + Pen resistPen = null; + string labelText = ""; + MaxOrMin currTrend = MaxOrMin.Nothing; + bool changed = false; + Brush labelBrush = new SolidBrush(Color.White); + for (int i = 1; i < pivots.Count; i++) + { + var currPivot = pivots[i]; + int endIndex = currPivot.index; + bool isFinished = false; + for (int j = currPivot.index; j < this.HistoricalData.Count; j++) { - if (p1.MaxOrMin == MaxOrMin.LocalMaximum && p2.value > p1.value) - { - currPen = BullishPen; - } - else if (p1.MaxOrMin == MaxOrMin.LocalMinimum && p2.value < p1.value) + var currBar = (HistoryItemBar)this.HistoricalData[j, SeekOriginHistory.Begin]; + if ((currPivot.value < currBar.Close && currPivot.value > currBar.Open) || (currPivot.value > currBar.Close && currPivot.value < currBar.Open)) { - currPen = this.BearishPen; + if (currPivot.MaxOrMin == MaxOrMin.LocalMaximum && currBar.High > currPivot.value) + { + currPen = BullishPen; + endIndex = j; + isFinished = true; + } + else if (currPivot.MaxOrMin == MaxOrMin.LocalMinimum && currBar.Low < currPivot.value) + { + currPen = this.BearishPen; + endIndex = j; + isFinished = true; + } + else + { + isFinished = false; + } + break; } - else - continue; - startPoint.Y = (float)currWindow.CoordinatesConverter.GetChartY(p1.value); + } + if (isFinished) + { + startPoint.Y = (float)currWindow.CoordinatesConverter.GetChartY(currPivot.value); endPoint.Y = startPoint.Y; - startPoint.X = (float)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[p1.index, SeekOriginHistory.Begin].TimeLeft) + this.CurrentChart.BarsWidth / 2; - endPoint.X = (float)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[p2.index, SeekOriginHistory.Begin].TimeLeft) + this.CurrentChart.BarsWidth / 2; + startPoint.X = (float)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[currPivot.index, SeekOriginHistory.Begin].TimeLeft) + this.CurrentChart.BarsWidth / 2; + endPoint.X = (float)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[endIndex, SeekOriginHistory.Begin].TimeLeft) + this.CurrentChart.BarsWidth / 2; if (showLabel) switch (labelLocation) { @@ -148,7 +170,7 @@ public override void OnPaintChart(PaintChartEventArgs args) break; } labelBrush = new SolidBrush(currPen.Color); - if (p1.MaxOrMin == currTrend) + if (currPivot.MaxOrMin == currTrend && ((currTrend == MaxOrMin.LocalMaximum && endPoint.Y < prevEndPoint.Y) || (currTrend == MaxOrMin.LocalMinimum && endPoint.Y > prevEndPoint.Y))) { gr.DrawLine(currPen, startPoint, endPoint); if (showLabel) @@ -171,198 +193,216 @@ public override void OnPaintChart(PaintChartEventArgs args) } } - break; } else { - currTrend = p1.MaxOrMin; + currTrend = currPivot.MaxOrMin; changed = true; prevStartPoint = startPoint; prevEndPoint = endPoint; prevlabelPoint = labelPoint; if (showResist) { - if (p1.MaxOrMin == MaxOrMin.LocalMaximum) + if (currPivot.MaxOrMin == MaxOrMin.LocalMaximum) resistPen = BullishResistPen; else resistPen = BearishResistPen; gr.DrawLine(resistPen, startPoint, endPoint); } - break; } } } } + finally + { + gr.SetClip(prevClipRectangle); + } } - finally - { - gr.SetClip(prevClipRectangle); - } - } - private MaxOrMin LocalMaxOrMin(int index = 0) - { - MaxOrMin maxOrMin = MaxOrMin.Nothing; - for (int i = 1; i <= this.Period; i++) + private MaxOrMin LocalMaxOrMin(int index = 0) { - HistoryItemBar leftBar = (HistoryItemBar)this.HistoricalData[index - i, SeekOriginHistory.Begin]; - HistoryItemBar rightBar = (HistoryItemBar)this.HistoricalData[index + i, SeekOriginHistory.Begin]; - HistoryItemBar currentBar = (HistoryItemBar)this.HistoricalData[index, SeekOriginHistory.Begin]; - if (leftBar.High <= currentBar.High && rightBar.High <= currentBar.High) + MaxOrMin maxOrMin = MaxOrMin.Nothing; + for (int i = 1; i <= this.Period; i++) { - if (maxOrMin == MaxOrMin.LocalMaximum || maxOrMin == MaxOrMin.Nothing) - maxOrMin = MaxOrMin.LocalMaximum; - else + HistoryItemBar leftBar = (HistoryItemBar)this.HistoricalData[index - i, SeekOriginHistory.Begin]; + HistoryItemBar rightBar = (HistoryItemBar)this.HistoricalData[index + i, SeekOriginHistory.Begin]; + HistoryItemBar currentBar = (HistoryItemBar)this.HistoricalData[index, SeekOriginHistory.Begin]; + if (leftBar.High <= currentBar.High && rightBar.High <= currentBar.High) { - maxOrMin = MaxOrMin.Nothing; - break; + if (maxOrMin == MaxOrMin.LocalMaximum || maxOrMin == MaxOrMin.Nothing) + maxOrMin = MaxOrMin.LocalMaximum; + else + { + maxOrMin = MaxOrMin.Nothing; + break; + } + } + else if (leftBar.Low >= currentBar.Low && rightBar.Low >= currentBar.Low) + { + if (maxOrMin == MaxOrMin.LocalMinimum || maxOrMin == MaxOrMin.Nothing) + maxOrMin = MaxOrMin.LocalMinimum; + else + { + maxOrMin = MaxOrMin.Nothing; + break; + } } - } - else if (leftBar.Low >= currentBar.Low && rightBar.Low >= currentBar.Low) - { - if (maxOrMin == MaxOrMin.LocalMinimum || maxOrMin == MaxOrMin.Nothing) - maxOrMin = MaxOrMin.LocalMinimum; else { maxOrMin = MaxOrMin.Nothing; break; } } - else - { - maxOrMin = MaxOrMin.Nothing; - break; - } - } - return maxOrMin; - } - public override IList Settings - { - get - { - var settings = base.Settings; - settings.Add(new SettingItemInteger("Period", this.Period) - { - Text = "Period", - SortIndex = 1, - Minimum = 2, - }); - settings.Add(new SettingItemLineOptions("BullishOptions", this.BullishOptions) - { - Text = "Bullish Line Style", - SortIndex = 2, - ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, - UseEnabilityToggler = true - }); - settings.Add(new SettingItemLineOptions("BearishOptions", this.BearishOptions) - { - Text = "Bearish Line Style", - SortIndex= 2, - ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, - UseEnabilityToggler = true - }); - settings.Add(new SettingItemBoolean("showLabel", this.showLabel) - { - Text = "Show Label", - SortIndex = 2 - }); - SettingItemRelationVisibility visibleLabel = new SettingItemRelationVisibility("showLabel", true); - settings.Add(new SettingItemFont("Font", this.labelFont) - { - Text = "Label Font", - SortIndex = 2, - Relation = visibleLabel - }); - settings.Add(new SettingItemSelectorLocalized("labelLocation", this.labelLocation, new List { new SelectItem("Start", LabelLocation.Start), new SelectItem("Middle", LabelLocation.Middle), new SelectItem("End", LabelLocation.End) }) - { - Text = "Label Location", - SortIndex = 2, - Relation = visibleLabel - }); - settings.Add(new SettingItemBoolean("showResist", this.showResist) - { - Text = "Show Resistance", - SortIndex = 2 - }); - SettingItemRelationVisibility visibleResistance = new SettingItemRelationVisibility("showResist", true); - settings.Add(new SettingItemLineOptions("BullishResistOptions", this.BullishResistOptions) - { - Text = "Bullish Resistance Style", - SortIndex = 2, - ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, - UseEnabilityToggler = true, - Relation = visibleResistance - }); - settings.Add(new SettingItemLineOptions("BearishResistOptions", this.BearishResistOptions) - { - Text = "Bearish Resistance Style", - SortIndex = 2, - ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, - UseEnabilityToggler = true, - Relation=visibleResistance - }); - return settings; + return maxOrMin; } - set + public override IList Settings { - if (value.TryGetValue("Period", out int Period)) - this.Period = Period; - if (value.TryGetValue("BullishOptions", out LineOptions BullishOptions)) - { - this.BullishPen.Width = BullishOptions.Width; - this.BullishPen.Color = BullishOptions.Color; - this.BullishPen.DashStyle = (DashStyle)BullishOptions.LineStyle; - } - if (value.TryGetValue("BearishOptions", out LineOptions BearishOptions)) + get { - this.BearishPen.Width = BearishOptions.Width; - this.BearishPen.Color = BearishOptions.Color; - this.BearishPen.DashStyle = (DashStyle)BearishOptions.LineStyle; - } - if (value.TryGetValue("showLabel", out bool showLabel)) - this.showLabel = showLabel; - if (value.TryGetValue("Font", out Font labelFont)) - this.labelFont = labelFont; - if (value.TryGetValue("showResist", out bool showResist)) - this.showResist = showResist; - if (value.TryGetValue("BullishResistOptions", out LineOptions BullishResistOptions)) - { - this.BullishResistPen.Width = BullishResistOptions.Width; - this.BullishResistPen.Color = BullishResistOptions.Color; - this.BullishResistPen.DashStyle = (DashStyle)BullishResistOptions.LineStyle; + var settings = base.Settings; + settings.Add(new SettingItemInteger("Period", this.Period) + { + Text = "Period", + SortIndex = 1, + Minimum = 2, + }); + settings.Add(new SettingItemLineOptions("BullishOptions", this.BullishOptions) + { + Text = "Bullish Line Style", + SortIndex = 2, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true + }); + settings.Add(new SettingItemLineOptions("BearishOptions", this.BearishOptions) + { + Text = "Bearish Line Style", + SortIndex = 2, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true + }); + settings.Add(new SettingItemBoolean("showLabel", this.showLabel) + { + Text = "Show Label", + SortIndex = 2 + }); + SettingItemRelationVisibility visibleLabel = new SettingItemRelationVisibility("showLabel", true); + settings.Add(new SettingItemFont("Font", this.labelFont) + { + Text = "Label Font", + SortIndex = 2, + Relation = visibleLabel + }); + settings.Add(new SettingItemSelectorLocalized("labelLocation", this.labelLocation, new List { new SelectItem("Start", LabelLocation.Start), new SelectItem("Middle", LabelLocation.Middle), new SelectItem("End", LabelLocation.End) }) + { + Text = "Label Location", + SortIndex = 2, + Relation = visibleLabel + }); + settings.Add(new SettingItemBoolean("showResist", this.showResist) + { + Text = "Show Resistance", + SortIndex = 2 + }); + SettingItemRelationVisibility visibleResistance = new SettingItemRelationVisibility("showResist", true); + settings.Add(new SettingItemLineOptions("BullishResistOptions", this.BullishResistOptions) + { + Text = "Bullish Resistance Style", + SortIndex = 2, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true, + Relation = visibleResistance + }); + settings.Add(new SettingItemLineOptions("BearishResistOptions", this.BearishResistOptions) + { + Text = "Bearish Resistance Style", + SortIndex = 2, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true, + Relation = visibleResistance + }); + + return settings; } - if (value.TryGetValue("BearishResistOptions", out LineOptions BearishResistOptions)) + set { - this.BearishResistPen.Width = BearishResistOptions.Width; - this.BearishResistPen.Color = BearishResistOptions.Color; - this.BearishResistPen.DashStyle = (DashStyle)BearishResistOptions.LineStyle; + base.Settings = value; + if (value.TryGetValue("Period", out int Period)) + this.Period = Period; + if (value.TryGetValue("BullishOptions", out LineOptions BullishOptions)) + { + this.BullishOptions = BullishOptions; + this.BullishPen.Width = BullishOptions.Width; + this.BullishPen.Color = BullishOptions.Color; + this.BullishPen.DashStyle = (DashStyle)BullishOptions.LineStyle; + } + if (value.TryGetValue("BearishOptions", out LineOptions BearishOptions)) + { + this.BearishOptions = BearishOptions; + this.BearishPen.Width = BearishOptions.Width; + this.BearishPen.Color = BearishOptions.Color; + this.BearishPen.DashStyle = (DashStyle)BearishOptions.LineStyle; + } + if (value.TryGetValue("showLabel", out bool showLabel)) + this.showLabel = showLabel; + if (value.TryGetValue("Font", out Font labelFont)) + this.labelFont = labelFont; + if (value.TryGetValue("showResist", out bool showResist)) + this.showResist = showResist; + if (value.TryGetValue("BullishResistOptions", out LineOptions BullishResistOptions)) + { + this.BullishResistOptions = BullishResistOptions; + this.BullishResistPen.Width = BullishResistOptions.Width; + this.BullishResistPen.Color = BullishResistOptions.Color; + this.BullishResistPen.DashStyle = (DashStyle)BullishResistOptions.LineStyle; + } + if (value.TryGetValue("BearishResistOptions", out LineOptions BearishResistOptions)) + { + this.BearishResistOptions = BearishResistOptions; + this.BearishResistPen.Width = BearishResistOptions.Width; + this.BearishResistPen.Color = BearishResistOptions.Color; + this.BearishResistPen.DashStyle = (DashStyle)BearishResistOptions.LineStyle; + } + if (value.TryGetValue("labelLocation", out LabelLocation labelLocation)) + this.labelLocation = labelLocation; + this.OnSettingsUpdated(); } - if (value.TryGetValue("labelLocation", out LabelLocation labelLocation)) - this.labelLocation = labelLocation; - this.OnSettingsUpdated(); + } + private static LineOptions CreateLineOptions(Pen pen) => new() + { + Color = pen.Color, + Width = (int)pen.Width, + LineStyle = (LineStyle)pen.DashStyle, + WithCheckBox = false + }; + + private static void ApplyLineOptions(Pen pen, LineOptions options) + { + pen.Color = options.Color; + pen.Width = options.Width; + pen.DashStyle = (DashStyle)options.LineStyle; } } -} -internal enum MaxOrMin -{ - LocalMaximum, - LocalMinimum, - Nothing -} -internal enum LabelLocation -{ - Start, - Middle, - End -} -internal class PivotPoint -{ - public MaxOrMin MaxOrMin { get; set; } - public int index { get; set; } - public double value { get; set; } - public PivotPoint(MaxOrMin maxOrMin, int index) + internal enum MaxOrMin + { + LocalMaximum, + LocalMinimum, + Nothing + } + internal enum LabelLocation { - this.MaxOrMin = maxOrMin; - this.index = index; + Start, + Middle, + End + } + internal class PivotPoint + { + public MaxOrMin MaxOrMin { get; set; } + public int index { get; set; } + public double value { get; set; } + public PivotPoint(MaxOrMin maxOrMin, int index) + { + this.MaxOrMin = maxOrMin; + this.index = index; + } } } \ No newline at end of file diff --git a/Indicators/IndicatorMovingAverageConvergenceDivergence.cs b/Indicators/IndicatorMovingAverageConvergenceDivergence.cs index f71d516..bc17c2f 100644 --- a/Indicators/IndicatorMovingAverageConvergenceDivergence.cs +++ b/Indicators/IndicatorMovingAverageConvergenceDivergence.cs @@ -1,7 +1,10 @@ // Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. using System; +using System.Collections.Generic; using System.Drawing; +using System.IO; +using System.Linq; using TradingPlatform.BusinessLayer; namespace Oscillators; @@ -38,6 +41,11 @@ public sealed class IndicatorMovingAverageConvergenceDivergence : Indicator, IWa private Indicator sma; private HistoricalDataCustom customHD; + private Color level1_Color; + private Color level2_Color; + private Color level3_Color; + private Color level4_Color; + /// /// Indicator's constructor. Contains general information: name, description, LineSeries etc. /// @@ -49,11 +57,16 @@ public IndicatorMovingAverageConvergenceDivergence() this.Description = "A trend-following momentum indicator that shows the relationship between two moving averages of prices"; // Defines line on demand with particular parameters. - this.AddLineSeries("MACD", Color.DodgerBlue, 1, LineStyle.Solid); - this.AddLineSeries("Signal", Color.Red, 1, LineStyle.Solid); this.AddLineSeries("OsMA", Color.Green, 4, LineStyle.Histogramm); this.AddLineLevel(0, "0 level", Color.DarkGreen, 1, LineStyle.Solid); + this.AddLineSeries("MACD", Color.DodgerBlue, 1, LineStyle.Solid); + this.AddLineSeries("Signal", Color.Red, 1, LineStyle.Solid); this.SeparateWindow = true; + + this.level1_Color = Color.FromArgb(0, 178, 89); + this.level2_Color = Color.FromArgb(50, this.level1_Color); + this.level3_Color = Color.FromArgb(251, 87, 87); + this.level4_Color = Color.FromArgb(50, this.level3_Color); } /// @@ -93,7 +106,7 @@ protected override void OnUpdate(UpdateArgs args) // Calculate a difference bettwen two EMA indicators and set value to 'MACD' line buffer. double differ = this.fastEMA.GetValue() - this.slowEMA.GetValue(); - this.SetValue(differ); + this.SetValue(differ, 1); // The calculated value must be set as close price against the custom HistoricalData, // because the SMA indicator was initialized with the source price - PriceType.Close. @@ -108,9 +121,73 @@ protected override void OnUpdate(UpdateArgs args) return; // Set value to the 'Signal' line buffer. - this.SetValue(signal, 1); + this.SetValue(signal, 2); // Set value to the 'OsMA' line buffer. - this.SetValue(differ - signal, 2); + this.SetValue(differ - signal, 0); + + var osMAValue = differ - signal; + if (osMAValue > 0) + this.LinesSeries[0].SetMarker(0, osMAValue > this.LinesSeries[0].GetValue(1) ? this.level1_Color : this.level2_Color); + else + this.LinesSeries[0].SetMarker(0, osMAValue < this.LinesSeries[0].GetValue(1) ? this.level3_Color : this.level4_Color); + } + + public override IList Settings + { + get + { + var settings = base.Settings; + + if (settings.GetItemByName("Line_0") is SettingItemGroup lineGroup) + { + var items = lineGroup.Value as IList; + var separatorGeoup = items?.FirstOrDefault()?.SeparatorGroup; + + lineGroup.AddItem(new SettingItemColor("Color 1", this.level1_Color, 1) { ColorText = loc._("Color"), SeparatorGroup = separatorGeoup }); + lineGroup.AddItem(new SettingItemColor("Color 2", this.level2_Color, 1) { ColorText = loc._("Color"), SeparatorGroup = separatorGeoup }); + lineGroup.AddItem(new SettingItemColor("Color 3", this.level3_Color, 1) { ColorText = loc._("Color"), SeparatorGroup = separatorGeoup }); + lineGroup.AddItem(new SettingItemColor("Color 4", this.level4_Color, 1) { ColorText = loc._("Color"), SeparatorGroup = separatorGeoup }); + } + + return settings; + } + set + { + base.Settings = value; + + if (value.GetItemByName("Line_2") is SettingItemGroup lineGroup) + { + bool needUpdate = false; + var colorsHolder = new SettingsHolder(lineGroup.Value as IList); + + if (colorsHolder.TryGetValue("Color 1", out var item)) + { + this.level1_Color = item.GetValue(); + needUpdate |= true; + } + + if (colorsHolder.TryGetValue("Color 2", out item)) + { + this.level2_Color = item.GetValue(); + needUpdate |= true; + } + + if (colorsHolder.TryGetValue("Color 3", out item)) + { + this.level3_Color = item.GetValue(); + needUpdate |= true; + } + + if (colorsHolder.TryGetValue("Color 4", out item)) + { + this.level4_Color = item.GetValue(); + needUpdate |= true; + } + + if (needUpdate) + this.OnSettingsUpdated(); + } + } } } \ No newline at end of file diff --git a/Indicators/IndicatorOpeningRange.cs b/Indicators/IndicatorOpeningRange.cs index 5338c22..8fc2fcd 100644 --- a/Indicators/IndicatorOpeningRange.cs +++ b/Indicators/IndicatorOpeningRange.cs @@ -254,8 +254,10 @@ private void UpdateIndicatorLines(double high, double low, int startIndex, int e if (startIndex < 0) startIndex = 0; - int startOffset = this.GetOffset(startIndex); + int startOffset = this.GetOffset(startIndex) + 1; int endOffset = this.GetOffset(endIndex); + if (startOffset >= this.HistoricalData.Count) + startOffset = this.HistoricalData.Count - 1; for (int i = endOffset; i <= startOffset; i++) { @@ -511,7 +513,7 @@ public void Reload(bool forceReload = false) ForceReload = forceReload, })); } - + // middle part if (!token.IsCancellationRequested && ceilingStartDT < floorEndDT) { @@ -539,7 +541,7 @@ public void Reload(bool forceReload = false) ForceReload = forceReload, })); } - + // if (!token.IsCancellationRequested) { diff --git a/Indicators/IndicatorPivotPoint.cs b/Indicators/IndicatorPivotPoint.cs index 030b252..b2c7b6d 100644 --- a/Indicators/IndicatorPivotPoint.cs +++ b/Indicators/IndicatorPivotPoint.cs @@ -17,6 +17,7 @@ public class IndicatorPivotPoint : Indicator, IWatchlistIndicator private const string BASE_PERIOD_INPUT_PARAMETER = "Base period"; private const string RANGE_INPUT_PARAMETER = "Range"; private const string CALCULATION_METHOD_INPUT_PARAMETER = "Calculation method"; + private const int MIN_HISTORY_COUNT = 3; private const int PP_LINE_INDEX = 0; @@ -35,32 +36,56 @@ public class IndicatorPivotPoint : Indicator, IWatchlistIndicator private const int S5_LINE_INDEX = 11; private const int S6_LINE_INDEX = 12; - [InputParameter(CURRENT_PERIOD_INPUT_PARAMETER, 0)] - public bool OnlyCurrentPeriod = false; + private const int MID_PP_R1 = 13; + private const int MID_R1_R2 = 14; + private const int MID_R2_R3 = 15; + private const int MID_R3_R4 = 16; + private const int MID_R4_R5 = 17; + private const int MID_R5_R6 = 18; + private const int MID_PP_S1 = 19; + private const int MID_S1_S2 = 20; + private const int MID_S2_S3 = 21; + private const int MID_S3_S4 = 22; + private const int MID_S4_S5 = 23; + private const int MID_S5_S6 = 24; - [InputParameter(BASE_PERIOD_INPUT_PARAMETER, 1, variants: new object[] + public bool OnlyCurrentPeriod = false; + public string CustomSessionName = string.Empty; + public DateTime CustomRangeStartTime { - "Hour", BasePeriod.Hour, - "Day", BasePeriod.Day, - "Week", BasePeriod.Week, - "Month", BasePeriod.Month - })] - public BasePeriod BasePeriod = BasePeriod.Day; + get + { + if (this.customRangeStartTime == default) + { + var session = this.CreateDefaultSession(); + this.customRangeStartTime = Core.Instance.TimeUtils.DateTimeUtcNow.Date.AddTicks(session.OpenTime.Ticks); + } + return this.customRangeStartTime; + } + set => this.customRangeStartTime = value; + } + private DateTime customRangeStartTime; + public DateTime CustomRangeEndTime + { + get + { + if (this.customRangeEndTime == default) + this.customRangeEndTime = this.CustomRangeStartTime; - [InputParameter(RANGE_INPUT_PARAMETER, 2, 1, 60, 1, 0)] + return this.customRangeEndTime; + } + set => this.customRangeEndTime = value; + } + private DateTime customRangeEndTime; + public BasePeriod BasePeriod = BasePeriod.Day; public int PeriodValue = 1; - - [InputParameter(CALCULATION_METHOD_INPUT_PARAMETER, 3, variants: new object[] - { - "Classic", CalculationMethod.Classic, - "Camarilla", CalculationMethod.Camarilla, - "Fibonacci", CalculationMethod.Fibonacci, - "Woodie", CalculationMethod.Woodie, - "DeMark", CalculationMethod.DeMark - })] public CalculationMethod IndicatorCalculationMethod = CalculationMethod.Classic; - + public bool ShowMidPivots = true; + public DailySessionType DailySessionType = DailySessionType.AllDay; + private readonly Color MidColor = Color.FromArgb(128, 128, 128, 128); private Task loadingTask; + private ISession currentSession; + private ISessionsContainer chartSessionContainer; private CancellationTokenSource cancellationSource; private HistoricalData history; private string formattedPeriod; @@ -129,7 +154,19 @@ public IndicatorPivotPoint() this.AddLineSeries("S6", Color.DodgerBlue, 1, LineStyle.Solid); // 12 this.SeparateWindow = false; - + this.AddLineSeries("R1-PP", MidColor, 1, LineStyle.Solid); // 13 + this.AddLineSeries("R2-R1", MidColor, 1, LineStyle.Solid); // 14 + this.AddLineSeries("R3-R2", MidColor, 1, LineStyle.Solid); // 15 + this.AddLineSeries("R4-R3", MidColor, 1, LineStyle.Solid); // 16 + this.AddLineSeries("R5-R4", MidColor, 1, LineStyle.Solid); // 17 + this.AddLineSeries("R6-R5", MidColor, 1, LineStyle.Solid); // 18 + + this.AddLineSeries("S1-PP", MidColor, 1, LineStyle.Solid); // 19 + this.AddLineSeries("S2-S1", MidColor, 1, LineStyle.Solid); // 20 + this.AddLineSeries("S3-S2", MidColor, 1, LineStyle.Solid); // 21 + this.AddLineSeries("S4-S3", MidColor, 1, LineStyle.Solid); // 22 + this.AddLineSeries("S5-S4", MidColor, 1, LineStyle.Solid); // 23 + this.AddLineSeries("S6-S5", MidColor, 1, LineStyle.Solid); // 24 //this.font = new Font("Tahoma", 8, FontStyle.Bold); //this.centerCenterSF = new StringFormat() { LineAlignment = StringAlignment.Center, Alignment = StringAlignment.Center }; //this.messageBrush = new SolidBrush(Color.DodgerBlue); @@ -139,12 +176,28 @@ public IndicatorPivotPoint() protected override void OnInit() { base.OnInit(); - this.AbortPreviousTask(); var token = this.cancellationSource.Token; if (this.Symbol == null) return; + if (this.DailySessionType == DailySessionType.AllDay) + { + // default session + this.currentSession = this.CreateDefaultSession(); + } + else if (this.DailySessionType == DailySessionType.SpecifiedSession) + { + if (!string.IsNullOrEmpty(this.CustomSessionName)) + { + // selected chart session + var sessions = this.GetAvailableCustomChartSessions().Concat(this.GetAvailableSymbolSessions()).ToList(); + if (sessions.Count > 0) + this.currentSession = sessions.FirstOrDefault(s => s.Name.Equals(this.CustomSessionName) && s.Type == SessionType.Main); + } + } + else if (this.DailySessionType == DailySessionType.CustomRange) + this.currentSession = new Session("Custom session", this.CustomRangeStartTime.TimeOfDay, this.CustomRangeEndTime.TimeOfDay); var inputPeriod = new Period(this.BasePeriod, this.PeriodValue); this.formattedPeriod = this.GetFormattedPeriod(this.BasePeriod, this.PeriodValue); @@ -183,7 +236,7 @@ protected override void OnInit() return; this.State = IndicatorState.Loading; - var fromTime = this.HistoricalData.FromTime; + var fromTime = this.HistoricalData.FromTime.Add(-2 * inputPeriod.Duration); var coefficient = 0; var prevHistoryCount = -1; @@ -243,6 +296,7 @@ protected override void OnUpdate(UpdateArgs args) { base.OnUpdate(args); + if (args.Reason == UpdateReason.NewBar && this.State == IndicatorState.Calculation) this.DrawIndicatorLines(this.lastPivotPeriod, 0, 0); } @@ -260,15 +314,176 @@ public override IList Settings { var settings = base.Settings; - if (settings.GetItemByName(RANGE_INPUT_PARAMETER) is SettingItemInteger rangeSi) - rangeSi.ValueChangingBehavior = SettingItemValueChangingBehavior.WithConfirmation; + var hour = new SelectItem("Hour", BasePeriod.Hour); + var day = new SelectItem("Day", BasePeriod.Day); + var week = new SelectItem("Week", BasePeriod.Week); + var month = new SelectItem("Month", BasePeriod.Month); + + var classic = new SelectItem("Classic", CalculationMethod.Classic); + var camarilla = new SelectItem("Camarilla", CalculationMethod.Camarilla); + var fibonacci = new SelectItem("Fibonacci", CalculationMethod.Fibonacci); + var woodie = new SelectItem("Woodie", CalculationMethod.Woodie); + var demark = new SelectItem("DeMark", CalculationMethod.DeMark); + + var defaultSeparator = settings.FirstOrDefault()?.SeparatorGroup; + var allDay = new SelectItem("All day", DailySessionType.AllDay); + var specifiedSession = new SelectItem("Specified session", DailySessionType.SpecifiedSession); + var customRange = new SelectItem("Custom range", DailySessionType.CustomRange); + + var dailyPeriodTypeRelation = new SettingItemRelationVisibility(BASE_PERIOD_INPUT_PARAMETER, day); + // Only current period + settings.Add(new SettingItemBoolean(CURRENT_PERIOD_INPUT_PARAMETER, this.OnlyCurrentPeriod, 0) + { + SeparatorGroup = defaultSeparator, + Text = loc._(CURRENT_PERIOD_INPUT_PARAMETER) + }); + + // Base period + settings.Add(new SettingItemSelectorLocalized( + BASE_PERIOD_INPUT_PARAMETER, + new SelectItem(BASE_PERIOD_INPUT_PARAMETER, this.BasePeriod), + new List { hour, day, week, month }) + { + SeparatorGroup = defaultSeparator, + Text = loc._(BASE_PERIOD_INPUT_PARAMETER), + SortIndex = 5, + ValueChangingBehavior = SettingItemValueChangingBehavior.WithConfirmation + }); + settings.Add(new SettingItemSelectorLocalized("Session type", new SelectItem("Session type", this.DailySessionType), new List + { + allDay, + specifiedSession, + customRange + }) + { + SeparatorGroup = defaultSeparator, + Text = "Session type", + SortIndex = 10, + Relation = dailyPeriodTypeRelation, + }); + // + var customRangeSimRelation = new SettingItemRelationVisibility("Session type", customRange); + var customRangeMultRelation = new SettingItemMultipleRelation(dailyPeriodTypeRelation, customRangeSimRelation); + settings.Add(new SettingItemString("Custom session name", this.CustomSessionName, 20) + { + SeparatorGroup = defaultSeparator, + Text = loc._("Custom session name"), + Relation = new SettingItemRelationVisibility("Session type", specifiedSession) + }); + settings.Add(new SettingItemDateTime("Start time", this.customRangeStartTime, 20) + { + SeparatorGroup = defaultSeparator, + Text = loc._("Start time"), + Format = DatePickerFormat.LongTime, + ValueChangingBehavior = SettingItemValueChangingBehavior.WithConfirmation, + Relation = customRangeMultRelation + }); + settings.Add(new SettingItemDateTime("End time", this.customRangeEndTime, 20) + { + SeparatorGroup = defaultSeparator, + Text = loc._("End time"), + Format = DatePickerFormat.LongTime, + ValueChangingBehavior = SettingItemValueChangingBehavior.WithConfirmation, + Relation = customRangeMultRelation + }); + settings.Add(new SettingItemInteger(RANGE_INPUT_PARAMETER, this.PeriodValue, 10) + { + SeparatorGroup = defaultSeparator, + Text = loc._(RANGE_INPUT_PARAMETER), + ValueChangingBehavior = SettingItemValueChangingBehavior.WithConfirmation + }); + + // Calculation method + settings.Add(new SettingItemSelectorLocalized( + CALCULATION_METHOD_INPUT_PARAMETER, + new SelectItem(CALCULATION_METHOD_INPUT_PARAMETER, this.IndicatorCalculationMethod), + new List { classic, camarilla, fibonacci, woodie, demark }) + { + SeparatorGroup = defaultSeparator, + Text = loc._(CALCULATION_METHOD_INPUT_PARAMETER), + SortIndex = 20 + }); - if (settings.GetItemByName(BASE_PERIOD_INPUT_PARAMETER) is SettingItemSelectorLocalized periodSi) - periodSi.ValueChangingBehavior = SettingItemValueChangingBehavior.WithConfirmation; + // Show mid pivot points + settings.Add(new SettingItemBoolean("Show mid pivot points", this.ShowMidPivots, 90) + { + SeparatorGroup = defaultSeparator, + Text = loc._("Show mid pivot points") + }); return settings; } + set + { + var holder = new SettingsHolder(value); + + var needRefresh = false; + + if (holder.TryGetValue(CURRENT_PERIOD_INPUT_PARAMETER, out var item) && item.Value is bool onlyCurrent) + { + if (this.OnlyCurrentPeriod != onlyCurrent) + { + this.OnlyCurrentPeriod = onlyCurrent; + // Если меняли вручную — обновим расчёты + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + + if (holder.TryGetValue(BASE_PERIOD_INPUT_PARAMETER, out item)) + { + var newBase = item.GetValue(); + if (this.BasePeriod != newBase) + { + this.BasePeriod = newBase; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + + if (holder.TryGetValue(RANGE_INPUT_PARAMETER, out item) && item.Value is int range) + { + if (this.PeriodValue != range) + { + this.PeriodValue = range; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + + if (holder.TryGetValue(CALCULATION_METHOD_INPUT_PARAMETER, out item)) + { + var newMethod = item.GetValue(); + if (this.IndicatorCalculationMethod != newMethod) + { + this.IndicatorCalculationMethod = newMethod; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + } + if (holder.TryGetValue("Session type", out item) && item.GetValue() != this.DailySessionType) + { + this.DailySessionType = item.GetValue(); + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + if (holder.TryGetValue("Custom session name", out item) && item.Value is string customSessionName) + this.CustomSessionName = customSessionName; + if (holder.TryGetValue("Start time", out item) && item.Value is DateTime dtStartTime) + { + this.customRangeStartTime = dtStartTime; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + if (holder.TryGetValue("End time", out item) && item.Value is DateTime dtEndTime) + { + this.customRangeEndTime = dtEndTime; + needRefresh |= item.ValueChangingReason == SettingItemValueChangingReason.Manually; + } + if (holder.TryGetValue("Show mid pivot points", out item) && item.Value is bool showMid) + this.ShowMidPivots = showMid; + + if (needRefresh) + this.Refresh(); + + base.Settings = value; + } } + public override void OnPaintChart(PaintChartEventArgs args) { base.OnPaintChart(args); @@ -478,30 +693,78 @@ private void DrawIndicatorLines(PivotPointCalculationResponce ppItem, int fromIn { if (ppItem == null) return; - + double pp = ppItem.PP; + double r1 = ppItem.R1, r2 = ppItem.R2, r3 = ppItem.R3, + r4 = ppItem.R4, r5 = ppItem.R5, r6 = ppItem.R6; + double s1 = ppItem.S1, s2 = ppItem.S2, s3 = ppItem.S3, + s4 = ppItem.S4, s5 = ppItem.S5, s6 = ppItem.S6; for (int y = fromIndex; y >= toIndex; y--) { - this.SetValue(ppItem.PP, PP_LINE_INDEX, y); - this.SetValue(ppItem.R1, R1_LINE_INDEX, y); - this.SetValue(ppItem.S1, S1_LINE_INDEX, y); + var currentBarTime = this.HistoricalData[y].TimeLeft; + var inSession = this.currentSession.ContainsDate(currentBarTime); + if (!inSession) + continue; + this.SetValue(pp, PP_LINE_INDEX, y); + this.SetValue(r1, R1_LINE_INDEX, y); + this.SetValue(s1, S1_LINE_INDEX, y); if (ppItem.Method != CalculationMethod.DeMark) { - this.SetValue(ppItem.R2, R2_LINE_INDEX, y); - this.SetValue(ppItem.R3, R3_LINE_INDEX, y); - this.SetValue(ppItem.S2, S2_LINE_INDEX, y); - this.SetValue(ppItem.S3, S3_LINE_INDEX, y); + this.SetValue(r2, R2_LINE_INDEX, y); + this.SetValue(r3, R3_LINE_INDEX, y); + this.SetValue(s2, S2_LINE_INDEX, y); + this.SetValue(s3, S3_LINE_INDEX, y); } if (ppItem.Method == CalculationMethod.Camarilla) { - this.SetValue(ppItem.R4, R4_LINE_INDEX, y); - this.SetValue(ppItem.R5, R5_LINE_INDEX, y); - this.SetValue(ppItem.R6, R6_LINE_INDEX, y); + this.SetValue(r4, R4_LINE_INDEX, y); + this.SetValue(r5, R5_LINE_INDEX, y); + this.SetValue(r6, R6_LINE_INDEX, y); - this.SetValue(ppItem.S4, S4_LINE_INDEX, y); - this.SetValue(ppItem.S5, S5_LINE_INDEX, y); - this.SetValue(ppItem.S6, S6_LINE_INDEX, y); + this.SetValue(s4, S4_LINE_INDEX, y); + this.SetValue(s5, S5_LINE_INDEX, y); + this.SetValue(s6, S6_LINE_INDEX, y); + } + if (this.ShowMidPivots) + { + bool hasR2 = !double.IsNaN(r2) && r2 != 0.0; + bool hasR3 = !double.IsNaN(r3) && r3 != 0.0; + bool hasR4 = !double.IsNaN(r4) && r4 != 0.0; + bool hasR5 = !double.IsNaN(r5) && r5 != 0.0; + bool hasR6 = !double.IsNaN(r6) && r6 != 0.0; + + bool hasS2 = !double.IsNaN(s2) && s2 != 0.0; + bool hasS3 = !double.IsNaN(s3) && s3 != 0.0; + bool hasS4 = !double.IsNaN(s4) && s4 != 0.0; + bool hasS5 = !double.IsNaN(s5) && s5 != 0.0; + bool hasS6 = !double.IsNaN(s6) && s6 != 0.0; + + if (!double.IsNaN(r1) && r1 != 0.0) + this.SetValue(Mid(pp, r1), MID_PP_R1, y); + if (hasR2) + this.SetValue(Mid(r1, r2), MID_R1_R2, y); + if (hasR3) + this.SetValue(Mid(r2, r3), MID_R2_R3, y); + if (hasR4) + this.SetValue(Mid(r3, r4), MID_R3_R4, y); + if (hasR5) + this.SetValue(Mid(r4, r5), MID_R4_R5, y); + if (hasR6) + this.SetValue(Mid(r5, r6), MID_R5_R6, y); + + if (!double.IsNaN(s1) && s1 != 0.0) + this.SetValue(Mid(pp, s1), MID_PP_S1, y); + if (hasS2) + this.SetValue(Mid(s1, s2), MID_S1_S2, y); + if (hasS3) + this.SetValue(Mid(s2, s3), MID_S2_S3, y); + if (hasS4) + this.SetValue(Mid(s3, s4), MID_S3_S4, y); + if (hasS5) + this.SetValue(Mid(s4, s5), MID_S4_S5, y); + if (hasS6) + this.SetValue(Mid(s5, s6), MID_S5_S6, y); } } } @@ -509,7 +772,7 @@ private void DrawIndicatorLines(PivotPointCalculationResponce ppItem, int fromIn #endregion Drawing private void History_NewHistoryItem(object sender, HistoryEventArgs e) => this.CalculateLastPeriod(); - + private double Mid(double a, double b) => (a + b) * 0.5; #region Misc private void AbortPreviousTask() { @@ -542,6 +805,20 @@ private string GetFormattedPeriod(BasePeriod basePeriod, int range) return $"{range} {period}"; } + private Session CreateDefaultSession() + { + // 00:00 + var startTime = new DateTime(Core.Instance.TimeUtils.DateTimeUtcNow.Date.Ticks, DateTimeKind.Unspecified); + // 23:59:59 + var endTime = new DateTime(Core.Instance.TimeUtils.DateTimeUtcNow.Date.AddHours(23).AddMinutes(59).AddSeconds(59).Ticks, DateTimeKind.Unspecified); + + var timeZone = this.CurrentChart?.CurrentTimeZone ?? Core.Instance.TimeUtils.SelectedTimeZone; + return new Session("Default", + Core.Instance.TimeUtils.ConvertFromTimeZoneToUTC(startTime, timeZone).TimeOfDay, + Core.Instance.TimeUtils.ConvertFromTimeZoneToUTC(endTime, timeZone).TimeOfDay); + } + private IList GetAvailableCustomChartSessions() => this.chartSessionContainer?.ActiveSessions?.ToList() ?? new List(); + private IList GetAvailableSymbolSessions() => this.Symbol?.CurrentSessionsInfo?.ActiveSessions?.ToList() ?? new List(); #endregion Misc } @@ -589,4 +866,5 @@ public enum IndicatorState IncorrectPeriod, OneTickNotAllowed } +public enum DailySessionType { AllDay, SpecifiedSession, CustomRange, } #endregion Utils \ No newline at end of file diff --git a/Indicators/IndicatorPolynomialRegressionChannel.cs b/Indicators/IndicatorPolynomialRegressionChannel.cs new file mode 100644 index 0000000..c79a576 --- /dev/null +++ b/Indicators/IndicatorPolynomialRegressionChannel.cs @@ -0,0 +1,89 @@ +// Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. + +using MathNet.Numerics; +using MathNet.Numerics.Statistics; +using System; +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace Channels; + +public sealed class IndicatorPolynomialRegressionChannel : Indicator +{ + [InputParameter("Period of Hull Moving Average", 10, 1, 9999, 1, 0)] + public int MaPeriod; + + /// + /// Price type of moving average. + /// + [InputParameter("Sources prices for MA", 20, variants: new object[] + { + "Close", PriceType.Close, + "Open", PriceType.Open, + "High", PriceType.High, + "Low", PriceType.Low, + "Typical", PriceType.Typical, + "Medium", PriceType.Median, + "Weighted", PriceType.Weighted, + })] + public PriceType SourcePrice; + + [InputParameter("Type of moving average", 3, variants: new object[]{ + "Simple Moving Average", MaMode.SMA, + "Exponential Moving Average", MaMode.EMA, + "Smoothed Moving Average", MaMode.SMMA, + "Linearly Weighted Moving Average", MaMode.LWMA, + })] + public MaMode MaType; + + [InputParameter("Polynomial degree", 10, 1, 9999, 1, 0)] + public int polynomialDegree; + + private Indicator ma; + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorPolynomialRegressionChannel.cs"; + public IndicatorPolynomialRegressionChannel() + : base() + { + this.Name = "Polynomial Regression Channel"; + this.SeparateWindow = false; + this.MaPeriod = 20; + this.SourcePrice = PriceType.Close; + this.MaType = MaMode.SMA; + this.polynomialDegree = 2; + + this.AddLineSeries("Upper Band", Color.Green, 1, LineStyle.Solid); + this.AddLineSeries("Lower Band", Color.Red, 1, LineStyle.Solid); + this.AddLineSeries("Regression", Color.CadetBlue, 1, LineStyle.Solid); + } + protected override void OnInit() + { + this.ma = Core.Instance.Indicators.BuiltIn.MA(this.MaPeriod, this.SourcePrice, this.MaType); + this.AddIndicator(this.ma); + } + protected override void OnUpdate(UpdateArgs args) + { + double[] xData = new double[this.MaPeriod]; + double[] yData = new double[this.MaPeriod]; + for (int i = 0; i < this.MaPeriod; i++) + { + xData[i] = i; + yData[i] = this.ma.GetValue(i); + } + + double[] coefficients = Fit.Polynomial(xData, yData, this.polynomialDegree); + double[] predictedValues = new double[this.MaPeriod]; + for (int i = 0; i < this.MaPeriod; i++) + { + double predictedValue = 0; + for (int j = 0; j <= this.polynomialDegree; j++) + predictedValue += coefficients[j] * Math.Pow(i, j); + + predictedValues[i] = predictedValue; + } + + double stdDev = Statistics.StandardDeviation(predictedValues); + this.SetValue(predictedValues[0] + 2 * stdDev, 0); + this.SetValue(predictedValues[0] - 2 * stdDev, 1); + this.SetValue(predictedValues[0], 2); + } +} \ No newline at end of file diff --git a/Indicators/IndicatorPowerOfThree.cs b/Indicators/IndicatorPowerOfThree.cs index 5e39bbf..c90310f 100644 --- a/Indicators/IndicatorPowerOfThree.cs +++ b/Indicators/IndicatorPowerOfThree.cs @@ -7,6 +7,8 @@ using TradingPlatform.BusinessLayer; using TradingPlatform.BusinessLayer.Utils; using TradingPlatform.BusinessLayer.Chart; +using System.Threading.Tasks; +using System.Threading; namespace IndicatorPowerOfThree @@ -17,6 +19,7 @@ public class IndicatorPowerOfThree : Indicator private Period tfPeriod = Period.DAY1; private bool useTFPeriod = true; private int barsPeriod = 20; + private int barsCount = 1; private int offset = 0; private bool useCustomBarWidth = false; private int customBarWidth = 10; @@ -122,12 +125,22 @@ public LineOptions HighLevelLineOptions private Font labelFont; private HorizontalPosition horizontalPosition = HorizontalPosition.Right; private bool showLevelLine = false; - private bool showOpenLevelLine = false; - private bool showLowLevelLine = false; - private bool showHighLevelLine = false; private bool tfDataLoaded = false; + private bool needRedownload = false; private HistoricalData tfData; + private IndicatorState state; + private Task loadingTask; + private CancellationTokenSource cancellationSource; + private IndicatorState State + { + get => this.state; + set + { + this.state = value; + + } + } public IndicatorPowerOfThree() : base() { @@ -158,10 +171,12 @@ public IndicatorPowerOfThree() } protected override void OnInit() { - if (this.useTFPeriod && !tfDataLoaded) + base.OnInit(); + this.AbortPreviousTask(); + if (this.useTFPeriod && !this.tfDataLoaded) { - this.tfData = this.Symbol.GetHistory(this.tfPeriod, this.Symbol.HistoryType, 1); - this.tfDataLoaded = true; + var token = this.cancellationSource.Token; + this.loadingTask = Task.Factory.StartNew(() => HistoryDownload(this.cancellationSource.Token)); } this.growingBorderPen.Width = this.borderWidth; this.decreasingBorderPen.Width = this.borderWidth; @@ -174,13 +189,24 @@ public override void OnPaintChart(PaintChartEventArgs args) { base.OnPaintChart(args); - if (this.CurrentChart == null || this.HistoricalData == null || this.tfData == null) + if (this.CurrentChart == null || this.HistoricalData == null) return; var gr = args.Graphics; var currWindow = this.CurrentChart.Windows[args.WindowIndex]; RectangleF prevClipRectangle = gr.ClipBounds; gr.SetClip(args.Rectangle); + switch (this.State) + { + case IndicatorState.Loading: + gr.DrawString("Downloading historical data", new Font("Tahoma", 20), Brushes.Red, 20, 50); + return; + case IndicatorState.NoData: + gr.DrawString("No downloaded historical data", new Font("Tahoma", 20), Brushes.Red, 20, 50); + return; + default: + break; + } try { // bar width correction logic @@ -204,72 +230,45 @@ public override void OnPaintChart(PaintChartEventArgs args) SolidBrush currBodyBrush = new SolidBrush(Color.White); Pen currBodyPen = new Pen(currBodyBrush); Pen currWickPen = new Pen(currBodyBrush); - candleBody.X = (float)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[0].TimeLeft) + this.offset + barLeftOffset + barWidth; + candleBody.X = (float)currWindow.CoordinatesConverter.GetChartX(this.HistoricalData[0].TimeLeft) + this.offset + barLeftOffset + 1 + barWidth; candleBody.Width = this.useCustomBarWidth ? this.customBarWidth : barWidth; shadowTop.X = candleBody.X + candleBody.Width / 2; shadowBottom.X = shadowTop.X; HistoryItemBar resultBar = new HistoryItemBar(); - if (this.useTFPeriod) - { - if (this.tfData == null || this.tfData.Count == 0) - return; - if (this.tfPeriod != Period.TICK1) + for (int i = this.barsCount-1; i >= 0; i--) + { + resultBar = this.GetResultBar(i); + if (resultBar == null) + continue; + shadowTop.Y = (float)currWindow.CoordinatesConverter.GetChartY(resultBar.High); + shadowBottom.Y = (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Low); + if (resultBar.Close == resultBar.Open) { - HistoryItemBar lastTFBar = (HistoryItemBar)this.tfData[0]; - resultBar.High = lastTFBar.High; - resultBar.Low = lastTFBar.Low; - resultBar.Close = lastTFBar.Close; - resultBar.Open = lastTFBar.Open; + candleBody.Height = 1; + currBodyBrush = this.dojiCandleBrush; + currBodyPen = this.dojiBorderPen; + currWickPen = this.dojiWickPen; + candleBody.Y = (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Close); } else { - HistoryItemLast lastTick = (HistoryItemLast)this.tfData[0]; - resultBar.High = lastTick.Price; - resultBar.Low = lastTick.Price; - resultBar.Close = lastTick.Price; - resultBar.Open = lastTick.Price; + candleBody.Y = resultBar.Close > resultBar.Open ? (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Close) : (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Open); + currBodyBrush = resultBar.Close > resultBar.Open ? this.growingCandleBrush : this.decreasingCandleBrush; + currBodyPen = resultBar.Close > resultBar.Open ? this.growingBorderPen : this.decreasingBorderPen; + currWickPen = resultBar.Close > resultBar.Open ? this.growingWickPen : this.decreasingWickPen; + candleBody.Height = Math.Abs((float)currWindow.CoordinatesConverter.GetChartY(resultBar.Close) - (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Open)); } + gr.FillRectangle(currBodyBrush, candleBody); + if (this.drawBorder) + gr.DrawRectangle(currBodyPen, candleBody); + else + currBodyPen = new Pen(currBodyBrush.Color, this.borderWidth); + gr.DrawLine(currWickPen, shadowTop, new PointF(shadowTop.X, candleBody.Y)); + gr.DrawLine(currWickPen, shadowBottom, new PointF(shadowBottom.X, candleBody.Y + candleBody.Height)); + candleBody.X += barLeftOffset*2 + barWidth; + shadowTop.X += barLeftOffset*2 + barWidth; + shadowBottom.X += barLeftOffset*2 + barWidth; } - else - { - if (this.Count < this.barsPeriod) - return; - resultBar.Close = this.Close(); - resultBar.Low = this.Low(); - resultBar.Open = this.Open(this.barsPeriod-1); - for (int i = 0; i= resultBar.High) - resultBar.High = this.High(i); - if (this.Low(i) <= resultBar.Low) - resultBar.Low = this.Low(i); - } - } - shadowTop.Y = (float)currWindow.CoordinatesConverter.GetChartY(resultBar.High); - shadowBottom.Y = (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Low); - if (resultBar.Close == resultBar.Open) - { - candleBody.Height = 1; - currBodyBrush = this.dojiCandleBrush; - currBodyPen = this.dojiBorderPen; - currWickPen = this.dojiWickPen; - candleBody.Y = (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Close); - } - else - { - candleBody.Y = resultBar.Close > resultBar.Open ? (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Close) : (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Open); - currBodyBrush = resultBar.Close > resultBar.Open ? this.growingCandleBrush : this.decreasingCandleBrush; - currBodyPen = resultBar.Close > resultBar.Open ? this.growingBorderPen : this.decreasingBorderPen; - currWickPen = resultBar.Close > resultBar.Open ? this.growingWickPen : this.decreasingWickPen; - candleBody.Height = Math.Abs((float)currWindow.CoordinatesConverter.GetChartY(resultBar.Close) - (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Open)); - } - gr.FillRectangle(currBodyBrush, candleBody); - if (this.drawBorder) - gr.DrawRectangle(currBodyPen, candleBody); - else - currBodyPen = new Pen(currBodyBrush.Color, this.borderWidth); - gr.DrawLine(currWickPen, shadowTop, new PointF(shadowTop.X, candleBody.Y)); - gr.DrawLine(currWickPen, shadowBottom, new PointF(shadowBottom.X, candleBody.Y + candleBody.Height)); if (this.showLabel) { @@ -289,20 +288,20 @@ public override void OnPaintChart(PaintChartEventArgs args) if (this.OpenLevelLineOptions.Enabled) { - PointF endOpenLine = new PointF(shadowTop.X, (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Open)); - PointF startOpenLine = new PointF((float)currWindow.CoordinatesConverter.GetChartX(this.tfData[0].TimeLeft) + barWidth/2, endOpenLine.Y); + PointF endOpenLine = new PointF(shadowTop.X - barWidth/2, (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Open)); + PointF startOpenLine = this.GetStartLineCoordinates(resultBar.Open, PriceType.Open, currWindow.CoordinatesConverter); gr.DrawLine(this.openLevelLinePen, startOpenLine, endOpenLine); } if (this.HighLevelLineOptions.Enabled) { - PointF endHighLine = new PointF(shadowTop.X, (float)currWindow.CoordinatesConverter.GetChartY(resultBar.High)); + PointF endHighLine = new PointF(shadowTop.X - barWidth / 2, (float)currWindow.CoordinatesConverter.GetChartY(resultBar.High)); PointF startHighLine = this.GetStartLineCoordinates(resultBar.High, PriceType.High, currWindow.CoordinatesConverter); gr.DrawLine(this.highLevelLinePen, startHighLine, endHighLine); } if (this.LowLevelLineOptions.Enabled) { - PointF endLowLine = new PointF(shadowTop.X, (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Low)); + PointF endLowLine = new PointF(shadowTop.X - barWidth / 2, (float)currWindow.CoordinatesConverter.GetChartY(resultBar.Low)); PointF startLowLine = this.GetStartLineCoordinates(resultBar.Low, PriceType.Low, currWindow.CoordinatesConverter); gr.DrawLine(this.lowLevelLinePen, startLowLine, endLowLine); } @@ -313,6 +312,55 @@ public override void OnPaintChart(PaintChartEventArgs args) gr.SetClip(prevClipRectangle); } } + private HistoryItemBar GetResultBar(int currBarIndex) + { + HistoryItemBar resultBar = new HistoryItemBar(); + + if (this.useTFPeriod) + { + if (this.tfData == null || this.tfData.Count == 0) + return null; + if (this.tfPeriod != Period.TICK1) + { + HistoryItemBar lastTFBar = (HistoryItemBar)this.tfData[currBarIndex, SeekOriginHistory.End]; + resultBar.High = lastTFBar.High; + resultBar.Low = lastTFBar.Low; + resultBar.Close = lastTFBar.Close; + resultBar.Open = lastTFBar.Open; + } + else + { + HistoryItemLast lastTick = (HistoryItemLast)this.tfData[currBarIndex, SeekOriginHistory.Begin]; + resultBar.High = lastTick.Price; + resultBar.Low = lastTick.Price; + resultBar.Close = lastTick.Price; + resultBar.Open = lastTick.Price; + } + } + else + { + int startIndex = (this.barsCount - currBarIndex - 1) * this.barsPeriod; + int finalIndex = startIndex + this.barsPeriod - 1; + if (this.Count <= startIndex) + return null; + if (finalIndex >= this.Count) + finalIndex = this.Count - 1; + resultBar.High = this.High(finalIndex); + resultBar.Close = this.Close(startIndex); + resultBar.Low = this.Low(finalIndex); + resultBar.Open = this.Open(finalIndex); + for (int i = 0; i < this.barsPeriod - 1; i++) + { + if (startIndex + i >= this.Count) + break; + if (this.High(startIndex+i) >= resultBar.High) + resultBar.High = this.High(startIndex+i); + if (this.Low(startIndex+i) <= resultBar.Low) + resultBar.Low = this.Low(startIndex+i); + } + } + return resultBar; + } private PointF GetStartLineCoordinates(double price, PriceType priceType, IChartWindowCoordinatesConverter converter) { int leftIndex = 0; @@ -329,7 +377,7 @@ private PointF GetStartLineCoordinates(double price, PriceType priceType, IChart double currPrice = priceType == PriceType.Open ? currBar.Open : priceType == PriceType.High ? currBar.High : currBar.Low; - if ((currPrice <= price && priceType == PriceType.Low) || (currPrice >= price && priceType == PriceType.High)) + if ((currPrice <= price && priceType == PriceType.Low) || (currPrice >= price && priceType == PriceType.High) || (currPrice == price && priceType == PriceType.Open)) X = (float)converter.GetChartX(this.HistoricalData[i, SeekOriginHistory.Begin].TimeLeft) + this.CurrentChart.BarsWidth/2; } return new PointF(X, (float)converter.GetChartY(price)); @@ -378,6 +426,13 @@ public override IList Settings Relation = customWidth, SeparatorGroup = barDrawingGroup, }); + settings.Add(new SettingItemInteger("barsCount", this.barsCount) + { + Text = "Bars Count", + SortIndex = 1, + Minimum = 1, + SeparatorGroup = calculatingGroup, + }); settings.Add(new SettingItemBoolean("useTFPeriod", this.useTFPeriod) { Text = "Use Specified Time Frame", @@ -538,13 +593,28 @@ public override IList Settings Relation = visibleRelationLevelLine, SeparatorGroup = levelLinesDrawingGroup, }); + settings.Add(new SettingItemLineOptions("highLevelLineOptions", this._highLevelLineOptions) + { + Text = "High level line", + SortIndex = 7, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points }, + UseEnabilityToggler = true, + Relation = visibleRelationLevelLine, + SeparatorGroup = levelLinesDrawingGroup, + }); return settings; } set { + this.needRedownload = false; base.Settings = value; if (value.TryGetValue("barsPeriod", out int barsPeriod)) this.barsPeriod = barsPeriod; + if (value.TryGetValue("barsCount", out int barsCount)) + { + this.barsCount = barsCount; + this.needRedownload = true; + } if (value.TryGetValue("offset", out int offset)) this.offset = offset; if (value.TryGetValue("useCustomBarWidth", out bool useCustomBarWidth)) @@ -554,7 +624,7 @@ public override IList Settings if (value.TryGetValue("tfPeriod", out Period tfPeriod)) { this.tfPeriod = tfPeriod; - this.tfDataLoaded = false; + this.needRedownload = true; } if (value.TryGetValue("useTFPeriod", out bool useTFPeriod)) this.useTFPeriod = useTFPeriod; @@ -596,7 +666,81 @@ public override IList Settings this.HighLevelLineOptions = highLevelLineOptions; if (value.TryGetValue("labelColor", out Color labelColor)) this.labelColor = labelColor; - this.OnSettingsUpdated(); + if (this.needRedownload && this.useTFPeriod) + { + this.AbortPreviousTask(); + this.loadingTask = Task.Factory.StartNew(() => HistoryDownload(this.cancellationSource.Token)); + } + } + } + protected override void OnClear() + { + base.OnClear(); + this.AbortPreviousTask(); + } + + private void AbortPreviousTask() + { + if (this.tfData != null) + this.tfData.Dispose(); + + if (this.cancellationSource != null) + this.cancellationSource.Cancel(); + + this.cancellationSource = new CancellationTokenSource(); + } + + private bool IsValidLoadedHistory(HistoricalData history) + { + if (history == null || history.Count == 0) + return false; + return true; + } + private void HistoryDownload(CancellationToken token) + { + this.State = IndicatorState.Loading; + var fromTime = DateTime.UtcNow; + + var prevHistoryCount = -1; + + var needReload = true; + while (needReload) + { + needReload = false; + fromTime -= this.tfPeriod.Duration * this.barsCount; + if (token.IsCancellationRequested) + return; + if (this.Symbol == null) + continue; + HistoryAggregation aggregation; + if (this.tfPeriod.BasePeriod == BasePeriod.Tick) + { + if (this.tfPeriod.PeriodMultiplier == 1) + aggregation = new HistoryAggregationTick(this.Symbol.HistoryType); + else + aggregation = new HistoryAggregationTickBars(this.tfPeriod.PeriodMultiplier, this.Symbol.HistoryType); + } + else + aggregation = new HistoryAggregationTime(this.tfPeriod, this.Symbol.HistoryType); + this.tfData = this.Symbol.GetHistory(new HistoryRequestParameters() + { + Symbol = this.Symbol, + FromTime = fromTime, + CancellationToken = this.cancellationSource.Token, + Aggregation = aggregation + }); + if (token.IsCancellationRequested || prevHistoryCount == this.tfData.Count) + { + this.State = IndicatorState.NoData; + } + else if (this.IsValidLoadedHistory(this.tfData)) + this.State = IndicatorState.Ready; + else + { + prevHistoryCount = this.tfData.Count; + needReload = true; + this.tfData.Dispose(); + } } } internal enum HorizontalPosition @@ -604,5 +748,14 @@ internal enum HorizontalPosition Right, Left } + + public enum IndicatorState + { + Ready, + Loading, + NoData, + IncorrectPeriod, + OneTickNotAllowed + } } -} +} \ No newline at end of file diff --git a/Indicators/IndicatorQstick.cs b/Indicators/IndicatorQstick.cs index 351d516..e97f49e 100644 --- a/Indicators/IndicatorQstick.cs +++ b/Indicators/IndicatorQstick.cs @@ -22,7 +22,17 @@ public sealed class IndicatorQstick : Indicator, IWatchlistIndicator "Linear Weighted", MaMode.LWMA} )] public MaMode MAType = MaMode.SMA; + [InputParameter("Smoothing period", 0, 1, 999, 0, 0)] + public int SmoothingPeriod = 20; + // Displays Input Parameter as dropdown list. + [InputParameter("Smoothing Type", 1, variants: new object[] { + "Simple", MaMode.SMA, + "Exponential", MaMode.EMA, + "Modified", MaMode.SMMA, + "Linear Weighted", MaMode.LWMA} + )] + public MaMode SmoothingType = MaMode.SMA; // [InputParameter("Calculation type", 5, variants: new object[] { @@ -37,6 +47,8 @@ public sealed class IndicatorQstick : Indicator, IWatchlistIndicator private HistoricalDataCustom customHistData; private Indicator ma; + private Indicator smoothing; + /// /// Indicator's constructor. Contains general information: name, description, LineSeries etc. @@ -51,6 +63,7 @@ public IndicatorQstick() // Defines line on demand with particular parameters. this.AddLineSeries("Qstick'Line", Color.Blue, 1, LineStyle.Solid); this.AddLineLevel(0, "0'Line", Color.Gray, 1, LineStyle.Solid); + this.AddLineSeries("Smoothing line", Color.IndianRed, 1, LineStyle.Solid); this.SeparateWindow = true; } @@ -68,6 +81,9 @@ protected override void OnInit() // Adds the smoothing indicator to the custom historical data. this.customHistData.AddIndicator(this.ma); + + this.smoothing = Core.Indicators.BuiltIn.MA(this.SmoothingPeriod, PriceType.Open, this.SmoothingType, this.CalculationType); + this.customHistData.AddIndicator(this.smoothing); } /// @@ -85,8 +101,10 @@ protected override void OnUpdate(UpdateArgs args) // Skip if count is smaller than period value. if (this.Count < this.MinHistoryDepths) return; - + var qStickValue = this.ma.GetValue(); + this.customHistData[PriceType.Open] = this.Close() - this.Open(); // Sets value (smoothing value on the custom historical data) for displaying on the chart. - this.SetValue(this.ma.GetValue()); + this.SetValue(qStickValue); + this.SetValue(this.smoothing.GetValue(), 1); } } \ No newline at end of file diff --git a/Indicators/IndicatorRLarryWilliams.cs b/Indicators/IndicatorRLarryWilliams.cs index 6808e57..c698d73 100644 --- a/Indicators/IndicatorRLarryWilliams.cs +++ b/Indicators/IndicatorRLarryWilliams.cs @@ -11,7 +11,7 @@ namespace Oscillators; public sealed class IndicatorRLarryWilliams : Indicator, IWatchlistIndicator { // Displays Input Parameter as input field (or checkbox if value type is bolean). - [InputParameter("Period", 0, 1, 999, 1, 0)] + [InputParameter("Period", 0, 1, 9999, 1, 0)] public int Period = 14; public int MinHistoryDepths => this.Period; diff --git a/Indicators/IndicatorSchaffTrendCycle.cs b/Indicators/IndicatorSchaffTrendCycle.cs new file mode 100644 index 0000000..5c84a2d --- /dev/null +++ b/Indicators/IndicatorSchaffTrendCycle.cs @@ -0,0 +1,121 @@ +// Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. + +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace TrendIndicators; + +public class IndicatorSchaffTrendCycle : Indicator +{ + [InputParameter("Fast Period", 10, 1, 9999, 1, 0)] + public int fastPeriod; + + [InputParameter("Slow Period", 10, 1, 9999, 1, 0)] + public int slowPeriod; + + [InputParameter("k Period", 10, 1, 9999, 1, 0)] + public int kPeriod; + + [InputParameter("d Period", 10, 1, 9999, 1, 0)] + public int dPeriod; + + [InputParameter("High Line", 10, 0, 100)] + public int HighLine; + + [InputParameter("Low Line", 10, 0, 100)] + public int LowLine; + + [InputParameter("Sources prices for MA", 20, variants: new object[] + { + "Close", PriceType.Close, + "Open", PriceType.Open, + "High", PriceType.High, + "Low", PriceType.Low, + "Typical", PriceType.Typical, + "Medium", PriceType.Median, + "Weighted", PriceType.Weighted, + })] + public PriceType SourcePrice; + + [InputParameter("Smoothing type", 3, variants: new object[]{ + "Simple Moving Average", MaMode.SMA, + "Exponential Moving Average", MaMode.EMA, + "Smoothed Moving Average", MaMode.SMMA, + "Linearly Weighted Moving Average", MaMode.LWMA, + })] + public MaMode MaType; + + private Indicator fastMA; + private Indicator slowMA; + private Indicator stochasticOscillator; + private Indicator stochasticOscillatorPF; + private Indicator stochasticOscillatorPFF; + + private HistoricalDataCustom macd; + private HistoricalDataCustom pf; + private HistoricalDataCustom pff; + + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorSchaffTrendCycle.cs"; + public IndicatorSchaffTrendCycle() + : base() + { + this.Name = "Schaff Trend Cycle"; + this.SeparateWindow = true; + + this.MaType = MaMode.SMA; + this.SourcePrice = PriceType.Close; + + this.fastPeriod = 23; + this.slowPeriod = 50; + this.kPeriod = 10; + this.dPeriod = 10; + this.HighLine = 80; + this.LowLine = 20; + + this.AddLineSeries("Main Line", Color.CadetBlue, 1, LineStyle.Solid); + this.AddLineSeries("High Line", Color.Green, 1, LineStyle.Solid); + this.AddLineSeries("Low Line", Color.Red, 1, LineStyle.Solid); + } + + + protected override void OnInit() + { + this.fastMA = Core.Instance.Indicators.BuiltIn.EMA(this.fastPeriod, this.SourcePrice); + this.slowMA = Core.Instance.Indicators.BuiltIn.EMA(this.slowPeriod, this.SourcePrice); + + this.stochasticOscillator = Core.Instance.Indicators.BuiltIn.Stochastic(this.kPeriod, this.dPeriod, this.dPeriod * 2, this.MaType); + this.stochasticOscillatorPF = Core.Instance.Indicators.BuiltIn.Stochastic(this.kPeriod, this.dPeriod, this.dPeriod * 2, this.MaType); + this.stochasticOscillatorPFF = Core.Instance.Indicators.BuiltIn.Stochastic(this.kPeriod, this.dPeriod, this.dPeriod * 2, this.MaType); + + this.macd = new HistoricalDataCustom(this); + this.pf = new HistoricalDataCustom(this); + this.pff = new HistoricalDataCustom(this); + + this.AddIndicator(this.fastMA); + this.AddIndicator(this.slowMA); + + this.macd.AddIndicator(this.stochasticOscillator); + this.pf.AddIndicator(this.stochasticOscillatorPF); + this.pff.AddIndicator(this.stochasticOscillatorPFF); + } + + protected override void OnUpdate(UpdateArgs args) + { + if (this.Count < this.slowPeriod) + return; + + double currentMACD = this.fastMA.GetValue() - this.slowMA.GetValue(); + this.macd.SetValue(currentMACD, currentMACD, currentMACD, currentMACD); + + double d = this.stochasticOscillator.GetValue(0, 1); + this.pf.SetValue(d, d, d, d); + + double pf = this.stochasticOscillatorPF.GetValue(); + this.pff.SetValue(pf, pf, pf, pf); + + double pff = this.stochasticOscillatorPFF.GetValue(0, 1); + this.SetValue(pff); + this.SetValue(this.HighLine, 1); + this.SetValue(this.LowLine, 2); + } +} \ No newline at end of file diff --git a/Indicators/IndicatorSuperTrend.cs b/Indicators/IndicatorSuperTrend.cs index f4e0e91..3344b31 100644 --- a/Indicators/IndicatorSuperTrend.cs +++ b/Indicators/IndicatorSuperTrend.cs @@ -54,9 +54,11 @@ protected override void OnUpdate(UpdateArgs args) if (this.Count < this.AtrPeriod) return; - var isNewBar = this.HistoricalData.Aggregation.GetPeriod == Period.TICK1 - ? args.Reason == UpdateReason.NewTick || args.Reason == UpdateReason.HistoricalBar - : args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; + var isNewBar = false; + if (this.HistoricalData.Aggregation is HistoryAggregationTick) + isNewBar = args.Reason == UpdateReason.NewTick || args.Reason == UpdateReason.HistoricalBar; + else + isNewBar = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; if (isNewBar) { diff --git a/Indicators/IndicatorTTMSqueeze.cs b/Indicators/IndicatorTTMSqueeze.cs new file mode 100644 index 0000000..ec3d57f --- /dev/null +++ b/Indicators/IndicatorTTMSqueeze.cs @@ -0,0 +1,393 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace IndicatorTTMSqueeze +{ + public class IndicatorTTMSqueeze : Indicator + { + private int Period = 20; + private double BBCoeff = 2.0; + private double KCCoeffHigh = 1.0; + private double KCCoeffMid = 1.5; + private double KCCoeffLow = 2.0; + private PriceType calculationPriceType = PriceType.Typical; + + private Color noSqzColor = Color.Gray; + private Color lowSqzColor = Color.Lime; + private Color midSqzColor = Color.Yellow; + private Color highSqzColor = Color.Tomato; + + private Color posUp = Color.Blue; + private Color posDown = Color.LightBlue; + private Color negUp = Color.IndianRed; + private Color negDown = Color.DarkRed; + + private int markerSize = 10; + + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorTTMSqueeze.cs"; + public IndicatorTTMSqueeze() + { + this.Name = "TTM Squeeze"; + this.SeparateWindow = true; + this.AddLineSeries("Impulse", Color.CadetBlue, 1, LineStyle.Histogramm); + } + + protected override void OnUpdate(UpdateArgs args) + { + if (this.Count < Period) + return; + double mean = GetMean(0, Period, calculationPriceType); + double std = GetStdDev(0, Period, calculationPriceType); + double bbUpper = mean + BBCoeff * std; + double bbLower = mean - BBCoeff * std; + + double atr = GetMeanTR(0, Period); + double kcHigh1 = mean + atr * KCCoeffHigh; + double kcLow1 = mean - atr * KCCoeffHigh; + double kcHigh2 = mean + atr * KCCoeffMid; + double kcLow2 = mean - atr * KCCoeffMid; + double kcHigh3 = mean + atr * KCCoeffLow; + double kcLow3 = mean - atr * KCCoeffLow; + + double regressionValue = GetLinearRegression(0, Period); + this.SetValue(regressionValue); + double curr = this.GetValue(0); + double prev = this.GetValue(1); + + Color color = noSqzColor; + if (curr > 0) + color = curr > prev ? posUp : posDown; + else + color = curr > prev ? negUp : negDown; + + this.LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color)); + } + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + if (this.CurrentChart == null) + return; + + var gr = args.Graphics; + var wnd = this.CurrentChart.Windows[args.WindowIndex]; + var converter = wnd.CoordinatesConverter; + RectangleF prevClip = gr.ClipBounds; + gr.SetClip(args.Rectangle); + + try + { + int left = (int)converter.GetBarIndex(converter.GetTime(0)); + int right = (int)converter.GetBarIndex(converter.GetTime(args.Rectangle.Width)); + left = Math.Max(0, left); + right = Math.Min(this.HistoricalData.Count - 1, right); + + float y = (float)converter.GetChartY(0) - markerSize / 2; + int barWidth = this.CurrentChart.BarsWidth; + RectangleF marker = new RectangleF(0, 0, markerSize, markerSize); + + for (int i = left; i <= right; i++) + { + + double mean = GetMean(this.HistoricalData.Count - i - 1, Period, calculationPriceType); + double std = GetStdDev(this.HistoricalData.Count - i - 1, Period, calculationPriceType); + double bbUpper = mean + BBCoeff * std; + double bbLower = mean - BBCoeff * std; + + double atr = GetMeanTR(this.HistoricalData.Count - i - 1, Period); + double kcHigh1 = mean + atr * KCCoeffHigh; + double kcLow1 = mean - atr * KCCoeffHigh; + double kcHigh2 = mean + atr * KCCoeffMid; + double kcLow2 = mean - atr * KCCoeffMid; + double kcHigh3 = mean + atr * KCCoeffLow; + double kcLow3 = mean - atr * KCCoeffLow; + + Color sqzColor = noSqzColor; + if (bbLower >= kcLow1 && bbUpper <= kcHigh1) + sqzColor = highSqzColor; + else if (bbLower >= kcLow2 && bbUpper <= kcHigh2) + sqzColor = midSqzColor; + else if (bbLower >= kcLow3 && bbUpper <= kcHigh3) + sqzColor = lowSqzColor; + + marker.X = (float)converter.GetChartX(this.HistoricalData[i, SeekOriginHistory.Begin].TimeLeft) + barWidth / 2 - markerSize / 2; + marker.Y = y; + gr.FillEllipse(new SolidBrush(sqzColor), marker); + } + } + finally + { + gr.SetClip(prevClip); + } + } + public override IList Settings + { + get + { + var settings = base.Settings; + settings.Add(new SettingItemInteger("Period", this.Period) + { + Text = "Squeeze Period", + SortIndex = 0, + }); + settings.Add(new SettingItemDouble("BBCoeff", this.BBCoeff) + { + Text = "BB Coefficient", + DecimalPlaces = 2, + Increment = 0.01, + SortIndex = 0, + }); + settings.Add(new SettingItemDouble("KCCoeffHigh", this.KCCoeffHigh) + { + Text = "KC Coefficient High", + SortIndex = 0, + DecimalPlaces = 2, + Increment = 0.01, + }); + settings.Add(new SettingItemDouble("KCCoeffMid", this.KCCoeffMid) + { + Text = "KC Coefficient Mid", + SortIndex = 0, + DecimalPlaces = 2, + Increment = 0.01, + }); + settings.Add(new SettingItemDouble("KCCoeffLow", this.KCCoeffLow) + { + Text = "KC Coefficient Low", + SortIndex = 0, + DecimalPlaces = 2, + Increment = 0.01, + }); + settings.Add(new SettingItemSelectorLocalized("PriceType", this.calculationPriceType, new List + { + new SelectItem("Close", PriceType.Close), + new SelectItem("Open", PriceType.Open), + new SelectItem("High", PriceType.High), + new SelectItem("Low", PriceType.Low), + new SelectItem("Typical", PriceType.Typical), + new SelectItem("Weighted", PriceType.Weighted), + new SelectItem("Median", PriceType.Median), + }) + { + Text = "Price Type", + SortIndex = 0, + }); + settings.Add(new SettingItemColor("posUp", this.posUp) + { + Text = "Negative Ascending", + SortIndex = 1, + }); + settings.Add(new SettingItemColor("posDown", this.posDown) + { + Text = "Positive Falling", + SortIndex = 1, + }); + settings.Add(new SettingItemColor("negUp", this.negUp) + { + Text = "Negative Ascending", + SortIndex = 1, + }); + settings.Add(new SettingItemColor("negDown", this.negDown) + { + Text = "Negative Falling", + SortIndex = 1, + }); + settings.Add(new SettingItemColor("noSqzColor", this.noSqzColor) + { + Text = "No Squeeze Color", + SortIndex = 1, + }); + settings.Add(new SettingItemColor("lowSqzColor", this.lowSqzColor) + { + Text = "Low Squeeze Color", + SortIndex = 1, + }); + settings.Add(new SettingItemColor("midSqzColor", this.midSqzColor) + { + Text = "Mid Squeeze Color", + SortIndex = 1, + }); + settings.Add(new SettingItemColor("highSqzColor", this.highSqzColor) + { + Text = "High Squeeze Color", + SortIndex = 1, + }); + settings.Add(new SettingItemInteger("markerSize", this.markerSize) + { + Text = "Marker Size", + SortIndex = 1, + }); + return settings; + } + set + { + base.Settings = value; + bool needRecalculation = false; + if (value.TryGetValue("Period", out int Period)) + { + this.Period = Period; + needRecalculation = true; + } + if (value.TryGetValue("BBCoeff", out double BBCoeff)) + { + this.BBCoeff = BBCoeff; + needRecalculation = true; + } + + if (value.TryGetValue("KCCoeffHigh", out double KCCoeffHigh)) + { + this.KCCoeffHigh = KCCoeffHigh; + needRecalculation = true; + } + + if (value.TryGetValue("KCCoeffMid", out double KCCoeffMid)) + { + this.KCCoeffMid = KCCoeffMid; + needRecalculation = true; + } + if (value.TryGetValue("KCCoeffLow", out double KCCoeffLow)) + { + this.KCCoeffLow = KCCoeffLow; + needRecalculation = true; + } + if (value.TryGetValue("PriceType", out PriceType calculationPriceType)) + { + this.calculationPriceType = calculationPriceType; + needRecalculation = true; + } + if (value.TryGetValue("noSqzColor", out Color noSqzColor)) + this.noSqzColor = noSqzColor; + if (value.TryGetValue("lowSqzColor", out Color lowSqzColor)) + this.lowSqzColor = lowSqzColor; + if (value.TryGetValue("midSqzColor", out Color midSqzColor)) + this.midSqzColor = midSqzColor; + if (value.TryGetValue("highSqzColor", out Color highSqzColor)) + this.highSqzColor = highSqzColor; + if (value.TryGetValue("posDown", out Color posDown)) + { + this.posDown = posDown; + needRecalculation = true; + } + if (value.TryGetValue("posUp", out Color posUp)) + { + this.posUp = posUp; + needRecalculation = true; + } + if (value.TryGetValue("negDown", out Color negDown)) + { + this.negDown = negDown; + needRecalculation = true; + } + if (value.TryGetValue("negUp", out Color negUp)) + { + this.negUp = negUp; + needRecalculation = true; + } + if (value.TryGetValue("markerSize", out int markerSize)) + this.markerSize = markerSize; + if (needRecalculation) + this.OnSettingsUpdated(); + } + } + private double GetMean(int bar, int length, PriceType type) + { + double sum = 0; + int count = 0; + for (int i = 0; i < length; i++) + { + if (bar+i >= this.Count) + break; + double price = this.GetPrice(type, bar + i); + if (!double.IsNaN(price)) + { + sum += price; + count++; + } + } + return count > 0 ? sum / count : double.NaN; + } + + private double GetStdDev(int bar, int length, PriceType type) + { + double mean = GetMean(bar, length, type); + double sumSq = 0; + int count = 0; + for (int i = 0; i < length; i++) + { + if (bar + i >= this.Count) + break; + double price = this.GetPrice(type, bar + i); + if (!double.IsNaN(price)) + { + sumSq += Math.Pow(price - mean, 2); + count++; + } + } + return count > 0 ? Math.Sqrt(sumSq / count) : double.NaN; + } + + private double GetMeanTR(int bar, int length) + { + double sum = 0; + int count = 0; + for (int i = 0; i < length; i++) + { + double tr = GetTR(bar + i); + if (!double.IsNaN(tr)) + { + sum += tr; + count++; + } + } + return count > 0 ? sum / count : double.NaN; + } + + private double GetTR(int bar) + { + if (bar <= 0 || bar >= this.Count-1) + return double.NaN; + + double high = this.GetPrice(PriceType.High, bar); + double low = this.GetPrice(PriceType.Low, bar); + double closePrev = this.GetPrice(PriceType.Close, bar + 1); + + if (double.IsNaN(closePrev)) + return high - low; + + return Math.Max(high - low, Math.Max(Math.Abs(high - closePrev), Math.Abs(low - closePrev))); + } + + private double GetLinearRegression(int bar, int length) + { + double[] diff = new double[length]; + for (int i = 0; i < length; i++) + { + int index = bar + i; + double high = this.GetPrice(PriceType.High, index); + double low = this.GetPrice(PriceType.Low, index); + double close = this.GetPrice(PriceType.Close, index); + double hl2 = (high + low) / 2.0; + double avg = GetMean(index, length, PriceType.Close); + double baseLine = (hl2 + avg) / 2.0; + diff[i] = close - baseLine; + } + + double sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; + for (int i = 0; i < length; i++) + { + sumX += i; + sumY += diff[i]; + sumXY += i * diff[i]; + sumXX += i * i; + } + + double n = length; + double slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX); + double intercept = (sumY - slope * sumX) / n; + + return intercept + slope * (n - 1); + } + } +} \ No newline at end of file diff --git a/Indicators/IndicatorTimeSessions.cs b/Indicators/IndicatorTimeSessions.cs index 6ea2e20..6475b2a 100644 --- a/Indicators/IndicatorTimeSessions.cs +++ b/Indicators/IndicatorTimeSessions.cs @@ -3,200 +3,625 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Drawing.Drawing2D; using TradingPlatform.BusinessLayer; +using TradingPlatform.BusinessLayer.Utils; -namespace VolumeIndicators; - -public sealed class IndicatorTimeSessions : Indicator +namespace VolumeIndicators { - public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorTimeSessions.cs"; + public sealed class IndicatorTimeSessions : Indicator + { + public override string SourceCodeLink => "https://github.com/Quantower/Scripts/blob/main/Indicators/IndicatorTimeSessions.cs"; - private Period currentPeriod; + private Period currentPeriod; - private readonly Session[] sessions; + private readonly Session[] sessions; - public IndicatorTimeSessions() - : base() - { - // Defines indicator's name and description. - this.Name = "TimeSessions"; + public IndicatorTimeSessions() + : base() + { + this.Name = "TimeSessions"; + this.SeparateWindow = false; + this.OnBackGround = true; - // By default indicator will be applied on main window of the chart - this.SeparateWindow = false; - this.OnBackGround = true; + this.sessions = new Session[] + { + new Session(Color.Green,"First Session",1), + new Session(Color.Red,"Second Session",2), + new Session(Color.GreenYellow, "Third Session",3), + new Session(Color.Blue,"Fourth Session", 4), + new Session(Color.Cyan,"Fifth Session",5) + }; + } - this.sessions = new Session[] + protected override void OnInit() { - new Session(Color.Green, "First Session", 1), - new Session(Color.Red, "Second Session", 2), - new Session(Color.GreenYellow, "Third Session", 3), - new Session(Color.Blue, "Fourth Session", 4), - new Session(Color.Cyan, "Fifth Session", 5) - }; - } + this.currentPeriod = this.HistoricalData.Aggregation.GetPeriod; + base.OnInit(); + if(this.HistoricalData.Aggregation is HistoryAggregationTime haTime) + this.currentPeriod = haTime.Period; + else + this.currentPeriod = Period.TICK1; - protected override void OnInit() => this.currentPeriod = this.HistoricalData.Aggregation.GetPeriod; + } - protected override void OnUpdate(UpdateArgs args) { } + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + if (this.CurrentChart == null) + return; - public override void OnPaintChart(PaintChartEventArgs args) - { - base.OnPaintChart(args); + if (this.currentPeriod.Duration.Days >= 1) + return; - if (this.CurrentChart == null) - return; + var graphics = args.Graphics; + var mainWindow = this.CurrentChart.MainWindow; + RectangleF prevClipRectangle = graphics.ClipBounds; + graphics.SetClip(args.Rectangle); + try + { + var leftBorderTime = this.Time(this.Count - 1); + var rightBorderTime = this.Time(0); - if (this.currentPeriod.Duration.Days >= 1) - return; + var bordersSpan = rightBorderTime - leftBorderTime; + int daysSpan = (int)bordersSpan.TotalDays; - var graphics = args.Graphics; + int leftCoordinate; + int rightCoordinate; - var mainWindow = this.CurrentChart.MainWindow; + DateTime startTime; + DateTime endTime; - var leftBorderTime = this.Time(this.Count - 1); - var rightBorderTime = this.Time(0); - var bordersSpan = rightBorderTime - leftBorderTime; - int daysSpan = (int)bordersSpan.TotalDays; - int leftCoordinate; - int rightCoordinate; + int panelTop = mainWindow.ClientRectangle.Top; + int panelBottom = mainWindow.ClientRectangle.Bottom; + int panelHeight = mainWindow.ClientRectangle.Height; - DateTime startTime; - DateTime endTime; - for (int i = 0; i < this.sessions.Length; i++) - { - if (!this.sessions[i].SessionVisibility) - continue; + DateTime screenLeftTime = mainWindow.CoordinatesConverter.GetTime((int)mainWindow.ClientRectangle.Left); + DateTime screenRightTime = mainWindow.CoordinatesConverter.GetTime((int)mainWindow.ClientRectangle.Right); + if (screenLeftTime < leftBorderTime) + screenLeftTime = leftBorderTime; + if (screenRightTime > rightBorderTime) + screenRightTime = rightBorderTime; - var leftTime = this.sessions[i].SessionFirstTime; - var rightTime = this.sessions[i].SessionSecondTime; + for (int i = 0; i < this.sessions.Length; i++) + { + var s = this.sessions[i]; + if (!s.SessionVisibility) + continue; + + var leftTime = s.SessionFirstTime; + var rightTime = s.SessionSecondTime; + + + startTime = new DateTime(leftBorderTime.Year, leftBorderTime.Month, leftBorderTime.Day, leftTime.Hour, leftTime.Minute, leftTime.Second); + endTime = new DateTime(leftBorderTime.Year, leftBorderTime.Month, leftBorderTime.Day, rightTime.Hour, rightTime.Minute, rightTime.Second); + + if (leftTime.Hour > rightTime.Hour || (leftTime.Hour == rightTime.Hour && leftTime.Minute > rightTime.Minute)) + endTime = endTime.AddDays(1); + + for (int j = 0; j <= daysSpan + 1; j++) + { + if (startTime < screenRightTime && endTime > screenLeftTime) + { + if (startTime < screenLeftTime) + leftCoordinate = (int)mainWindow.CoordinatesConverter.GetChartX(screenLeftTime); + else + leftCoordinate = (int)mainWindow.CoordinatesConverter.GetChartX(startTime); + + if (endTime > screenRightTime) + rightCoordinate = (int)mainWindow.CoordinatesConverter.GetChartX(screenRightTime); + else + rightCoordinate = (int)mainWindow.CoordinatesConverter.GetChartX(endTime); + int topY = 0; + int height = panelHeight; + + if (s.DrawMode == SessionDrawMode.Simple) + { + if (rightCoordinate > leftCoordinate) + graphics.FillRectangle(s.sessionBrush, leftCoordinate, topY, rightCoordinate - leftCoordinate, panelHeight); + if (s.DrawInnerArea) + { + var startBar = this.HistoricalData[(int)mainWindow.CoordinatesConverter.GetBarIndex(startTime), SeekOriginHistory.Begin]; + var endBar = this.HistoricalData[(int)mainWindow.CoordinatesConverter.GetBarIndex(endTime), SeekOriginHistory.Begin]; + int zoneOpenY = (int)mainWindow.CoordinatesConverter.GetChartY(startBar[PriceType.Open]); + int zoneCloseY = (int)mainWindow.CoordinatesConverter.GetChartY(endBar[PriceType.Close]); + int areaY = zoneOpenY < zoneCloseY ? zoneOpenY : zoneCloseY; + graphics.FillRectangle(s.sessionBrush, leftCoordinate, areaY, rightCoordinate - leftCoordinate, Math.Abs(zoneOpenY-zoneCloseY)); + + } + } + else + { + double hi = double.MinValue; + double lo = double.MaxValue; + bool hasBars = false; + + for (int k = 0; k < this.Count; k++) + { + var t = this.Time(k); + + if (t >= endTime) + continue; + + if (t < startTime) + break; + + var bar = this.HistoricalData[k, SeekOriginHistory.End]; + double bh = bar[PriceType.High]; + double bl = bar[PriceType.Low]; + + if (bh > hi) hi = bh; + if (bl < lo) lo = bl; + hasBars = true; + } + + if (hasBars && hi > lo && rightCoordinate > leftCoordinate) + { + int yHigh = (int)this.CurrentChart.MainWindow.CoordinatesConverter.GetChartY(hi); + int yLow = (int)this.CurrentChart.MainWindow.CoordinatesConverter.GetChartY(lo); + + topY = Math.Min(yHigh, yLow); + height = Math.Abs(yHigh - yLow); + + if (height > 0) + graphics.FillRectangle(s.sessionBrush, leftCoordinate, topY, rightCoordinate - leftCoordinate, height); + + } + } + if (s.DrawBorder) + { + var tmpPen = new Pen(s.BorderLineOptions.Color, s.BorderLineOptions.Width); + tmpPen.DashStyle = (DashStyle)s.BorderLineOptions.LineStyle; + graphics.DrawRectangle(tmpPen, leftCoordinate, topY, rightCoordinate - leftCoordinate, height); + } + if (s.ShowLabel) + { + var labelRect = new Rectangle( + leftCoordinate, + topY, + Math.Max(0, rightCoordinate - leftCoordinate), + Math.Max(0, height) + ); + + this.DrawSessionLabel(graphics, labelRect, s, startTime, endTime); + } + } + + startTime = startTime.AddDays(1); + endTime = endTime.AddDays(1); + } + } + } + finally + { + graphics.SetClip(prevClipRectangle); + } + } - startTime = new DateTime(leftBorderTime.Year, leftBorderTime.Month, leftBorderTime.Day, leftTime.Hour, leftTime.Minute, leftTime.Second); - endTime = new DateTime(leftBorderTime.Year, leftBorderTime.Month, leftBorderTime.Day, rightTime.Hour, rightTime.Minute, rightTime.Second); + public override IList Settings + { + get + { + var settings = base.Settings; + for (int i = 0; i < this.sessions.Length; i++) + settings.Add(new SettingItemGroup(this.sessions[i].SessionName, this.sessions[i].Settings)); + return settings; + } + set + { + base.Settings = value; + for (int i = 0; i < this.sessions.Length; i++) + this.sessions[i].Settings = value; + } + } + private void DrawSessionLabel(Graphics g, Rectangle rect, Session s, DateTime startTime, DateTime endTime) + { + if (!s.ShowLabel || rect.Width <= 0 || rect.Height <= 0) + return; - if (leftTime.Hour > rightTime.Hour) - endTime = endTime.AddDays(1); + string text = s.LabelText ?? string.Empty; - for (int j = 0; j <= daysSpan+1; j++) + if (s.ShowDelta) { - if (startTime < mainWindow.CoordinatesConverter.GetTime(mainWindow.ClientRectangle.Right) && endTime > mainWindow.CoordinatesConverter.GetTime((int)mainWindow.ClientRectangle.Left)) - { - if (startTime < mainWindow.CoordinatesConverter.GetTime(mainWindow.ClientRectangle.Left)) - leftCoordinate = mainWindow.ClientRectangle.Left; - else - leftCoordinate = (int)mainWindow.CoordinatesConverter.GetChartX(startTime); - if (endTime > mainWindow.CoordinatesConverter.GetTime(mainWindow.ClientRectangle.Right)) - rightCoordinate = mainWindow.ClientRectangle.Right; - else - rightCoordinate = (int)mainWindow.CoordinatesConverter.GetChartX(endTime); - - graphics.FillRectangle(this.sessions[i].sessionBrush, leftCoordinate, 0, rightCoordinate - leftCoordinate, this.CurrentChart.MainWindow.ClientRectangle.Height); + var mainWindow = this.CurrentChart.MainWindow; + int iStart = (int)mainWindow.CoordinatesConverter.GetBarIndex(startTime); + int iEnd = (int)mainWindow.CoordinatesConverter.GetBarIndex(endTime); + + iStart = Math.Max(0, Math.Min(iStart, this.HistoricalData.Count - 1)); + iEnd = Math.Max(0, Math.Min(iEnd, this.HistoricalData.Count - 1)); + + var startBar = this.HistoricalData[iStart, SeekOriginHistory.Begin]; + var endBar = this.HistoricalData[iEnd, SeekOriginHistory.Begin]; + + double open = startBar?[PriceType.Open] ?? double.NaN; + double close = endBar?[PriceType.Close] ?? double.NaN; + + if (!double.IsNaN(open) && !double.IsNaN(close)) + { + double delta = close - open; + + int prec = this.Symbol != null + ? Math.Max(0, (int)Math.Round(-Math.Log10(this.Symbol.TickSize))) + : 2; + string deltaStr = delta.ToString("F" + prec); + + text = string.IsNullOrEmpty(text) ? $"Δ {deltaStr}" : $"{text} Δ {deltaStr}"; } - startTime = startTime.AddDays(1); - endTime = endTime.AddDays(1); } + + if (string.IsNullOrEmpty(text)) + return; + + var font = s.LabelFont; + var brush = s.LabelBrush; + var size = g.MeasureString(text, font); + + + bool inside = s.DrawMode == SessionDrawMode.Simple || s.LabelPlacement == LabelPlacement.Inside; + + float x = rect.Left, y = rect.Top; + int pad = 4; + if (inside) + { + x = s.LabelHAlign switch + { + NativeAlignment.Left => rect.Left + pad, + NativeAlignment.Center => rect.Left + (rect.Width - size.Width) / 2f, + NativeAlignment.Right => rect.Right - size.Width - pad, + _ => rect.Left + (rect.Width - size.Width) / 2f + }; + } + else + { + x = s.LabelHAlign switch + { + NativeAlignment.Left => rect.Left - size.Width - pad, + NativeAlignment.Center => rect.Left + (rect.Width - size.Width) / 2f, + NativeAlignment.Right => rect.Right + pad, + _ => rect.Left + (rect.Width - size.Width) / 2f + }; + } + + + if (inside) + { + y = s.LabelVAlign switch + { + LabelVAlign.Top => rect.Top + pad, + LabelVAlign.Middle => rect.Top + (rect.Height - size.Height) / 2f, + LabelVAlign.Bottom => rect.Bottom - size.Height - pad, + _ => rect.Top + (rect.Height - size.Height) / 2f + }; + } + else + { + y = s.LabelVAlign switch + { + LabelVAlign.Top => rect.Top - size.Height - pad, + LabelVAlign.Middle => rect.Top + (rect.Height - size.Height) / 2f, + LabelVAlign.Bottom => rect.Bottom + pad, + _ => rect.Top + (rect.Height - size.Height) / 2f + }; + } + + g.DrawString(text, font, brush, new PointF(x, y)); } } - public override IList Settings + internal sealed class Session : ICustomizable { - get - { - var settings = base.Settings; - for (int i = 0; i < this.sessions.Length; i++) - settings.Add(new SettingItemGroup(this.sessions[i].SessionName, this.sessions[i].Settings)); + public DateTime SessionFirstTime { get; set; } + public DateTime SessionSecondTime { get; set; } + public Color SessionColor { get; set; } + public bool SessionVisibility { get; set; } + public string SessionName { get; set; } + private int sessionSortIndex { get; set; } - return settings; - } - set + public SolidBrush sessionBrush { get; set; } + + public SessionDrawMode DrawMode { get; set; } = SessionDrawMode.Simple; + public bool DrawBorder { get; set; } = false; + + private LineOptions _borderLineOptions; + public LineOptions BorderLineOptions { - base.Settings = value; - for (int i = 0; i < this.sessions.Length; i++) - this.sessions[i].Settings = value; + get => this._borderLineOptions; + set + { + this._borderLineOptions = value; + this.borderPen.Color = value.Color; + this.borderPen.Width = value.Width; + this.borderPen.DashStyle = (DashStyle)value.LineStyle; + } } - } -} + private readonly Pen borderPen; -internal sealed class Session : ICustomizable -{ - public DateTime SessionFirstTime { get; set; } - public DateTime SessionSecondTime { get; set; } - public Color SessionColor { get; set; } - public bool SessionVisibility { get; set; } - public string SessionName { get; set; } - private int sessionSortIndex { get; set; } - public SolidBrush sessionBrush { get; set; } - public Session(Color color, string name = "Session X", int sortingIndex = 20) - { - this.SessionName = name; - this.sessionSortIndex = sortingIndex; - this.SessionColor = Color.FromArgb(51, color); - this.SessionFirstTime = new DateTime(); - this.SessionSecondTime = new DateTime(); - this.SessionVisibility = false; - this.sessionBrush = new SolidBrush(this.SessionColor); - } - public IList Settings - { - get - { - var settings = new List(); - var separatorGroup1 = new SettingItemSeparatorGroup(this.SessionName, this.sessionSortIndex); + public bool DrawInnerArea { get; set; } - string relationName = $"{this.SessionName}SessionVisibility"; - settings.Add(new SettingItemBoolean(relationName, this.SessionVisibility) - { - Text = "Visible", - SortIndex = sessionSortIndex, - SeparatorGroup = separatorGroup1, - }); + public bool ShowLabel { get; set; } + public string LabelText { get; set; } + public bool ShowDelta { get; set; } + + public Font LabelFont { get; set; } + public Color LabelColor { get; set; } + public SolidBrush LabelBrush { get; set; } - var visibleRelation = new SettingItemRelationVisibility(relationName, true); + public LabelPlacement LabelPlacement { get; set; } = LabelPlacement.Inside; + public NativeAlignment LabelHAlign { get; set; } = NativeAlignment.Center; + public LabelVAlign LabelVAlign { get; set; } = LabelVAlign.Middle; - settings.Add(new SettingItemDateTime("SessionFirstTime", this.SessionFirstTime) + public Session(Color color, string name = "Session X", int sortingIndex = 20) + { + this.SessionName = name; + this.sessionSortIndex = sortingIndex; + this.SessionColor = Color.FromArgb(51, color); + this.SessionFirstTime = new DateTime(); + this.SessionSecondTime = new DateTime(); + this.SessionVisibility = false; + + this.sessionBrush = new SolidBrush(this.SessionColor); + + this._borderLineOptions = new LineOptions(); + this._borderLineOptions.Color = Color.White; + this._borderLineOptions.Width = 1; + this._borderLineOptions.LineStyle = LineStyle.Solid; + this._borderLineOptions.WithCheckBox = false; + this._borderLineOptions.Enabled = false; + + this.borderPen = new Pen(this._borderLineOptions.Color, this._borderLineOptions.Width) { - Text = "Start Time", - SortIndex = sessionSortIndex, - Format = DatePickerFormat.Time, - SeparatorGroup = separatorGroup1, - Relation = visibleRelation - }); - - settings.Add(new SettingItemDateTime("SessionSecondTime", this.SessionSecondTime) + DashStyle = (DashStyle)this._borderLineOptions.LineStyle + }; + + this.DrawInnerArea = false; + + this.ShowLabel = false; + this.LabelText = string.Empty; + this.ShowDelta = false; + this.LabelFont = new Font("Arial", 9f, FontStyle.Regular); + this.LabelColor = Color.Yellow; + this.LabelBrush = new SolidBrush(this.LabelColor); + this.LabelPlacement= LabelPlacement.Inside; + this.LabelHAlign = NativeAlignment.Center; + this.LabelVAlign = LabelVAlign.Top; + } + + public IList Settings + { + get { - Text = "End Time", - SortIndex = sessionSortIndex, - Format = DatePickerFormat.Time, - SeparatorGroup = separatorGroup1, - Relation = visibleRelation - }); - settings.Add(new SettingItemColor("SessionColor", this.SessionColor) + var settings = new List(); + var separatorGroup1 = new SettingItemSeparatorGroup(this.SessionName, this.sessionSortIndex); + + string visibleRelationName = $"{this.SessionName}SessionVisibility"; + settings.Add(new SettingItemBoolean(visibleRelationName, this.SessionVisibility) + { + Text = "Visible", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + }); + var visibleRelation = new SettingItemRelationVisibility(visibleRelationName, true); + + var simple = new SelectItem("Simple", SessionDrawMode.Simple); + var box = new SelectItem("Box", SessionDrawMode.Box); + settings.Add(new SettingItemSelectorLocalized( + "SessionDrawMode", + new SelectItem("SessionDrawMode", this.DrawMode), + new List { simple, box }) + { + Text = "Draw mode", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = visibleRelation, + ValueChangingBehavior = SettingItemValueChangingBehavior.WithConfirmation + }); + + + settings.Add(new SettingItemDateTime("SessionFirstTime", this.SessionFirstTime) + { + Text = "Start Time", + SortIndex = sessionSortIndex, + Format = DatePickerFormat.Time, + SeparatorGroup = separatorGroup1, + Relation = visibleRelation + }); + + settings.Add(new SettingItemDateTime("SessionSecondTime", this.SessionSecondTime) + { + Text = "End Time", + SortIndex = sessionSortIndex, + Format = DatePickerFormat.Time, + SeparatorGroup = separatorGroup1, + Relation = visibleRelation + }); + + + settings.Add(new SettingItemColor("SessionColor", this.SessionColor) + { + Text = "Color", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = visibleRelation + }); + + string borderVisibleName = $"{this.SessionName}DrawBorder"; + + settings.Add(new SettingItemBoolean(borderVisibleName, this.DrawBorder) + { + Text = "Draw border", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = visibleRelation + }); + var borderRelation = new SettingItemMultipleRelation(visibleRelation, new SettingItemRelationVisibility(borderVisibleName, true)); + + + settings.Add(new SettingItemLineOptions("BorderLineOptions", this._borderLineOptions) + { + Text = "Border line", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = borderRelation, + UseEnabilityToggler = true, + ExcludedStyles = new LineStyle[] { LineStyle.Histogramm, LineStyle.Points } + }); + settings.Add(new SettingItemBoolean("DrawInnerArea", this.DrawInnerArea) + { + Text = "Draw the open-close area", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = visibleRelation + }); + settings.Add(new SettingItemBoolean("ShowLabel", this.ShowLabel) + { + Text = "Show label", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = visibleRelation + }); + var labelVisibleRelation = new SettingItemRelationVisibility("ShowLabel", true); + + settings.Add(new SettingItemTextArea("LabelText", this.LabelText) + { + Text = "Custom text", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = labelVisibleRelation + }); + + settings.Add(new SettingItemBoolean("ShowDelta", this.ShowDelta) + { + Text = "Append Δ price", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = labelVisibleRelation + }); + + settings.Add(new SettingItemFont("LabelFont", this.LabelFont) + { + Text = "Label font", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = labelVisibleRelation + }); + settings.Add(new SettingItemColor("LabelColor", this.LabelColor) + { + Text = "Label color", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = labelVisibleRelation + }); + + var allowOutsideRelation = new SettingItemMultipleRelation( + labelVisibleRelation, + new SettingItemRelationVisibility("SessionDrawMode", SessionDrawMode.Box) + ); + + settings.Add(new SettingItemSelectorLocalized("LabelPlacement", this.LabelPlacement, + new List { + new SelectItem("Inside", LabelPlacement.Inside), + new SelectItem("Outside", LabelPlacement.Outside) + }) + { + Text = "Placement (Box only)", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = allowOutsideRelation + }); + settings.Add(new SettingItemAlignment("LabelHAlign", this.LabelHAlign) + { + Text = "Horizontal align", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = labelVisibleRelation + }); + + settings.Add(new SettingItemSelectorLocalized("LabelVAlign", this.LabelVAlign, + new List { + new SelectItem("Top", LabelVAlign.Top), + new SelectItem("Middle", LabelVAlign.Middle), + new SelectItem("Bottom", LabelVAlign.Bottom), + }) + { + Text = "Vertical align", + SortIndex = sessionSortIndex, + SeparatorGroup = separatorGroup1, + Relation = labelVisibleRelation + }); + return settings; + } + set { - Text = "Color", - SortIndex = sessionSortIndex, - SeparatorGroup = separatorGroup1, - Relation = visibleRelation - }); + var settings = new List(); + string borderVisibleName = $"{this.SessionName}DrawBorder"; - return settings; - } - set - { - var settings = new List(); - - if (value.TryGetValue(this.SessionName, out List inputSettings)) - settings = inputSettings; - if (settings.TryGetValue($"{this.SessionName}SessionVisibility", out bool SessionVisibility)) - this.SessionVisibility = SessionVisibility; - if (settings.TryGetValue("SessionFirstTime", out DateTime SessionFirstTime)) - this.SessionFirstTime = SessionFirstTime; - if (settings.TryGetValue("SessionSecondTime", out DateTime SessionSecondTime)) - this.SessionSecondTime = SessionSecondTime; - if (settings.TryGetValue("SessionColor", out Color SessionColor)) - this.SessionColor = SessionColor; - this.sessionBrush.Color = this.SessionColor; + if (value.TryGetValue(this.SessionName, out List inputSettings)) + settings = inputSettings; + + if (settings.TryGetValue($"{this.SessionName}SessionVisibility", out bool SessionVisibility)) + this.SessionVisibility = SessionVisibility; + + if (settings.TryGetValue("SessionDrawMode", out SessionDrawMode drawMode)) + this.DrawMode = drawMode; + + if (settings.TryGetValue("SessionFirstTime", out DateTime SessionFirstTime)) + this.SessionFirstTime = SessionFirstTime; + + if (settings.TryGetValue("SessionSecondTime", out DateTime SessionSecondTime)) + this.SessionSecondTime = SessionSecondTime; + + if (settings.TryGetValue("SessionColor", out Color SessionColor)) + { + this.SessionColor = SessionColor; + this.sessionBrush.Color = this.SessionColor; + } + + if (settings.TryGetValue(borderVisibleName, out bool drawBorder)) + this.DrawBorder = drawBorder; + + if (settings.TryGetValue("BorderLineOptions", out LineOptions borderLineOptions)) + this.BorderLineOptions = borderLineOptions; + + if (settings.TryGetValue("DrawInnerArea", out bool DrawInnerArea)) + this.DrawInnerArea = DrawInnerArea; + + if (settings.TryGetValue("ShowLabel", out bool showLabel)) + this.ShowLabel = showLabel; + + if (settings.TryGetValue("LabelText", out string labelText)) + this.LabelText = labelText; + + if (settings.TryGetValue("ShowDelta", out bool showDelta)) + this.ShowDelta = showDelta; + + if (settings.TryGetValue("LabelFont", out Font labelFont)) + this.LabelFont = labelFont; + + if (settings.TryGetValue("LabelColor", out Color labelColor)) + { + this.LabelColor = labelColor; + this.LabelBrush.Color = labelColor; + } + + if (settings.TryGetValue("LabelPlacement", out LabelPlacement labelPlacement)) + this.LabelPlacement = labelPlacement; + + if (settings.TryGetValue("LabelHAlign", out NativeAlignment hAlign)) + this.LabelHAlign = hAlign; + + if (settings.TryGetValue("LabelVAlign", out LabelVAlign vAlign)) + this.LabelVAlign = vAlign; + } } } -} \ No newline at end of file + internal enum SessionDrawMode + { + Simple = 0, + Box = 1 + } + internal enum LabelPlacement { Inside = 0, Outside = 1 } + internal enum LabelVAlign { Top = 0, Middle = 1, Bottom = 2 } +} diff --git a/Indicators/IndicatorVerticalLine.cs b/Indicators/IndicatorVerticalLine.cs index 03beaf8..c839a8f 100644 --- a/Indicators/IndicatorVerticalLine.cs +++ b/Indicators/IndicatorVerticalLine.cs @@ -11,6 +11,17 @@ namespace ChanneIsIndicators; public class IndicatorVerticalLine : Indicator { private TimeLine[] lines = new TimeLine[5] { new TimeLine(Color.Green, "First Line", 1), new TimeLine(Color.Red, "Second Line", 2), new TimeLine(Color.GreenYellow, "Third Line", 3), new TimeLine(Color.Blue, "Fourth Line", 4), new TimeLine(Color.Cyan, "Fifth Line", 5) }; + private bool allWeekAvailable = true; + private Dictionary awailableDays = new Dictionary() + { + {DayOfWeek.Monday, true}, + {DayOfWeek.Tuesday, true}, + {DayOfWeek.Wednesday, true}, + {DayOfWeek.Thursday, true}, + {DayOfWeek.Friday, true}, + {DayOfWeek.Saturday, true}, + {DayOfWeek.Sunday, true}, + }; private Period currentPeriod; @@ -58,38 +69,42 @@ public override void OnPaintChart(PaintChartEventArgs args) { DateTime lineTime = lines[i].Time; - DateTime currentLineTime = new DateTime(leftBorderTime.Year, leftBorderTime.Month, leftBorderTime.Day, lineTime.Hour, lineTime.Minute, lineTime.Second, DateTimeKind.Utc); + DateTime currentLineTime = new DateTime(leftBorderTime.Year, leftBorderTime.Month, leftBorderTime.Day, lineTime.Hour, lineTime.Minute, lineTime.Second, DateTimeKind.Utc); if (currentLineTime < leftBorderTime) currentLineTime = currentLineTime.AddDays(1); if (currentLineTime > rightBorderTime) currentLineTime = currentLineTime.AddDays(-1); - // int labelY = 0; if (lines[i].LabelPosition == Position.MiddleLeft || lines[i].LabelPosition == Position.MiddleRight) - labelY=bottomY/2; + labelY = bottomY / 2; else if (lines[i].LabelPosition == Position.BottomLeft || lines[i].LabelPosition == Position.BottomRight) - labelY=bottomY; + labelY = bottomY; if (lines[i].LineVisibility && currentLineTime > leftBorderTime && currentLineTime < rightBorderTime) - { + { + bool drawCurrentLine = true; for (int j = 0; j <= daysSpan; j++) { - int topX = (int)mainWindow.CoordinatesConverter.GetChartX(currentLineTime); - graphics.DrawLine(lines[i].linePen, topX, 0, topX, bottomY); - - if (lines[i].LabelVisibility) + if (!this.allWeekAvailable) + drawCurrentLine = this.awailableDays[currentLineTime.DayOfWeek]; + if (drawCurrentLine) { - // - string labelText = ""; - if (lines[i].textFormat == Format.DateTime || lines[i].textFormat == Format.DateTimeText) - labelText = Core.Instance.TimeUtils.ConvertFromUTCToSelectedTimeZone(currentLineTime).ToString(); - if (lines[i].textFormat == Format.DateTimeText || lines[i].textFormat == Format.Text) - labelText = labelText + " " + lines[i].labelText; + int topX = (int)mainWindow.CoordinatesConverter.GetChartX(currentLineTime); + graphics.DrawLine(lines[i].linePen, topX, 0, topX, bottomY); + if (lines[i].LabelVisibility) + { + // + string labelText = ""; + if (lines[i].textFormat == Format.DateTime || lines[i].textFormat == Format.DateTimeText) + labelText = Core.Instance.TimeUtils.ConvertFromUTCToSelectedTimeZone(currentLineTime).ToString(); + if (lines[i].textFormat == Format.DateTimeText || lines[i].textFormat == Format.Text) + labelText = labelText + " " + lines[i].labelText; - // - graphics.DrawString(labelText, lines[i].labelFont, lines[i].labelBrush, new PointF(topX, labelY), lines[i].lineSF); + // + graphics.DrawString(labelText, lines[i].labelFont, lines[i].labelBrush, new PointF(topX, labelY), lines[i].lineSF); + } } currentLineTime = currentLineTime.AddDays(1); } @@ -107,6 +122,63 @@ public override IList Settings get { var settings = base.Settings; + SettingItemSeparatorGroup availableDaysGroup = new SettingItemSeparatorGroup("Days Settings", 0); + settings.Add(new SettingItemBooleanSwitcher("allWeekAvailable", this.allWeekAvailable) + { + Text = "Draw Every Day Line", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + }); + SettingItemRelationVisibility visibleRelationNotAllDays = new SettingItemRelationVisibility("allWeekAvailable", false); + settings.Add(new SettingItemBoolean("Monday", this.awailableDays[DayOfWeek.Monday]) + { + Text = "Monday", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + Relation = visibleRelationNotAllDays + }); + settings.Add(new SettingItemBoolean("Tuesday", this.awailableDays[DayOfWeek.Tuesday]) + { + Text = "Tuesday", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + Relation = visibleRelationNotAllDays + }); + settings.Add(new SettingItemBoolean("Wednesday", this.awailableDays[DayOfWeek.Wednesday]) + { + Text = "Wednesday", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + Relation = visibleRelationNotAllDays + }); + settings.Add(new SettingItemBoolean("Thursday", this.awailableDays[DayOfWeek.Thursday]) + { + Text = "Thursday", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + Relation = visibleRelationNotAllDays + }); + settings.Add(new SettingItemBoolean("Friday", this.awailableDays[DayOfWeek.Friday]) + { + Text = "Friday", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + Relation = visibleRelationNotAllDays + }); + settings.Add(new SettingItemBoolean("Saturday", this.awailableDays[DayOfWeek.Saturday]) + { + Text = "Saturday", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + Relation = visibleRelationNotAllDays + }); + settings.Add(new SettingItemBoolean("Sunday", this.awailableDays[DayOfWeek.Sunday]) + { + Text = "Sunday", + SortIndex = 0, + SeparatorGroup = availableDaysGroup, + Relation = visibleRelationNotAllDays + }); for (int i = 0; i < lines.Length; i++) settings.Add(new SettingItemGroup(lines[i].LineName, lines[i].Settings)); @@ -115,6 +187,22 @@ public override IList Settings set { base.Settings = value; + if (value.TryGetValue("allWeekAvailable", out bool allWeekAvailable)) + this.allWeekAvailable = allWeekAvailable; + if (value.TryGetValue("Monday", out bool Monday)) + this.awailableDays[DayOfWeek.Monday] = Monday; + if (value.TryGetValue("Tuesday", out bool Tuesday)) + this.awailableDays[DayOfWeek.Tuesday] = Tuesday; + if (value.TryGetValue("Wednesday", out bool Wednesday)) + this.awailableDays[DayOfWeek.Wednesday] = Wednesday; + if (value.TryGetValue("Thursday", out bool Thursday)) + this.awailableDays[DayOfWeek.Thursday] = Thursday; + if (value.TryGetValue("Friday", out bool Friday)) + this.awailableDays[DayOfWeek.Friday] = Friday; + if (value.TryGetValue("Saturday", out bool Saturday)) + this.awailableDays[DayOfWeek.Saturday] = Saturday; + if (value.TryGetValue("Sunday", out bool Sunday)) + this.awailableDays[DayOfWeek.Sunday] = Sunday; for (int i = 0; i < lines.Length; i++) { lines[i].Settings = value; @@ -143,7 +231,7 @@ public Position LabelPosition get => this.labelPosition; set { - this.labelPosition=value; + this.labelPosition = value; this.UpdateLineSF(); } @@ -155,7 +243,7 @@ public Orientation LabelOrientation get => this.labelOrientation; set { - this.labelOrientation=value; + this.labelOrientation = value; this.UpdateLineSF(); } @@ -189,7 +277,7 @@ public IList Settings string relationName = this.LineName + "LineVisibility"; string relationNameLabel = this.LineName + "ShowLabel"; string relationNameCustomTextFormat = this.LineName + "CustomTextFormat"; - settings.Add(new SettingItemBoolean(relationName, this.LineVisibility) + settings.Add(new SettingItemBooleanSwitcher(relationName, this.LineVisibility) { Text = "Line Visibility", SortIndex = lineSortIndex, @@ -197,7 +285,7 @@ public IList Settings }); SettingItemRelationVisibility visibleRelation = new SettingItemRelationVisibility(relationName, true); SettingItemRelationVisibility visibleRelationLabel = new SettingItemRelationVisibility(relationNameLabel, true); - settings.Add(new SettingItemDateTime("LineTime", this.Time) + settings.Add(new SettingItemDateTime(this.LineName + "LineTime", this.Time) { Text = "Line Time", SortIndex = lineSortIndex, @@ -205,7 +293,7 @@ public IList Settings SeparatorGroup = separatorGroup1, Relation = visibleRelation }); - settings.Add(new SettingItemLineOptions("LineStyle", this.lineOptions) + settings.Add(new SettingItemLineOptions(this.LineName + "LineStyle", this.lineOptions) { Text = "Line Style", SortIndex = lineSortIndex, @@ -226,38 +314,39 @@ public IList Settings Text = "Label format", SortIndex = lineSortIndex, SeparatorGroup = separatorGroup1, - Relation = visibleRelationLabel + Relation = visibleRelationLabel, + }); SettingItemRelationVisibility visibleRelationCustomText = new SettingItemRelationVisibility(relationNameCustomTextFormat, new SelectItem[2] { new SelectItem("Text", Format.Text), new SelectItem("DateTime+Text", Format.DateTimeText) }); - settings.Add(new SettingItemTextArea("LabelText", this.labelText) + settings.Add(new SettingItemTextArea(this.LineName + "LabelText", this.labelText) { Text = "Custom text", SortIndex = lineSortIndex, SeparatorGroup = separatorGroup1, Relation = visibleRelationCustomText, }); - settings.Add(new SettingItemFont("Font", this.labelFont) + settings.Add(new SettingItemFont(this.LineName + "Font", this.labelFont) { Text = "Font", SortIndex = lineSortIndex, SeparatorGroup = separatorGroup1, Relation = visibleRelationLabel }); - settings.Add(new SettingItemColor("FontColor", this.labelColor) + settings.Add(new SettingItemColor(this.LineName + "FontColor", this.labelColor) { Text = "Font Color", SortIndex = lineSortIndex, SeparatorGroup = separatorGroup1, Relation = visibleRelationLabel }); - settings.Add(new SettingItemSelectorLocalized("LabelPosition", this.LabelPosition, new List { new SelectItem("Top Right", Position.TopRight), new SelectItem("Top Left", Position.TopLeft), new SelectItem("Bottom Right", Position.BottomRight), new SelectItem("Bottom Left", Position.BottomLeft), new SelectItem("Middle Left", Position.MiddleLeft), new SelectItem("Middle Right", Position.MiddleRight) }) + settings.Add(new SettingItemSelectorLocalized(this.LineName + "LabelPosition", this.LabelPosition, new List { new SelectItem("Top Right", Position.TopRight), new SelectItem("Top Left", Position.TopLeft), new SelectItem("Bottom Right", Position.BottomRight), new SelectItem("Bottom Left", Position.BottomLeft), new SelectItem("Middle Left", Position.MiddleLeft), new SelectItem("Middle Right", Position.MiddleRight) }) { Text = "Label position", SortIndex = lineSortIndex, SeparatorGroup = separatorGroup1, Relation = visibleRelationLabel }); - settings.Add(new SettingItemSelectorLocalized("LabelOrientation", this.LabelOrientation, new List { new SelectItem("Horizontal", Orientation.Horizontal), new SelectItem("Vertical", Orientation.Vertical) }) + settings.Add(new SettingItemSelectorLocalized(this.LineName + "LabelOrientation", this.LabelOrientation, new List { new SelectItem("Horizontal", Orientation.Horizontal), new SelectItem("Vertical", Orientation.Vertical) }) { Text = "Label orientation", SortIndex = lineSortIndex, @@ -273,9 +362,9 @@ public IList Settings settings = inputSettings; if (settings.TryGetValue(LineName + "LineVisibility", out bool SessionVisibility)) this.LineVisibility = SessionVisibility; - if (settings.TryGetValue("LineTime", out DateTime SessionFirstTime)) + if (settings.TryGetValue(this.LineName + "LineTime", out DateTime SessionFirstTime)) this.Time = SessionFirstTime; - if (settings.TryGetValue("LineStyle", out LineOptions lineStyle)) + if (settings.TryGetValue(this.LineName + "LineStyle", out LineOptions lineStyle)) { this.lineOptions = lineStyle; @@ -287,18 +376,18 @@ public IList Settings this.LabelVisibility = LabelVisibility; if (settings.TryGetValue(this.LineName + "CustomTextFormat", out Format textFormat)) this.textFormat = textFormat; - if (settings.TryGetValue("LabelText", out string customText)) + if (settings.TryGetValue(this.LineName + "LabelText", out string customText)) this.labelText = customText; - if (settings.TryGetValue("Font", out Font labelFont)) + if (settings.TryGetValue(this.LineName + "Font", out Font labelFont)) this.labelFont = labelFont; - if (settings.TryGetValue("FontColor", out Color labelColor)) + if (settings.TryGetValue(this.LineName + "FontColor", out Color labelColor)) { this.labelColor = labelColor; this.labelBrush.Color = labelColor; } - if (settings.TryGetValue("LabelPosition", out Position labelPosition)) + if (settings.TryGetValue(this.LineName + "LabelPosition", out Position labelPosition)) this.LabelPosition = labelPosition; - if (settings.TryGetValue("LabelOrientation", out Orientation labelOrientation)) + if (settings.TryGetValue(this.LineName + "LabelOrientation", out Orientation labelOrientation)) this.LabelOrientation = labelOrientation; } } @@ -386,4 +475,4 @@ public enum Orientation Vertical } -#endregion +#endregion \ No newline at end of file diff --git a/Indicators/IndicatorVolume.cs b/Indicators/IndicatorVolume.cs index c148e7f..9f0281b 100644 --- a/Indicators/IndicatorVolume.cs +++ b/Indicators/IndicatorVolume.cs @@ -97,8 +97,8 @@ public IndicatorVolume() { Color1 = Color.FromArgb(255, 251, 87, 87), Color2 = Color.FromArgb(255, 0, 178, 89), - Text1 = "Up", - Text2 = "Down" + Text1 = "Down", + Text2 = "Up" }; } @@ -136,6 +136,8 @@ protected override void OnUpdate(UpdateArgs args) this.LinesSeries[0].SetMarker(0, this.pairColor.Color2); else if (this.Open() > this.Close()) this.LinesSeries[0].SetMarker(0, this.pairColor.Color1); + else if(this.Open() == this.Close()) + this.LinesSeries[0].SetMarker(0, this.LinesSeries[0].Color); break; } case VolumeColoringScheme.ByDifference: diff --git a/Indicators/IndicatorZigZag.cs b/Indicators/IndicatorZigZag.cs index f8ea4f6..0d03e67 100644 --- a/Indicators/IndicatorZigZag.cs +++ b/Indicators/IndicatorZigZag.cs @@ -1,6 +1,9 @@ // Copyright QUANTOWER LLC. © 2017-2024. All rights reserved. +using System; +using System.Collections.Generic; using System.Drawing; +using System.Linq; using TradingPlatform.BusinessLayer; namespace Trend; @@ -14,11 +17,18 @@ public sealed class IndicatorZigZag : Indicator [InputParameter("Percent Deviation", 0, 0.01, 100.0, 0.01, 2)] public double deviation = 0.1; + private bool isLabelVisible; + private Color upLabelColor; + private Color downLabelColor; + private Font labelFont; + private LabelFormat labelFormat; + // Defines ZigZag calculation variables. - private int trendLineLenght; - private int retracementLenght; + private int trendLineLength; + private int retracementLength; private int direction; private double lastTurnPoint; + private double lastPriceChangeDirection; public override string ShortName => $"ZZ ({this.deviation})"; public override string HelpLink => "https://help.quantower.com/analytics-panels/chart/technical-indicators/trend/zigzag"; @@ -38,6 +48,12 @@ public IndicatorZigZag() this.AddLineSeries("ZZ'Line", Color.Yellow, 2, LineStyle.Solid); this.SeparateWindow = false; + + this.isLabelVisible = true; + this.upLabelColor = Color.Green; + this.downLabelColor = Color.Red; + this.labelFont = SystemFonts.DefaultFont; + this.labelFormat = LabelFormat.PriceAndChange; } /// @@ -46,10 +62,11 @@ public IndicatorZigZag() protected override void OnInit() { // Initializes calculation parameters. - this.trendLineLenght = 0; - this.retracementLenght = 0; + this.trendLineLength = 0; + this.retracementLength = 0; this.direction = 1; this.lastTurnPoint = 0; + this.lastPriceChangeDirection = double.NaN; } /// @@ -64,8 +81,8 @@ protected override void OnUpdate(UpdateArgs args) // Changes calculation parameters on each bar. if (args.Reason != UpdateReason.NewTick) { - this.trendLineLenght++; - this.retracementLenght++; + this.trendLineLength++; + this.retracementLength++; } if (this.Count == 0) @@ -81,8 +98,8 @@ protected override void OnUpdate(UpdateArgs args) if (high >= this.lastTurnPoint) { this.lastTurnPoint = high; - this.retracementLenght = 0; - this.DrawTrendLine(this.trendLineLenght + 1); + this.retracementLength = 0; + this.DrawTrendLine(this.trendLineLength + 1); return; } // Sloping trend detection block. @@ -90,9 +107,10 @@ protected override void OnUpdate(UpdateArgs args) { this.lastTurnPoint = low; this.direction = -1; - this.trendLineLenght = this.retracementLenght; - this.retracementLenght = 0; - this.DrawTrendLine(this.trendLineLenght + 1); + this.trendLineLength = this.retracementLength; + this.retracementLength = 0; + this.DrawTrendLine(this.trendLineLength + 1); + this.DrawLabel(this.trendLineLength); return; } } @@ -103,8 +121,8 @@ protected override void OnUpdate(UpdateArgs args) if (low <= this.lastTurnPoint) { this.lastTurnPoint = low; - this.retracementLenght = 0; - this.DrawTrendLine(this.trendLineLenght + 1); + this.retracementLength = 0; + this.DrawTrendLine(this.trendLineLength + 1); return; } // Sloping trend detection block. @@ -112,9 +130,10 @@ protected override void OnUpdate(UpdateArgs args) { this.lastTurnPoint = high; this.direction = 1; - this.trendLineLenght = this.retracementLenght; - this.retracementLenght = 0; - this.DrawTrendLine(this.trendLineLenght + 1); + this.trendLineLength = this.retracementLength; + this.retracementLength = 0; + this.DrawTrendLine(this.trendLineLength + 1); + this.DrawLabel(this.trendLineLength); return; } } @@ -137,4 +156,129 @@ private void DrawTrendLine(int x) this.SetValue(y, 0, i); } } + + private void DrawLabel(int x) + { + if (!this.isLabelVisible) + return; + + var upperIcon = this.direction == -1 ? IndicatorLineMarkerIconType.Text : IndicatorLineMarkerIconType.None; + var bottomIcon = this.direction == 1 ? IndicatorLineMarkerIconType.Text : IndicatorLineMarkerIconType.None; + var marker = new IndicatorLineMarker(this.LinesSeries[0].Color, upperIcon, bottomIcon); + + var currentPrice = this.LinesSeries[0].GetValue(x); + var priceText = this.Symbol.FormatPrice(currentPrice); + var percent = (currentPrice - this.lastPriceChangeDirection) / this.lastPriceChangeDirection * 100; + var percentText = double.IsNaN(percent) ? string.Empty : $"{Math.Round(percent, 2).ToString()}%"; + + var labelText = this.labelFormat switch + { + LabelFormat.Price => priceText, + LabelFormat.Change => percentText, + LabelFormat.PriceAndChange => $"{priceText} ({percentText})", + + _ => priceText + }; + + var color = this.LinesSeries[0].Color; + if (percent > 0) + color = this.upLabelColor; + else if (percent < 0) + color = this.downLabelColor; + + marker.Color = color; + marker.PaintLine = false; + marker.TextSettings.Text = labelText; + marker.TextSettings.Font = this.labelFont; + this.LinesSeries[0].SetMarker(x, marker); + + this.lastPriceChangeDirection = this.LinesSeries[0].GetValue(x); + } + + public override IList Settings + { + get + { + var settings = base.Settings; + + var separatorGroup = settings.FirstOrDefault(s => s.Name == "Percent Deviation")?.SeparatorGroup; + + settings.Add(new SettingItemBoolean("IsLabelVisible", this.isLabelVisible) + { + Text = loc._("Is price label visible"), + SeparatorGroup = separatorGroup + }); + var labelVisibleRelation = new SettingItemRelationVisibility("IsLabelVisible", true); + settings.Add(new SettingItemPairColor("LabelColors", new PairColor(this.upLabelColor, this.downLabelColor, loc._("Up"), loc._("Down"))) + { + Text = loc._("Label colors"), + Relation = labelVisibleRelation, + SeparatorGroup = separatorGroup + }); + settings.Add(new SettingItemFont("LabelFont", this.labelFont) + { + Text = loc._("Label font"), + Relation = labelVisibleRelation, + SeparatorGroup = separatorGroup + }); + var labelFormats = new List() + { + new SelectItem(loc._("Price"), LabelFormat.Price), + new SelectItem(loc._("Change"), LabelFormat.Change), + new SelectItem(loc._("Price and change"), LabelFormat.PriceAndChange), + }; + settings.Add(new SettingItemSelectorLocalized("LabelFormat", labelFormats.GetItemByValue(this.labelFormat), labelFormats) + { + Text = loc._("Label format"), + Relation = labelVisibleRelation, + SeparatorGroup = separatorGroup + }); + + return settings; + } + set + { + base.Settings = value; + var holder = new SettingsHolder(value); + + var settingsUpdated = false; + if (holder.TryGetValue("IsLabelVisible", out var item)) + { + settingsUpdated |= this.isLabelVisible != (bool)item.Value; + this.isLabelVisible = (bool)item.Value; + } + + if (holder.TryGetValue("LabelFormat", out item)) + { + var selectItem = (SelectItem)item.Value; + settingsUpdated |= (LabelFormat)selectItem.Value != this.labelFormat; + this.labelFormat = (LabelFormat)selectItem.Value; + } + + if (holder.TryGetValue("LabelFont", out item)) + { + settingsUpdated |= this.labelFont != (Font)item.Value; + this.labelFont = (Font)item.Value; + } + + if (holder.TryGetValue("LabelColors", out item)) + { + var pairColor = (PairColor)item.Value; + + settingsUpdated |= this.upLabelColor != pairColor.Color1 || this.downLabelColor != pairColor.Color2; + this.upLabelColor = pairColor.Color1; + this.downLabelColor = pairColor.Color2; + } + + if (settingsUpdated) + this.OnSettingsUpdated(); + } + } + + public enum LabelFormat + { + Price, + Change, + PriceAndChange + } } \ No newline at end of file