2 Commits

Author SHA1 Message Date
7fef9b1050 Improve BollingerBandRendering 2026-03-14 09:39:09 -04:00
01c2516eaa Fix the symbol cache and image cache 2026-02-22 14:04:44 -05:00
4 changed files with 160 additions and 135 deletions

View File

@@ -57,6 +57,7 @@ namespace PortfolioManager.Cache
{
lock (thisLock)
{
DisposeBitmaps();
imageCache = new Dictionary<ImageType, Bitmap>();
}
}
@@ -66,22 +67,30 @@ namespace PortfolioManager.Cache
lock (thisLock)
{
if (null == imageCacheInstance) return;
List<Bitmap> bitmaps = imageCache.Values.ToList();
foreach (Bitmap bitmap in bitmaps)
{
bitmap.Dispose();
}
DisposeBitmaps();
imageCache = null;
imageCacheInstance = null;
}
}
public IImage GetImage(ImageCache.ImageType imageType)
private void DisposeBitmaps()
{
lock(this)
if(null == imageCache)return;
List<Bitmap> bitmaps = imageCache.Values.ToList();
foreach (Bitmap bitmap in bitmaps)
{
return imageCache[imageType];
bitmap.Dispose();
}
}
public IImage GetImage(ImageType imageType)
{
lock(thisLock)
{
if (imageCache == null)throw new ObjectDisposedException(nameof(ImageCache));
if (!imageCache.TryGetValue(imageType, out var bitmap))throw new KeyNotFoundException($"Image {imageType} not found in cache.");
return bitmap;
}
}
}
}

View File

@@ -10,7 +10,8 @@ namespace PortfolioManager.Cache
public class SymbolCache : IDisposable
{
private List<String> symbolCache=new List<String>();
private Object thisLock=new Object();
private readonly Object thisLock=new Object();
private readonly Object fetchLock = new Object();
private Thread cacheMonitorThread=null;
private volatile bool threadRun=true;
private int cacheRefreshAfter=60000; // Invalidate cache after
@@ -60,12 +61,15 @@ namespace PortfolioManager.Cache
{
if(symbolCache.Count>0)return new List<string>(symbolCache);
}
List<String> symbols = PricingDA.GetSymbols();
lock(thisLock)
lock(fetchLock)
{
if(symbolCache.Count>0)return new List<string>(symbolCache);
symbolCache=new List<String>(symbols);
return new List<string>(symbols);
List<String> symbols = PricingDA.GetSymbols();
lock(thisLock)
{
if(symbolCache.Count>0)return new List<string>(symbolCache);
symbolCache=new List<String>(symbols);
return new List<string>(symbols);
}
}
}
@@ -81,7 +85,7 @@ namespace PortfolioManager.Cache
if(quantums>cacheRefreshAfter)
{
quantums=0;
List<String> symbols = PricingDA.GetSymbols();
List<String> symbols = PricingDA.GetSymbols();
lock(thisLock)
{
symbolCache=new List<string>(symbols);

View File

@@ -56,6 +56,13 @@ namespace PortfolioManager.Renderers
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)
{
if (eventArgs.PropertyName.Equals("ShowLegend"))
@@ -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<PortfolioTrade> 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();
@@ -236,98 +250,78 @@ namespace PortfolioManager.Renderers
imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image);
}
/// <summary>
/// Generate Stop Limits
/// </summary>
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);
}
}
else
{
if (null == zeroPrice) return;
if (null == internalStopLimits || null == zeroPrice) return;
for (int index = 0; index < internalStopLimits.Count; index++)
{
StopLimit limit = internalStopLimits[index];
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));
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);
}
// double percentOffsetFromLow = ((latestPrice.Low - stopLimit.StopPrice) / stopLimit.StopPrice);
if (!showTradeLabels) return;
// 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));
Price latestPrice = prices[0];
// coordinates = textMarkerManager.GetBestMarkerLocation(coordinates.X, coordinates.Y,offsets, offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC));
// Helper to draw text markers
void DrawTextMarker(StopLimit limit, bool isLast)
{
if (!IsVisible(limit.EffectiveDate)) return;
// ImageMarker imageMarker = Plotter.Plot.Add.ImageMarker(coordinates, image);
}
StringBuilder sb = new StringBuilder();
sb.Append(limit.StopType).Append(" ");
sb.Append(Utility.FormatCurrency(limit.StopPrice));
if (isLast)
{
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));
coordinates = textMarkerManager.GetBestMarkerLocation(
coordinates.X, coordinates.Y, offsets, offsets.Offset(OffsetDictionary.OffsetType.VerticalOffset6P5PC));
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);
}
}
/// <summary>
@@ -335,39 +329,55 @@ namespace PortfolioManager.Renderers
/// </summary>
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);
}
}
}
/// <summary>
/// Generate Insider Transactions
/// </summary>
@@ -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<InsiderTransactionSummary> disposedSummariesBin = BinHelper<InsiderTransactionSummary>.CreateBins(new BinItems<InsiderTransactionSummary>(disposedSummaries), 3);
BinCollection<InsiderTransactionSummary> acquiredSummariesBin = BinHelper<InsiderTransactionSummary>.CreateBins(new BinItems<InsiderTransactionSummary>(acquiredSummaries), 3);

View File

@@ -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