From 7fef9b1050cd75b68b8c30fc585171087f9c70ba Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 14 Mar 2026 09:39:09 -0400 Subject: [PATCH] Improve BollingerBandRendering --- .../Renderers/BollingerBandRenderer.cs | 244 +++++++++--------- PortfolioManager/saveparams.config | 4 +- 2 files changed, 130 insertions(+), 118 deletions(-) diff --git a/PortfolioManager/Renderers/BollingerBandRenderer.cs b/PortfolioManager/Renderers/BollingerBandRenderer.cs index 4e2f202..46c5507 100644 --- a/PortfolioManager/Renderers/BollingerBandRenderer.cs +++ b/PortfolioManager/Renderers/BollingerBandRenderer.cs @@ -55,6 +55,13 @@ namespace PortfolioManager.Renderers Plotter = plotter ?? throw new ArgumentNullException(nameof(plotter)); PropertyChanged += OnBollingerBandRendererPropertyChanged; } + + private DateTime EarliestBollingerBandDate {get;set;} // This gets set when the bollinger band is generated + + private bool IsVisible(DateTime date) + { + return syncTradeToBand || date >= EarliestBollingerBandDate; + } private void OnBollingerBandRendererPropertyChanged(Object sender, PropertyChangedEventArgs eventArgs) { @@ -94,6 +101,7 @@ namespace PortfolioManager.Renderers { lock(Plotter.Plot.Sync) { + int bollingerBandMovingAverageDays = 20; MDTrace.WriteLine(LogLevel.DEBUG,$"[SetData] ENTER"); this.selectedSymbol = selectedSymbol; this.selectedDayCount = selectedDayCount; @@ -106,13 +114,18 @@ namespace PortfolioManager.Renderers if (null != portfolioTrades && 0 != portfolioTrades.Count) { DateGenerator dateGenerator = new DateGenerator(); - DateTime earliestTrade = portfolioTrades[0].TradeDate; - earliestTrade = earliestTrade.AddDays(-30); + DateTime earliestTrade = portfolioTrades.First().TradeDate; // portfolio trades are ordered with earliest trade in the lowest index + earliestTrade = dateGenerator.GenerateHistoricalDate(earliestTrade, bollingerBandMovingAverageDays); // cover the moving average lag in the band int daysBetween = dateGenerator.DaysBetween(earliestTrade, DateTime.Now); - if (daysBetween < selectedDayCount || !syncTradeToBand) prices = PricingDA.GetPrices(selectedSymbol, selectedDayCount); - else prices = PricingDA.GetPrices(selectedSymbol, earliestTrade); - - DateTime earliestInsiderTransactionDate = dateGenerator.GenerateFutureBusinessDate(prices[prices.Count - 1].Date, 30); + if (daysBetween < selectedDayCount || !syncTradeToBand) + { + prices = PricingDA.GetPrices(selectedSymbol, selectedDayCount + bollingerBandMovingAverageDays); + } + else + { + prices = PricingDA.GetPrices(selectedSymbol, earliestTrade); + } + DateTime earliestInsiderTransactionDate = dateGenerator.GenerateFutureBusinessDate(prices.Last().Date, 30); insiderTransactionSummaries = InsiderTransactionDA.GetInsiderTransactionSummaries(selectedSymbol, earliestInsiderTransactionDate); // calculate the break even price on the open trades for this symbol @@ -123,8 +136,8 @@ namespace PortfolioManager.Renderers if (!syncTradeToBand) { - DateTime earliestPricingDate = prices[prices.Count - 1].Date; - earliestPricingDate = earliestPricingDate.AddDays(30); + DateTime earliestPricingDate = prices.Last().Date; + earliestPricingDate = dateGenerator.GenerateHistoricalDate(earliestPricingDate, bollingerBandMovingAverageDays); IEnumerable tradesInRange = (from portfolioTrade in portfolioTradesLots where portfolioTrade.TradeDate >= earliestPricingDate select portfolioTrade); portfolioTrades = new PortfolioTrades(); foreach (PortfolioTrade portfolioTrade in tradesInRange) portfolioTrades.Add(portfolioTrade); @@ -133,15 +146,16 @@ namespace PortfolioManager.Renderers } else { - prices = PricingDA.GetPrices(selectedSymbol, selectedDayCount); + prices = PricingDA.GetPrices(selectedSymbol, selectedDayCount + bollingerBandMovingAverageDays); if (null != prices && 0 != prices.Count) { DateGenerator dateGenerator = new DateGenerator(); - DateTime earliestInsiderTransactionDate = dateGenerator.GenerateFutureBusinessDate(prices[prices.Count - 1].Date, 30); + DateTime earliestInsiderTransactionDate = dateGenerator.GenerateFutureBusinessDate(prices.Last().Date, 30); insiderTransactionSummaries = InsiderTransactionDA.GetInsiderTransactionSummaries(selectedSymbol, earliestInsiderTransactionDate); } } - bollingerBands = BollingerBandGenerator.GenerateBollingerBands(prices); + bollingerBands = BollingerBandGenerator.GenerateBollingerBands(prices, bollingerBandMovingAverageDays); + EarliestBollingerBandDate = bollingerBands.Min(x=>x.Date); textMarkerManager.Clear(); CalculateOffsets(); GenerateBollingerBands(); @@ -154,7 +168,7 @@ namespace PortfolioManager.Renderers MDTrace.WriteLine(LogLevel.DEBUG,$"[SetData] LEAVE"); } } - + /// /// These offsets are used to place markers relative to the area in which the graph occupies. /// @@ -236,138 +250,134 @@ namespace PortfolioManager.Renderers imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image); } - /// - /// Generate Stop Limits - /// private void GenerateStopLimits() { - if (null == externalStopLimits && null == zeroPrice) return; - if (null != externalStopLimits) - { - StopLimits = StopLimitCompositeModel.CreateCompositeDataSource(externalStopLimits); - } - else if (null != internalStopLimits && null != zeroPrice) - { - StopLimits = StopLimitCompositeModel.CreateCompositeDataSource(zeroPrice.Date, internalStopLimits); - } - (DateTime[] dates, double[] values) = StopLimits.ToXYData(); + if (externalStopLimits == null && zeroPrice == null) return; - // Add the image markers - Image imageStopLimitMarker = TextMarkerImageGenerator.ToSPImage(ImageCache.GetInstance().GetImage(ImageCache.ImageType.RedTriangleUp)); - for (int index = 0; index < dates.Length; index++) - { - DateTime date = dates[index]; - double value = values[index]; - Coordinates coordinates = new Coordinates(date.ToOADate(), value); - ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, imageStopLimitMarker, SizeFactor.Normal); - } + // Create the composite data source + if (externalStopLimits != null) + StopLimits = StopLimitCompositeModel.CreateCompositeDataSource(externalStopLimits); + else if (internalStopLimits != null && zeroPrice != null) + StopLimits = StopLimitCompositeModel.CreateCompositeDataSource(zeroPrice.Date, internalStopLimits); - if(!showTradeLabels)return; - Price latestPrice = prices[0]; + (DateTime[] dates, double[] values) = StopLimits.ToXYData(); - // Add the text marker - if (null != externalStopLimits) - { - for (int index = 0; index < externalStopLimits.Count; index++) + // Add the image markers + Image imageStopLimitMarker = TextMarkerImageGenerator.ToSPImage( + ImageCache.GetInstance().GetImage(ImageCache.ImageType.RedTriangleUp)); + + for (int i = 0; i < dates.Length; i++) { - StopLimit limit = externalStopLimits[index]; + DateTime date = dates[i]; - StringBuilder sb = new StringBuilder(); - sb.Append(limit.StopType).Append(" "); - sb.Append(Utility.FormatCurrency(limit.StopPrice)); - if (index == externalStopLimits.Count - 1) - { - double percentOffsetFromLow = ((latestPrice.Low - limit.StopPrice) / limit.StopPrice); - sb.Append(" (").Append(percentOffsetFromLow > 0 ? "+" : "").Append(Utility.FormatPercent(percentOffsetFromLow)).Append(")"); - } - Image image = TextMarkerImageGenerator.GenerateImage(sb.ToString(), FontFactor.FontSize); - Coordinates coordinates = new Coordinates(limit.EffectiveDate.ToOADate(), - limit.StopPrice - offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + if (!IsVisible(date)) + continue; - coordinates = textMarkerManager.GetBestMarkerLocation(coordinates.X, coordinates.Y,offsets, offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); - - ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image); + double value = values[i]; + Coordinates coordinates = new Coordinates(date.ToOADate(), value); + Plotter.Plot.Add.ImageMarker(coordinates, imageStopLimitMarker, SizeFactor.Normal); } - } - else - { - if (null == zeroPrice) return; - if (null == internalStopLimits || null == zeroPrice) return; - for (int index = 0; index < internalStopLimits.Count; index++) + + if (!showTradeLabels) return; + + Price latestPrice = prices[0]; + + // Helper to draw text markers + void DrawTextMarker(StopLimit limit, bool isLast) { - StopLimit limit = internalStopLimits[index]; + if (!IsVisible(limit.EffectiveDate)) return; - StringBuilder sb = new StringBuilder(); - sb.Append(limit.StopType).Append(" "); - sb.Append(Utility.FormatCurrency(limit.StopPrice)); - if (index == internalStopLimits.Count - 1) - { - double percentOffsetFromLow = ((latestPrice.Low - limit.StopPrice) / limit.StopPrice); - sb.Append(" (").Append(percentOffsetFromLow > 0 ? "+" : "").Append(Utility.FormatPercent(percentOffsetFromLow)).Append(")"); - } - Image image = TextMarkerImageGenerator.GenerateImage(sb.ToString(), FontFactor.FontSize); - Coordinates coordinates = new Coordinates(limit.EffectiveDate.ToOADate(), - limit.StopPrice - offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + StringBuilder sb = new StringBuilder(); + sb.Append(limit.StopType).Append(" "); + sb.Append(Utility.FormatCurrency(limit.StopPrice)); - coordinates = textMarkerManager.GetBestMarkerLocation(coordinates.X, coordinates.Y,offsets, offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + if (isLast) + { + double percentOffsetFromLow = (latestPrice.Low - limit.StopPrice) / limit.StopPrice; + sb.Append(" (") + .Append(percentOffsetFromLow > 0 ? "+" : "") + .Append(Utility.FormatPercent(percentOffsetFromLow)) + .Append(")"); + } - ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image); - } - - // double percentOffsetFromLow = ((latestPrice.Low - stopLimit.StopPrice) / stopLimit.StopPrice); + Image image = TextMarkerImageGenerator.GenerateImage(sb.ToString(), FontFactor.FontSize); + Coordinates coordinates = new Coordinates( + limit.EffectiveDate.ToOADate(), + limit.StopPrice - offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); - // StringBuilder sb = new StringBuilder(); - // sb.Append(stopLimit.StopType).Append(" "); - // sb.Append(Utility.FormatCurrency(stopLimit.StopPrice)); - // sb.Append(" (").Append(percentOffsetFromLow > 0 ? "+" : "").Append(Utility.FormatPercent(percentOffsetFromLow)).Append(")"); - // Image image = TextMarkerImageGenerator.GenerateImage(sb.ToString(), FontFactor.FontSize); - // Coordinates coordinates = new Coordinates(latestPrice.Date.ToOADate() - offsets.Offset(OffsetDictionary.OffsetType.HorizontalOffset3PC), - // stopLimit.StopPrice - offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + coordinates = textMarkerManager.GetBestMarkerLocation( + coordinates.X, coordinates.Y, offsets, offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); - // coordinates = textMarkerManager.GetBestMarkerLocation(coordinates.X, coordinates.Y,offsets, offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); - - // ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image); - } - } + Plotter.Plot.Add.ImageMarker(coordinates, image); + } + // Draw external or internal stop limits + if (externalStopLimits != null) + { + for (int i = 0; i < externalStopLimits.Count; i++) + DrawTextMarker(externalStopLimits[i], i == externalStopLimits.Count - 1); + } + else if (internalStopLimits != null && zeroPrice != null) + { + for (int i = 0; i < internalStopLimits.Count; i++) + DrawTextMarker(internalStopLimits[i], i == internalStopLimits.Count - 1); + } + } + /// /// Generate Trade Points /// private void GenerateTradePoints() { - if (null == portfolioTradesLots || 0 == portfolioTradesLots.Count) return; + if (portfolioTradesLots == null || portfolioTradesLots.Count == 0) return; - // Here we add the image markers - Image tradePointMarker = TextMarkerImageGenerator.ToSPImage(ImageCache.GetInstance().GetImage(ImageCache.ImageType.YellowTriangleUp)); - for (int index = 0; index < portfolioTradesLots.Count; index++) - { - PortfolioTrade portfolioTrade = portfolioTradesLots[index]; - Coordinates coordinates = new Coordinates(portfolioTrade.TradeDate.ToOADate(), portfolioTrade.Price); - ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, tradePointMarker, SizeFactor.Normal); - } + // Add the image markers + Image tradePointMarker = TextMarkerImageGenerator.ToSPImage( + ImageCache.GetInstance().GetImage(ImageCache.ImageType.YellowTriangleUp)); - if (showTradeLabels) - { - // This adds the text markers - for (int index = 0; index < portfolioTradesLots.Count; index++) + foreach (var portfolioTrade in portfolioTradesLots) { - PortfolioTrade portfolioTrade = portfolioTradesLots[index]; - StringBuilder sb = new StringBuilder(); - sb.Append(portfolioTrade.BuySell.Equals("B") ? "Buy " : "Sell "); - sb.Append(Utility.FormatNumber(portfolioTrade.Shares)); - sb.Append("@"); - sb.Append(Utility.FormatCurrency(portfolioTrade.Price)); - Image image = TextMarkerImageGenerator.GenerateImage(sb.ToString(), FontFactor.FontSize); - Coordinates coordinates = new Coordinates(portfolioTrade.TradeDate.ToOADate(), - portfolioTrade.Price - offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + if (!IsVisible(portfolioTrade.TradeDate)) + continue; - coordinates = textMarkerManager.GetBestMarkerLocation(coordinates.X, coordinates.Y,offsets, offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + Coordinates coordinates = new Coordinates( + portfolioTrade.TradeDate.ToOADate(), + portfolioTrade.Price); - ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image); + ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, tradePointMarker, SizeFactor.Normal); + } + + if (!showTradeLabels) return; + + // Add the text markers + foreach (var portfolioTrade in portfolioTradesLots) + { + if (!IsVisible(portfolioTrade.TradeDate)) + continue; + + StringBuilder sb = new StringBuilder(); + sb.Append(portfolioTrade.BuySell.Equals("B") ? "Buy " : "Sell "); + sb.Append(Utility.FormatNumber(portfolioTrade.Shares)); + sb.Append("@"); + sb.Append(Utility.FormatCurrency(portfolioTrade.Price)); + + Image image = TextMarkerImageGenerator.GenerateImage(sb.ToString(), FontFactor.FontSize); + + Coordinates coordinates = new Coordinates( + portfolioTrade.TradeDate.ToOADate(), + portfolioTrade.Price - offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + + coordinates = textMarkerManager.GetBestMarkerLocation( + coordinates.X, + coordinates.Y, + offsets, + offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC)); + + ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image); } - } } + /// /// Generate Insider Transactions /// @@ -382,8 +392,8 @@ namespace PortfolioManager.Renderers // get the maximum date in the bollinger band series DateTime maxBollingerDate = (from BollingerBandElement bollingerBandElement in bollingerBands select bollingerBandElement.Date).Max(); // ensure that the insider transactions are clipped to the bollingerband max date. There are some items in insider transaction summaries (options dated in the future) that will throw the graphic out of proportion - InsiderTransactionSummaries disposedSummaries = new InsiderTransactionSummaries((from InsiderTransactionSummary insiderTransactionSummary in insiderTransactionSummaries where insiderTransactionSummary.NumberOfSharesAcquiredDisposed < 0 && insiderTransactionSummary.TransactionDate.Date <= maxBollingerDate select insiderTransactionSummary).ToList()); - InsiderTransactionSummaries acquiredSummaries = new InsiderTransactionSummaries((from InsiderTransactionSummary insiderTransactionSummary in insiderTransactionSummaries where insiderTransactionSummary.NumberOfSharesAcquiredDisposed > 0 && insiderTransactionSummary.TransactionDate.Date <= maxBollingerDate select insiderTransactionSummary).ToList()); + InsiderTransactionSummaries disposedSummaries = new InsiderTransactionSummaries((from InsiderTransactionSummary insiderTransactionSummary in insiderTransactionSummaries where insiderTransactionSummary.NumberOfSharesAcquiredDisposed < 0 && insiderTransactionSummary.TransactionDate.Date <= maxBollingerDate && IsVisible(insiderTransactionSummary.TransactionDate.Date) select insiderTransactionSummary).ToList()); + InsiderTransactionSummaries acquiredSummaries = new InsiderTransactionSummaries((from InsiderTransactionSummary insiderTransactionSummary in insiderTransactionSummaries where insiderTransactionSummary.NumberOfSharesAcquiredDisposed > 0 && insiderTransactionSummary.TransactionDate.Date <= maxBollingerDate && IsVisible(insiderTransactionSummary.TransactionDate.Date) select insiderTransactionSummary).ToList()); BinCollection disposedSummariesBin = BinHelper.CreateBins(new BinItems(disposedSummaries), 3); BinCollection acquiredSummariesBin = BinHelper.CreateBins(new BinItems(acquiredSummaries), 3); diff --git a/PortfolioManager/saveparams.config b/PortfolioManager/saveparams.config index 4ebf593..f2a7af2 100644 --- a/PortfolioManager/saveparams.config +++ b/PortfolioManager/saveparams.config @@ -1,4 +1,6 @@ Type,PortfolioManager.ViewModels.MGSHMomentumViewModel,PathFileName,C:\boneyard\marketdata\Sessions\MGSH20250331.TXT Type,PortfolioManager.ViewModels.MomentumViewModel,PathFileName,C:\boneyard\marketdata\Sessions\MG20180131.TXT Type,PortfolioManager.ViewModels.CMMomentumViewModel,PathFileName,C:\boneyard\marketdata\Sessions\CM20191031.TXT -Type,PortfolioManager.ViewModels.BollingerBandViewModel,SelectedSymbol,ALHC,SelectedWatchList,{ALL},SelectedDayCount,90,SyncTradeToBand,False,ShowTradeLabels,True,UseLeastSquaresFit,True,ShowInsiderTransactions,True +Type,PortfolioManager.ViewModels.BollingerBandViewModel,SelectedSymbol,ALHC,SelectedWatchList,Valuations,SelectedDayCount,90,SyncTradeToBand,True,ShowTradeLabels,True,UseLeastSquaresFit,True,ShowInsiderTransactions,True +Type,PortfolioManager.ViewModels.CMTrendViewModel,PathFileName,C:\boneyard\marketdata\bin\Debug\saferun\CMT20200817.TXT +Type,PortfolioManager.ViewModels.BollingerBandViewModel,SelectedSymbol,HWM,SelectedWatchList,Valuations,SelectedDayCount,90,SyncTradeToBand,False,ShowTradeLabels,True,UseLeastSquaresFit,True,ShowInsiderTransactions,True,StopHistoryCount,5,StopHistory_0,Symbol=HWM|StopPrice=176.71|Shares=0|StopType=Stop Quote|EffectiveDate=11/17/2025,StopHistory_1,Symbol=HWM|StopPrice=186.778498840332|Shares=0|StopType=Stop Quote|EffectiveDate=11/26/2025,StopHistory_2,Symbol=HWM|StopPrice=193.123001594543|Shares=0|StopType=Stop Quote|EffectiveDate=12/26/2025,StopHistory_3,Symbol=HWM|StopPrice=197.56385799408|Shares=0|StopType=Stop Quote|EffectiveDate=1/26/2026,StopHistory_4,Symbol=HWM|StopPrice=235.233001537323|Shares=0|StopType=Stop Quote|EffectiveDate=2/25/2026