From b7ef1c6cb391c2925d248a818a6eb0b849117464 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 13 Jun 2025 18:59:01 -0400 Subject: [PATCH] Continuing work on BollingerBandViewModel --- PortfolioManager/Cache/SymbolCache.cs | 83 ++++ .../CompositeDataSource.cs | 9 +- .../ViewModels/BollingerBandViewModel.cs | 394 +++++++++++++++++- .../Views/BollingerBandView.axaml | 32 +- 4 files changed, 493 insertions(+), 25 deletions(-) create mode 100644 PortfolioManager/Cache/SymbolCache.cs diff --git a/PortfolioManager/Cache/SymbolCache.cs b/PortfolioManager/Cache/SymbolCache.cs new file mode 100644 index 0000000..878f961 --- /dev/null +++ b/PortfolioManager/Cache/SymbolCache.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using MarketData; +using MarketData.Utils; +using MarketData.DataAccess; + +namespace PortfolioManager.Cache +{ + public class SymbolCache : IDisposable + { + private List symbolCache=new List(); + private Object thisLock=new Object(); + private Thread cacheMonitorThread=null; + private volatile bool threadRun=true; + private int cacheRefreshAfter=60000; // Invalidate cache after + private static SymbolCache symbolCacheInstance=null; + + private SymbolCache() + { + cacheMonitorThread=new Thread(new ThreadStart(ThreadProc)); + cacheMonitorThread.Start(); + } + public static SymbolCache GetInstance() + { + lock(typeof(SymbolCache)) + { + if(null==symbolCacheInstance) symbolCacheInstance=new SymbolCache(); + return symbolCacheInstance; + } + } + public void Clear() + { + lock(thisLock) + { + symbolCache=new List(); + } + } + public void Dispose() + { + lock(thisLock) + { + if(null==symbolCacheInstance || false==threadRun)return; + threadRun=false; + if(null!=cacheMonitorThread) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[SymbolCache:Dispose]Thread state is {0}. Joining main thread...",Utility.ThreadStateToString(cacheMonitorThread))); + cacheMonitorThread.Join(5000); + cacheMonitorThread=null; + MDTrace.WriteLine(LogLevel.DEBUG,"[SymbolCache:Dispose] End."); + } + symbolCacheInstance=null; + } + } + + public List GetSymbols() + { + lock(this) + { + if(0==symbolCache.Count)symbolCache=PricingDA.GetSymbols(); + return symbolCache; + } + } + private void ThreadProc() + { + int quantums=0; + int quantumInterval=1000; + while(threadRun) + { + Thread.Sleep(quantumInterval); + quantums+=quantumInterval; + if(quantums>cacheRefreshAfter) + { + quantums=0; + lock(thisLock) + { + symbolCache=PricingDA.GetSymbols(); + } + } + } + } + } +} diff --git a/PortfolioManager/DataSeriesViewModels/CompositeDataSource.cs b/PortfolioManager/DataSeriesViewModels/CompositeDataSource.cs index 514fdd2..4f027e1 100644 --- a/PortfolioManager/DataSeriesViewModels/CompositeDataSource.cs +++ b/PortfolioManager/DataSeriesViewModels/CompositeDataSource.cs @@ -1,11 +1,10 @@ -using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; using Eremex.AvaloniaUI.Charts; namespace PortfolioManager.DataSeriesViewModels { - public partial class CompositeDataSource : ObservableObject - { - [ObservableProperty] ISeriesDataAdapter dataAdapter; - } + public partial class CompositeDataSource : ObservableObject + { + [ObservableProperty] ISeriesDataAdapter dataAdapter; + } } \ No newline at end of file diff --git a/PortfolioManager/ViewModels/BollingerBandViewModel.cs b/PortfolioManager/ViewModels/BollingerBandViewModel.cs index b4c759b..3a978e0 100644 --- a/PortfolioManager/ViewModels/BollingerBandViewModel.cs +++ b/PortfolioManager/ViewModels/BollingerBandViewModel.cs @@ -2,38 +2,89 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Media; -using Avalonia.Platform.Storage; -using CommunityToolkit.Mvvm.Input; -using Eremex.AvaloniaUI.Controls; -using MarketData; +using DynamicData; +using Eremex.AvaloniaUI.Charts; using MarketData.DataAccess; using MarketData.Generator; -using MarketData.Generator.CMMomentum; -using MarketData.Generator.Interface; -using MarketData.Generator.Model; using MarketData.MarketDataModel; +using MarketData.Numerical; using MarketData.Utils; +using PortfolioManager.Cache; using PortfolioManager.DataSeriesViewModels; -using PortfolioManager.Dialogs; using PortfolioManager.Models; using PortfolioManager.UIUtils; -using Position = MarketData.Generator.CMMomentum.Position; namespace PortfolioManager.ViewModels { public partial class BollingerBandViewModel : WorkspaceViewModel { private bool isBusy = false; - + private List watchListNames; + private String selectedWatchList; + private List dayCounts = new int[] { 60, 90, 180, 360, 720, 1440, 3600 }.ToList(); + private int selectedDayCount = 90; + private ObservableCollection symbols = new ObservableCollection(); + private String selectedSymbol = default; + + private InsiderTransactionSummaries insiderTransactionSummaries = null; + private Price zeroPrice = null; + private PortfolioTrades portfolioTrades; + private PortfolioTrades portfolioTradesLots; + private StopLimit stopLimit; // This is the stop limit that is looked up in the database and displayed (if there is one) + private Prices prices = null; + + private bool syncTradeToBand = true; + private String companyName = default; + + + private CompositeDataSource compositeDataSourceZeroPoint = null; + private CompositeDataSource compositeDataSourceStopLimit = null; + + private CompositeDataSource compositeDataSourceInsiderTransactionPointDisposedSmall = null; + private CompositeDataSource compositeDataSourceInsiderTransactionPointDisposedMedium = null; + private CompositeDataSource compositeDataSourceInsiderTransactionPointDisposedLarge = null; + private CompositeDataSource compositeDataSourceInsiderTransactionPointAcquiredSmall = null; + private CompositeDataSource compositeDataSourceInsiderTransactionPointAcquiredMedium = null; + private CompositeDataSource compositeDataSourceInsiderTransactionPointAcquiredLarge = null; + + private CompositeDataSource compositeDataSourceK = null; + private CompositeDataSource compositeDataSourceKL1 = null; + private CompositeDataSource compositeDataSourceL = null; + private CompositeDataSource compositeDataSourceLP1 = null; + private CompositeDataSource compositeDataSourceHigh = null; + private CompositeDataSource compositeDataSourceLow = null; + private CompositeDataSource compositeDataSourceClose = null; + private CompositeDataSource compositeDataSourceSMAN = null; + private CompositeDataSource compositeDataSourceVolume = null; + private CompositeDataSource compositeDataSourceLeastSquares = null; + private CompositeDataSource compositeDataSourceTradePoints = null; + + private BollingerBands bollingerBands; + public BollingerBandViewModel() { + PropertyChanged += OnViewModelPropertyChanged; DisplayName = "BollingerBand View"; + Initialize(); + } + + private void Initialize() + { + Task workerTask = Task.Factory.StartNew(() => + { + watchListNames = WatchListDA.GetWatchLists(); + watchListNames.Insert(0, UIConstants.CONST_ALL); + selectedWatchList = watchListNames.Find(x => x.Equals("Valuations")); + symbols.AddRange(WatchListDA.GetWatchList(selectedWatchList)); + }); + workerTask.ContinueWith((continuation) => + { + base.OnPropertyChanged("Symbols"); + base.OnPropertyChanged("WatchListNames"); + base.OnPropertyChanged("SelectedWatchList"); + }); } public bool IsBusy @@ -49,7 +100,92 @@ namespace PortfolioManager.ViewModels } } -// ******************************************************************* P E R S I S T E N C E *************************************************** + public override String Title + { + get + { + return DisplayName; + } + } + + public override String DisplayName + { + get + { + return "BollingerBand Model"; + // if (null == pathFileName) return "MGSHMomentum Model"; + // String pureFileName = Utility.GetFileNameNoExtension(pathFileName); + // return "MGSHMomentum Model (" + pureFileName + ")"; + } + } + + public ObservableCollection Symbols + { + get + { + return symbols; + } + } + + public String SelectedSymbol + { + get + { + return selectedSymbol; + } + set + { + selectedSymbol = value; + base.OnPropertyChanged("SelectedSymbol"); + } + } + + public int SelectedDayCount + { + get + { + return selectedDayCount; + } + set + { + selectedDayCount = value; + base.OnPropertyChanged("SelectedDayCount"); + } + } + + public List DayCounts + { + get + { + return dayCounts; + } + } + + + // ****************************************************** P R O P E R T I E S ************************************************ + + public List WatchListNames + { + get + { + return watchListNames; + } + } + + public String SelectedWatchList + { + get + { + return selectedWatchList; + } + set + { + selectedWatchList = value; + base.OnPropertyChanged("SelectedWatchList"); + } + } + + // ******************************************************************* P E R S I S T E N C E *************************************************** public override bool CanPersist() { @@ -58,12 +194,234 @@ namespace PortfolioManager.ViewModels public override SaveParameters GetSaveParameters() { - return null; + return null; } public override void SetSaveParameters(SaveParameters saveParameters) { - return; + return; + } + + // ************************************************************************************************************************************* + private void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs eventArgs) + { + if (eventArgs.PropertyName.Equals("SelectedSymbol")) + { + InitializeDataSources(); + insiderTransactionSummaries = null; + zeroPrice = null; + prices = null; + portfolioTrades = null; + portfolioTradesLots = null; + stopLimit = null; + } + + if (eventArgs.PropertyName.Equals("SyncTradeToBand")|| + eventArgs.PropertyName.Equals("ShowTradeLabels")|| + eventArgs.PropertyName.Equals("SelectedSymbol")|| + eventArgs.PropertyName.Equals("ShowRiskFree")|| + eventArgs.PropertyName.Equals("LeastSquaresFit")|| + (eventArgs.PropertyName.Equals("SelectedDayCount") && null!=selectedSymbol)) + { + IsBusy=true; + Task workerTask=Task.Factory.StartNew(()=> + { + base.DisplayName="Bollinger("+selectedSymbol+")"; + base.OnPropertyChanged("DisplayName"); + +// DEBUG + stopLimit=PortfolioDA.GetStopLimit(selectedSymbol); + portfolioTrades = PortfolioDA.GetTradesSymbol(selectedSymbol); + portfolioTradesLots=LotAggregator.CombineLots(portfolioTrades); + if (null != portfolioTrades && 0 != portfolioTrades.Count) + { + DateGenerator dateGenerator = new DateGenerator(); + DateTime earliestTrade = portfolioTrades[0].TradeDate; + earliestTrade = earliestTrade.AddDays(-30); + 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); + insiderTransactionSummaries=InsiderTransactionDA.GetInsiderTransactionSummaries(selectedSymbol,earliestInsiderTransactionDate); + +// calculate the break even price on the open trades for this symbol + PortfolioTrades openTrades=portfolioTrades.GetOpenTrades(); + DateTime latestPricingDate = PricingDA.GetLatestDate(selectedSymbol); + Price latestPrice = PricingDA.GetPrice(selectedSymbol, latestPricingDate); + zeroPrice=ParityGenerator.GenerateGainLossValue(openTrades,latestPrice); + + if (!syncTradeToBand) + { + DateTime earliestPricingDate = prices[prices.Count - 1].Date; + earliestPricingDate = earliestPricingDate.AddDays(30); + IEnumerable tradesInRange = (from portfolioTrade in portfolioTradesLots where portfolioTrade.TradeDate >= earliestPricingDate select portfolioTrade); + portfolioTrades = new PortfolioTrades(); + foreach (PortfolioTrade portfolioTrade in tradesInRange) portfolioTrades.Add(portfolioTrade); + portfolioTradesLots = portfolioTrades; + } + } + else + { + prices = PricingDA.GetPrices(selectedSymbol, selectedDayCount); + if (null != prices && 0 != prices.Count) + { + DateGenerator dateGenerator = new DateGenerator(); + DateTime earliestInsiderTransactionDate = dateGenerator.GenerateFutureBusinessDate(prices[prices.Count - 1].Date, 30); + insiderTransactionSummaries = InsiderTransactionDA.GetInsiderTransactionSummaries(selectedSymbol, earliestInsiderTransactionDate); + } + } + companyName = PricingDA.GetNameForSymbol(selectedSymbol); + bollingerBands = BollingerBandGenerator.GenerateBollingerBands(prices); + CreateCompositeDataSources(); + }); + workerTask.ContinueWith((continuation)=> + { + IsBusy = false; + base.OnPropertyChanged("K"); + base.OnPropertyChanged("KL1"); + base.OnPropertyChanged("L"); + base.OnPropertyChanged("LP1"); + base.OnPropertyChanged("High"); + base.OnPropertyChanged("Low"); + base.OnPropertyChanged("Close"); + base.OnPropertyChanged("SMAN"); + base.OnPropertyChanged("Volume"); + base.OnPropertyChanged("LeastSquares"); + base.OnPropertyChanged("Title"); + base.OnPropertyChanged("TradePoints"); + base.OnPropertyChanged("Markers"); + base.OnPropertyChanged("ZeroPoint"); + base.OnPropertyChanged("ZeroPointMarkers"); + base.OnPropertyChanged("StopLimit"); + base.OnPropertyChanged("StopLimitMarkers"); + base.OnPropertyChanged("RiskFreeRatePoint"); + base.OnPropertyChanged("RiskFreeRatePointMarkers"); + + base.OnPropertyChanged("InsiderTransactionPointDisposedSmall"); + base.OnPropertyChanged("InsiderTransactionPointMarkersDisposedSmall"); + base.OnPropertyChanged("InsiderTransactionPointDisposedMedium"); + base.OnPropertyChanged("InsiderTransactionPointMarkersDisposedMedium"); + base.OnPropertyChanged("InsiderTransactionPointDisposedLarge"); + base.OnPropertyChanged("InsiderTransactionPointMarkersDisposedLarge"); + + base.OnPropertyChanged("InsiderTransactionPointAcquiredSmall"); + base.OnPropertyChanged("InsiderTransactionPointMarkersAcquiredSmall"); + base.OnPropertyChanged("InsiderTransactionPointAcquiredMedium"); + base.OnPropertyChanged("InsiderTransactionPointMarkersAcquiredMedium"); + base.OnPropertyChanged("InsiderTransactionPointAcquiredLarge"); + base.OnPropertyChanged("InsiderTransactionPointMarkersAcquiredLarge"); + }); + } + else if (eventArgs.PropertyName.Equals("SelectedWatchList")) + { + IsBusy = true; + Task workerTask = Task.Factory.StartNew(() => + { + if (selectedWatchList.Equals(Constants.CONST_ALL)) + { + symbols.Clear(); + symbols.AddRange(SymbolCache.GetInstance().GetSymbols()); + } + else + { + symbols.Clear(); + symbols.AddRange(WatchListDA.GetWatchList(selectedWatchList)); + } + }); + workerTask.ContinueWith((continuation) => + { + IsBusy = false; + base.OnPropertyChanged("Symbols"); + }); + } + + + + } + + public void CreateCompositeDataSources() + { + if(null==prices || 0==prices.Count)return; + double minClose=(from Price price in prices select price.Close).Min(); + + +// 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()); + + BinCollection disposedSummariesBin=BinHelper.CreateBins(new BinItems(disposedSummaries),3); + BinCollection acquiredSummariesBin=BinHelper.CreateBins(new BinItems(acquiredSummaries),3); + + + // compositeDataSourceZeroPoint = GainLossModel.Price(zeroPrice); +/* + if(null!=stopLimits) + { + compositeDataSourceStopLimit=StopLimitCompositeModel.CreateCompositeDataSource(stopLimits); + } + else if(null!=stopLimit && null!=zeroPrice) + { + compositeDataSourceStopLimit=GainLossModel.CreateCompositeDataSource(zeroPrice.Date,stopLimit.StopPrice); + } + + compositeDataSourceInsiderTransactionPointDisposedSmall=InsiderTransactionModel.InsiderTransactionSummaries(new InsiderTransactionSummaries(disposedSummariesBin[2]),minClose); + compositeDataSourceInsiderTransactionPointDisposedMedium=InsiderTransactionModel.InsiderTransactionSummaries(new InsiderTransactionSummaries(disposedSummariesBin[1]),minClose); + compositeDataSourceInsiderTransactionPointDisposedLarge=InsiderTransactionModel.InsiderTransactionSummaries(new InsiderTransactionSummaries(disposedSummariesBin[0]),minClose); + compositeDataSourceInsiderTransactionPointAcquiredSmall=InsiderTransactionModel.InsiderTransactionSummaries(new InsiderTransactionSummaries(acquiredSummariesBin[0]),minClose); + compositeDataSourceInsiderTransactionPointAcquiredMedium=InsiderTransactionModel.InsiderTransactionSummaries(new InsiderTransactionSummaries(acquiredSummariesBin[1]),minClose); + compositeDataSourceInsiderTransactionPointAcquiredLarge=InsiderTransactionModel.InsiderTransactionSummaries(new InsiderTransactionSummaries(acquiredSummariesBin[2]),minClose); + + compositeDataSourceK =BollingerBandModel.K(bollingerBands); + compositeDataSourceKL1 =BollingerBandModel.KL1(bollingerBands); + compositeDataSourceL =BollingerBandModel.L(bollingerBands); + compositeDataSourceLP1 =BollingerBandModel.LP1(bollingerBands); + compositeDataSourceHigh =BollingerBandModel.High(bollingerBands); + compositeDataSourceLow =BollingerBandModel.Low(bollingerBands); + compositeDataSourceClose =BollingerBandModel.Close(bollingerBands); + compositeDataSourceSMAN =BollingerBandModel.SMAN(bollingerBands); + compositeDataSourceVolume =BollingerBandModel.Volume(bollingerBands); + + compositeDataSourceLeastSquares = BollingerBandModel.LeastSquares(bollingerBands); + + compositeDataSourceTradePoints = PortfolioTradeModel.PortfolioTrades(portfolioTradesLots); +*/ + } + + private void InitializeDataSources() + { + compositeDataSourceStopLimit = Empty(); + compositeDataSourceZeroPoint = Empty(); + + compositeDataSourceInsiderTransactionPointDisposedSmall = Empty(); + compositeDataSourceInsiderTransactionPointDisposedMedium = Empty(); + compositeDataSourceInsiderTransactionPointDisposedLarge = Empty(); + + compositeDataSourceInsiderTransactionPointAcquiredSmall = Empty(); + compositeDataSourceInsiderTransactionPointAcquiredMedium = Empty(); + compositeDataSourceInsiderTransactionPointAcquiredLarge = Empty(); + + compositeDataSourceK = Empty(); + compositeDataSourceKL1 = Empty(); + compositeDataSourceL = Empty(); + compositeDataSourceLP1 = Empty(); + compositeDataSourceHigh = Empty(); + compositeDataSourceLow = Empty(); + compositeDataSourceClose = Empty(); + compositeDataSourceSMAN = Empty(); + compositeDataSourceVolume = Empty(); + compositeDataSourceLeastSquares = Empty(); + compositeDataSourceTradePoints = Empty(); + } + + private static CompositeDataSource Empty() + { + return new CompositeDataSource() + { + DataAdapter = new SortedDateTimeDataAdapter() + }; } } -} \ No newline at end of file +} diff --git a/PortfolioManager/Views/BollingerBandView.axaml b/PortfolioManager/Views/BollingerBandView.axaml index 4c4fd95..9fcc367 100644 --- a/PortfolioManager/Views/BollingerBandView.axaml +++ b/PortfolioManager/Views/BollingerBandView.axaml @@ -44,7 +44,29 @@ - +