diff --git a/MarketData/MarketDataLib/Cache/GBPriceCache.cs b/MarketData/MarketDataLib/Cache/GBPriceCache.cs index bb3b03b..bcb2e29 100755 --- a/MarketData/MarketDataLib/Cache/GBPriceCache.cs +++ b/MarketData/MarketDataLib/Cache/GBPriceCache.cs @@ -1,259 +1,355 @@ using System; using System.Collections.Generic; -using System.Text; -using MarketData.MarketDataModel; -using MarketData.DataAccess; -using MarketData.Utils; using System.Linq; +using System.Threading; +using MarketData.MarketDataModel; +using MarketData.Utils; using MarketData.Helper; using MarketData.Numerical; -using System.Threading; +using MarketData.DataAccess; - -// This cache is mainly used by the models. It is a short lived cache that gets cleared out every 2 minutes. -// This cache will attempt to load a price from the database if it is found in the cache. namespace MarketData.Cache { - public class GBPriceCache + public interface IPricingDataAccess { - private Thread cacheMonitorThread=null; - private volatile bool threadRun=true; - private Object thisLock=new Object(); - private Dictionary priceCache=new Dictionary(); // the main cache - private Dictionary realTimePriceCache=new Dictionary(); // short lived cache of realtime prices gets cleared out every cacheRefreshAfter(ms) - private Dictionary nullCache=new Dictionary(); - private DateGenerator dateGenerator=new DateGenerator(); - private static GBPriceCache priceCacheInstance=null; - private int cacheRefreshAfter=120000; // the cache will be cleaned up after 2 minutes + Price GetPrice(string symbol, DateTime date); + Prices GetPrices(string symbol, DateTime maxDate, DateTime minDate); + DateTime GetLatestDateOnOrBefore(string symbol, DateTime date); + } + + internal class RealPricingDA : IPricingDataAccess + { + public Price GetPrice(string symbol, DateTime date) => PricingDA.GetPrice(symbol, date); + public Prices GetPrices(string symbol, DateTime maxDate, DateTime minDate) => PricingDA.GetPrices(symbol, maxDate, minDate); + public DateTime GetLatestDateOnOrBefore(string symbol, DateTime date) => PricingDA.GetLatestDateOnOrBefore(symbol, date); + } + + internal class CacheSnapshot + { + public Dictionary PriceCache { get; } + public Dictionary RealTimePriceCache { get; } + public Dictionary NullCache { get; } + + public CacheSnapshot( + Dictionary priceCache, + Dictionary realTimePriceCache, + Dictionary nullCache) + { + PriceCache = priceCache; + RealTimePriceCache = realTimePriceCache; + NullCache = nullCache; + } + } + + public class GBPriceCache : IDisposable + { + private Thread cacheMonitorThread = null; + private volatile bool threadRun = true; + private Object thisLock = new Object(); + + private CacheSnapshot snapshot; + private DateGenerator dateGenerator = new DateGenerator(); + private static GBPriceCache priceCacheInstance = null; + private int cacheRefreshAfter = 120000; // 2 minutes + + private SemaphoreSlim fetchSemaphore = new SemaphoreSlim(8); // max 8 concurrent DB fetches + + public IPricingDataAccess PricingDataAccess { get; set; } = new RealPricingDA(); protected GBPriceCache() { - cacheMonitorThread=new Thread(new ThreadStart(ThreadProc)); + snapshot = new CacheSnapshot( + new Dictionary(), + new Dictionary(), + new Dictionary()); + + cacheMonitorThread = new Thread(new ThreadStart(ThreadProc)); cacheMonitorThread.Start(); } + public static GBPriceCache GetInstance() { - lock(typeof(GBPriceCache)) + lock (typeof(GBPriceCache)) { - if(null==priceCacheInstance) + if (priceCacheInstance == null) { - priceCacheInstance=new GBPriceCache(); + priceCacheInstance = new GBPriceCache(); } return priceCacheInstance; } } + public void Clear() { - lock(thisLock) + lock (thisLock) { - priceCache=new Dictionary(); - realTimePriceCache=new Dictionary(); - nullCache=new Dictionary(); + snapshot = new CacheSnapshot( + new Dictionary(), + new Dictionary(), + new Dictionary()); } } + public void Dispose() { - lock(thisLock) + lock (thisLock) { - if(null==priceCacheInstance || false==threadRun)return; - threadRun=false; - if(null!=cacheMonitorThread) + if (priceCacheInstance == null || !threadRun) return; + threadRun = false; + + if (cacheMonitorThread != null) { - MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[GBPriceCache:Dispose]Thread state is '{0}'. Joining main thread...",Utility.ThreadStateToString(cacheMonitorThread))); + MDTrace.WriteLine(LogLevel.DEBUG, "[GBPriceCache:Dispose] Joining monitor thread..."); cacheMonitorThread.Join(5000); - this.cacheMonitorThread=null; + cacheMonitorThread = null; } - MDTrace.WriteLine(LogLevel.DEBUG,"[GBPriceCache:Dispose] End."); - priceCacheInstance=null; + + priceCacheInstance = null; } } - public void ClearCacheOnOrBefore(DateTime onOrBeforeDate,bool collect=false) + + public void ClearCacheOnOrBefore(DateTime onOrBeforeDate, bool collect = false) { - lock(thisLock) + lock (thisLock) { - MDTrace.WriteLine(LogLevel.DEBUG,"Clearing GBPriceCache cache."); - List symbols=new List(priceCache.Keys); - foreach(String symbol in symbols) + Dictionary newPriceCache = new Dictionary(); + foreach (KeyValuePair entry in snapshot.PriceCache) { - PricesByDate pricesByDate=priceCache[symbol]; - List symbolDates=new List(pricesByDate.Keys); - foreach(DateTime symbolDate in symbolDates) + String symbol = entry.Key; + PricesByDate filteredPrices = new PricesByDate(); + PricesByDate existingPrices = entry.Value; + + foreach (KeyValuePair kv in existingPrices) { - if(symbolDate= onOrBeforeDate) + { + filteredPrices.Add(kv.Key, kv.Value); + } + } + + if (filteredPrices.Count > 0) + { + newPriceCache.Add(symbol, filteredPrices); } } - MDTrace.WriteLine(LogLevel.DEBUG,"Calling garbage collector..."); - if(collect) GC.Collect(); + + UpdateSnapshot( + newPriceCache, + snapshot.RealTimePriceCache, + snapshot.NullCache); + + if (collect) GC.Collect(); } } - public Price GetPriceOrLatestAvailable(String symbol,DateTime date) + + public Price GetPriceOrLatestAvailable(String symbol, DateTime date) { - lock(thisLock) + Price price = GetPrice(symbol, date); + if (price != null) return price; + + DateTime latestPricingDate = PricingDataAccess.GetLatestDateOnOrBefore(symbol, date); + price = GetPrice(symbol, latestPricingDate); + if (price != null) return price; + + fetchSemaphore.Wait(); + try { - Price price=GetPrice(symbol,date); - if(null!=price) return price; - DateTime latestPricingDate=PricingDA.GetLatestDateOnOrBefore(symbol,date); - price=GetPrice(symbol,latestPricingDate); - if(null!=price) return price; - price=PricingDA.GetPrice(symbol,latestPricingDate); - if(null!=price) AddPrice(price); - return price; + price = PricingDataAccess.GetPrice(symbol, latestPricingDate); } + finally + { + fetchSemaphore.Release(); + } + + if (price != null) AddPrice(price); + return price; } + public Price GetRealtimePrice(String symbol) { - if(realTimePriceCache.ContainsKey(symbol)) return realTimePriceCache[symbol]; - Price price=MarketDataHelper.GetLatestPrice(symbol); - if(null!=price) + if (snapshot.RealTimePriceCache.ContainsKey(symbol)) { - realTimePriceCache.Add(symbol,price); + return snapshot.RealTimePriceCache[symbol]; + } + + Price price = MarketDataHelper.GetLatestPrice(symbol); + if (price != null) + { + Dictionary newRealtime = new Dictionary(snapshot.RealTimePriceCache); + newRealtime.Add(symbol, price); + UpdateSnapshot(snapshot.PriceCache, newRealtime, snapshot.NullCache); } return price; } - public Price GetPrice(String symbol,DateTime date) + + public Price GetPrice(String symbol, DateTime date) { - lock(thisLock) + date = date.Date; + if (!ContainsPrice(symbol, date)) { - date=date.Date; - if(!ContainsPrice(symbol,date)) + String key = symbol + Utility.DateTimeToStringMMHDDHYYYY(date); + if (snapshot.NullCache.ContainsKey(key)) { - String key=symbol+Utility.DateTimeToStringMMHDDHYYYY(date); - if(nullCache.ContainsKey(key)) return null; - Price price=PricingDA.GetPrice(symbol,date); - if(null==price) - { - nullCache.Add(key,true); - return price; - } - AddPrice(price); + return null; } - if(!priceCache.ContainsKey(symbol)) return null; - PricesByDate pricesByDate=priceCache[symbol]; - if(!pricesByDate.ContainsKey(date.Date)) return null; - return pricesByDate[date]; + + fetchSemaphore.Wait(); + Price price; + try + { + price = PricingDataAccess.GetPrice(symbol, date); + } + finally + { + fetchSemaphore.Release(); + } + + if (price == null) + { + Dictionary newNullCache = new Dictionary(snapshot.NullCache); + newNullCache.Add(key, true); + UpdateSnapshot(snapshot.PriceCache, snapshot.RealTimePriceCache, newNullCache); + return null; + } + + AddPrice(price); } + + if (!snapshot.PriceCache.ContainsKey(symbol)) return null; + PricesByDate pricesByDate = snapshot.PriceCache[symbol]; + if (!pricesByDate.ContainsKey(date)) return null; + return pricesByDate[date]; } public Prices GetPrices(String symbol, DateTime earlierDate, DateTime laterDate) { - DateGenerator dateGenerator = new DateGenerator(); + DateGenerator localDateGenerator = new DateGenerator(); - if(laterDate datesList = dateGenerator.GenerateHistoricalDates(earlierDate, laterDate); + + List datesList = localDateGenerator.GenerateHistoricalDates(earlierDate, laterDate); datesList = datesList.Where(x => x >= earlierDate).ToList(); + return GetPrices(symbol, laterDate, datesList.Count); } -// The most recent price is returned at the lowest index - public Prices GetPrices(String symbol,DateTime startDate,int dayCount) + public Prices GetPrices(String symbol, DateTime startDate, int dayCount) { - lock(thisLock) + List historicalDates = dateGenerator.GenerateHistoricalDates(startDate, dayCount + 60); + + List missingDates = new List(); + foreach (DateTime historicalDate in historicalDates) { - List historicalDates=dateGenerator.GenerateHistoricalDates(startDate,dayCount+60); - Prices prices=null; - List missingDates=null; - foreach(DateTime historicalDate in historicalDates) + if (!ContainsPrice(symbol, historicalDate)) { - if(!ContainsPrice(symbol,historicalDate)) + String key = symbol + Utility.DateTimeToStringMMHDDHYYYY(historicalDate); + if (!snapshot.NullCache.ContainsKey(key)) { - String key=symbol+Utility.DateTimeToStringMMHDDHYYYY(historicalDate); - if(nullCache.ContainsKey(key)) continue; - if(null==missingDates)missingDates=new List(); missingDates.Add(historicalDate); } } - if(null!=missingDates) - { - DateTime minDate=(from DateTime date in missingDates select date).Min(); - DateTime maxDate=(from DateTime date in missingDates select date).Max(); - prices=PricingDA.GetPrices(symbol,maxDate,minDate); - foreach(Price price in prices) AddPrice(price); - prices=new Prices(); - foreach(DateTime historicalDate in historicalDates) - { - if(!ContainsPrice(symbol,historicalDate)) - { - String key=symbol+Utility.DateTimeToStringMMHDDHYYYY(historicalDate); - if(!nullCache.ContainsKey(key)) nullCache.Add(key,true); - } - else - { - if(!priceCache.ContainsKey(symbol)) continue; - PricesByDate pricesByDate=priceCache[symbol]; - if(!pricesByDate.ContainsKey(historicalDate.Date)) continue; - prices.Add(pricesByDate[historicalDate]); - } - } - } - else - { - prices=new Prices(); - foreach(DateTime historicalDate in historicalDates) - { - if(!priceCache.ContainsKey(symbol)) continue; - if(!priceCache[symbol].ContainsKey(historicalDate.Date)) - { - continue; - } - prices.Add((priceCache[symbol])[historicalDate]); - } - } - return new Prices(prices.OrderByDescending(x => x.Date).ToList().Take(dayCount).ToList()); } + + if (missingDates.Count > 0) + { + DateTime minDate = missingDates.Min(); + DateTime maxDate = missingDates.Max(); + + fetchSemaphore.Wait(); + Prices loadedPrices; + try + { + loadedPrices = PricingDataAccess.GetPrices(symbol, maxDate, minDate); + } + finally + { + fetchSemaphore.Release(); + } + + foreach (Price price in loadedPrices) + { + AddPrice(price); + } + } + + Prices prices = new Prices(); + foreach (DateTime historicalDate in historicalDates) + { + if (!snapshot.PriceCache.ContainsKey(symbol)) continue; + PricesByDate pricesByDate = snapshot.PriceCache[symbol]; + if (!pricesByDate.ContainsKey(historicalDate)) continue; + prices.Add(pricesByDate[historicalDate]); + } + + List ordered = prices.OrderByDescending(x => x.Date).ToList(); + return new Prices(ordered.Take(dayCount).ToList()); } + private void AddPrice(Price price) { - lock(thisLock) + if (price == null) return; + + lock (thisLock) { - if(null==price) return; - PricesByDate pricesByDate=null; - if(!priceCache.ContainsKey(price.Symbol)) + PricesByDate pricesByDate; + if (!snapshot.PriceCache.ContainsKey(price.Symbol)) { - pricesByDate=new PricesByDate(); - pricesByDate.Add(price.Date,price); - priceCache.Add(price.Symbol,pricesByDate); + pricesByDate = new PricesByDate(); + pricesByDate.Add(price.Date, price); + Dictionary newCache = new Dictionary(snapshot.PriceCache); + newCache.Add(price.Symbol, pricesByDate); + UpdateSnapshot(newCache, snapshot.RealTimePriceCache, snapshot.NullCache); } else { - pricesByDate=priceCache[price.Symbol]; - if(pricesByDate.ContainsKey(price.Date.Date)) return; - pricesByDate.Add(price.Date.Date,price); - } - } - } - public bool ContainsPrice(String symbol,DateTime date) - { - if(!priceCache.ContainsKey(symbol)) return false; - PricesByDate pricesByDate=priceCache[symbol]; - if(!pricesByDate.ContainsKey(date.Date)) return false; - return true; - } - private void ThreadProc() - { - int quantums=0; - int quantumInterval=1000; - while(threadRun) - { - Thread.Sleep(quantumInterval); - if(!threadRun) break; - quantums+=quantumInterval; - if(quantums>cacheRefreshAfter) - { - quantums=0; - lock(thisLock) + pricesByDate = snapshot.PriceCache[price.Symbol]; + if (!pricesByDate.ContainsKey(price.Date)) { - realTimePriceCache.Clear(); - MDTrace.WriteLine(LogLevel.DEBUG,"Clearing GBPriceCache price cache."); + pricesByDate.Add(price.Date, price); } } } - MDTrace.WriteLine(LogLevel.DEBUG,"[GBPriceCache:ThreadProc]Thread ended."); + } + + public bool ContainsPrice(String symbol, DateTime date) + { + if (!snapshot.PriceCache.ContainsKey(symbol)) return false; + PricesByDate pricesByDate = snapshot.PriceCache[symbol]; + return pricesByDate.ContainsKey(date); + } + + private void ThreadProc() + { + int quantums = 0; + int quantumInterval = 1000; + + while (threadRun) + { + Thread.Sleep(quantumInterval); + quantums += quantumInterval; + + if (quantums > cacheRefreshAfter) + { + quantums = 0; + lock (thisLock) + { + UpdateSnapshot(snapshot.PriceCache, new Dictionary(), snapshot.NullCache); + } + } + } + } + + private void UpdateSnapshot( + Dictionary newPriceCache, + Dictionary newRealtimePriceCache, + Dictionary newNullCache) + { + snapshot = new CacheSnapshot(newPriceCache, newRealtimePriceCache, newNullCache); } } } - diff --git a/MarketData/MarketDataLib/Cache/LocalPriceCache.cs b/MarketData/MarketDataLib/Cache/LocalPriceCache.cs index a274616..991ecee 100755 --- a/MarketData/MarketDataLib/Cache/LocalPriceCache.cs +++ b/MarketData/MarketDataLib/Cache/LocalPriceCache.cs @@ -1,61 +1,65 @@ using MarketData.MarketDataModel; using MarketData.Utils; using MarketData.DataAccess; +using System.Collections.Concurrent; -// This cache is mainly used by gainloss generator. This cache is intended to be front loaded and then used. -// This cache will not attempt to load an item that is not found. It does have a Refresh() that will reload only the most recent -// pricing data from the database in order to maintain the most updated pricing. namespace MarketData.Cache { - public class LocalPriceCache - { - private Dictionary priceCache=new Dictionary(); - private static LocalPriceCache instance=null; + public class LocalPriceCache + { + private Dictionary priceCache = new Dictionary(); + private static LocalPriceCache instance = null; private DateTime latestDate = Utility.Epoch; - private Thread cacheMonitorThread=null; - private volatile bool threadRun=true; - private int cacheCycle=300000; - private Object thisLock=new Object(); + private Thread cacheMonitorThread = null; + private volatile bool threadRun = true; + private int cacheCycle = 300000; + private object thisLock = new object(); + private object fetchLock = new object(); - private LocalPriceCache() - { - cacheMonitorThread=new Thread(new ThreadStart(ThreadProc)); + private LocalPriceCache() + { + cacheMonitorThread = new Thread(new ThreadStart(ThreadProc)); cacheMonitorThread.Start(); - } + } public void Clear() { - lock(thisLock) + lock (thisLock) { - priceCache=new Dictionary(); + priceCache = new Dictionary(); RefreshLatestDate(); } } public void Dispose() { - lock(thisLock) + Thread threadToJoin = null; + + lock (thisLock) { - if(null==instance || false==threadRun)return; - threadRun=false; - if(null!=cacheMonitorThread) - { - MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[LocalPriceCache:Dispose]Thread state is '{0}'. Joining main thread...",Utility.ThreadStateToString(cacheMonitorThread))); - cacheMonitorThread.Join(5000); - this.cacheMonitorThread=null; - } - MDTrace.WriteLine(LogLevel.DEBUG,"[LocalPriceCache:Dispose] End"); - instance=null; + if (instance == null || !threadRun) return; + threadRun = false; + threadToJoin = cacheMonitorThread; + cacheMonitorThread = null; + instance = null; } + + if (threadToJoin != null) + { + MDTrace.WriteLine(LogLevel.DEBUG, $"[LocalPriceCache:Dispose] Thread state is '{Utility.ThreadStateToString(threadToJoin)}'. Joining..."); + threadToJoin.Join(5000); + } + + MDTrace.WriteLine(LogLevel.DEBUG, "[LocalPriceCache:Dispose] End"); } public static LocalPriceCache GetInstance() { - lock(typeof(LocalPriceCache)) + lock (typeof(LocalPriceCache)) { - if(null==instance) + if (instance == null) { - instance=new LocalPriceCache(); + instance = new LocalPriceCache(); } return instance; } @@ -63,17 +67,17 @@ namespace MarketData.Cache public void RefreshLatestDate() { - lock(typeof(LocalPriceCache)) + lock (thisLock) { - latestDate=PricingDA.GetLatestDate(); + latestDate = PricingDA.GetLatestDate(); } } public DateTime GetLatestDate() { - lock(typeof(LocalPriceCache)) + lock (thisLock) { - if(Utility.IsEpoch(latestDate)) + if (Utility.IsEpoch(latestDate)) { RefreshLatestDate(); } @@ -83,63 +87,118 @@ namespace MarketData.Cache public void Refresh() { - lock(typeof(LocalPriceCache)) + List symbols; + Dictionary currentMaxDates; + + lock (thisLock) { - List symbols=new List(priceCache.Keys); - Dictionary maxDbDates = PricingDA.GetLatestDates(symbols); - RefreshLatestDate(); - foreach(String symbol in symbols) + symbols = priceCache.Keys.ToList(); + currentMaxDates = priceCache.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.MaxDate); + } + + if (symbols.Count == 0) return; + + ConcurrentDictionary fullReloads = new ConcurrentDictionary(); + ConcurrentDictionary singleUpdates = new ConcurrentDictionary(); + DateTime latestDateFromDb; + + lock (fetchLock) + { + Dictionary maxDbDates = PricingDA.GetLatestDates(symbols); + latestDateFromDb = PricingDA.GetLatestDate(); + + Parallel.ForEach(symbols, new ParallelOptions { MaxDegreeOfParallelism = 8 }, symbol => { - PricesByDate symbolPrices=priceCache[symbol]; - DateTime maxDate=symbolPrices.MaxDate; // get the latest date in the cache - if(maxDbDates.ContainsKey(symbol) && !maxDbDates[symbol].Date.Equals(maxDate.Date)) // if the cache date and the database date are not equal then reload the cache + if (!currentMaxDates.TryGetValue(symbol, out var cachedMax)) return; + + if (maxDbDates.TryGetValue(symbol, out var dbMax) && dbMax.Date != cachedMax.Date) { - Prices prices=PricingDA.GetPrices(symbol,symbolPrices.MinDate); // reload the prices for this symbol using the current minDate in the cache as a lower boundary - if(null==prices)continue; // if we can't load any prices for symbol then just continue - priceCache.Remove(symbol); // remove the pricing entries in the price cache for the symbol - priceCache.Add(symbol,prices.GetPricesByDate()); // reload the cache + Prices prices = PricingDA.GetPrices(symbol, cachedMax); + if (prices != null) fullReloads[symbol] = prices.GetPricesByDate(); } else { - Price price=PricingDA.GetPrice(symbol,maxDate); // the max date from the cache equals the max date from the database so just reload the latest price from the database - if(null==price)continue; // if no latest price then just continue - symbolPrices.Remove(maxDate); // remove the current price associated with the max date - symbolPrices.Add(maxDate,price); // reload the latest price for maxDate(symbol) into the cache + Price price = PricingDA.GetPrice(symbol, cachedMax); + if (price != null) singleUpdates[symbol] = price; + } + }); + } + + lock (thisLock) + { + latestDate = latestDateFromDb; + + foreach (var kvp in fullReloads) + { + if (priceCache.TryGetValue(kvp.Key, out PricesByDate existing) && existing.MaxDate == currentMaxDates[kvp.Key]) + { + priceCache[kvp.Key] = kvp.Value; + } + } + + foreach (var kvp in singleUpdates) + { + if (priceCache.TryGetValue(kvp.Key, out PricesByDate pricesByDate) && pricesByDate.MaxDate == currentMaxDates[kvp.Key]) + { + // Remove the old price (if any) and add the new price properly + if (pricesByDate.ContainsKey(kvp.Value.Date)) + pricesByDate.Remove(kvp.Value.Date); + pricesByDate.Add(kvp.Value.Date, kvp.Value); } } } + + MDTrace.WriteLine(LogLevel.DEBUG, $"Full reloads: {fullReloads.Count}, Single updates: {singleUpdates.Count}"); } -// This version of Add(PortfolioTrades) will account for adding multiple lots at different times. So instead of just checking for the existance of the symbol in the cache -// we look to see if the symbol is in the cache and what dates are available. If the date range specified in the trade are not available then we load those date ranges. -// This is a brute force approach always maintaining the gap between successive TradeDates in the portfolio trades and the maximum date for the symbol in the database. -// So while it is inefficient in terms of memory usage it alleviates the need for figuring out contiguous price sections public void Add(PortfolioTrades portfolioTrades) { - lock(typeof(LocalPriceCache)) + List symbols = portfolioTrades.Symbols; + Dictionary minTradeDates = symbols.ToDictionary(sym => sym, sym => portfolioTrades.GetMinTradeDate(sym)); + + Dictionary minCacheDates; + lock (thisLock) { - Profiler profiler=new Profiler(); - profiler.Start(); - List symbols=portfolioTrades.Symbols; - foreach(String symbol in symbols) + minCacheDates = symbols.ToDictionary(sym => sym, sym => priceCache.ContainsKey(sym) ? priceCache[sym].MinDate : DateTime.MaxValue); + } + + ConcurrentDictionary fetchedPrices = new ConcurrentDictionary(); + + Parallel.ForEach(symbols, new ParallelOptions { MaxDegreeOfParallelism = 8 }, symbol => + { + DateTime minTradeDate = minTradeDates[symbol]; + DateTime minCacheDate = minCacheDates[symbol]; + Prices prices = null; + + try { - DateTime minPortfolioTradeDate=portfolioTrades.GetMinTradeDate(symbol); - if(!ContainsSymbol(symbol)) + if (minCacheDate == DateTime.MaxValue) { - Prices prices=PricingDA.GetPrices(symbol,minPortfolioTradeDate); - if(null==prices)continue; - foreach(Price price in prices)Add(price); + prices = PricingDA.GetPrices(symbol, minTradeDate); } - else + else if (minTradeDate < minCacheDate) { - DateTime minCacheDate=GetMinCacheDate(symbol); - if(minPortfolioTradeDate 0) + { + fetchedPrices[symbol] = prices; + } + } + catch (Exception ex) + { + MDTrace.WriteLine(LogLevel.DEBUG, $"Error fetching prices for {symbol}: {ex.Message}"); + } + }); + + lock (thisLock) + { + foreach (var kvp in fetchedPrices) + { + foreach (var price in kvp.Value) + { + Add(price); } } } @@ -147,151 +206,186 @@ namespace MarketData.Cache public void Add(Prices prices) { - foreach(Price price in prices) - { - Add(price); - } + foreach (Price price in prices) + { + Add(price); + } } - public void Add(List symbols,DateTime pricingDate) + public void Add(List symbols, DateTime pricingDate) { - foreach(String symbol in symbols) + if (symbols == null || symbols.Count == 0) return; + + ConcurrentDictionary fetchedPrices = new ConcurrentDictionary(); + + Parallel.ForEach(symbols, new ParallelOptions { MaxDegreeOfParallelism = 8 }, symbol => { - if(ContainsPrice(symbol,pricingDate))continue; - Price price=PricingDA.GetPrice(symbol,pricingDate); - if(null==price)continue; - Add(price); + lock (thisLock) + { + if (ContainsPrice(symbol, pricingDate)) return; + } + + try + { + Price price = PricingDA.GetPrice(symbol, pricingDate); + if (price != null) fetchedPrices[symbol] = price; + } + catch (Exception ex) + { + MDTrace.WriteLine(LogLevel.DEBUG, $"Error fetching price for {symbol} on {pricingDate:yyyy-MM-dd}: {ex.Message}"); + } + }); + + lock (thisLock) + { + foreach (var kvp in fetchedPrices) + { + Add(kvp.Value); + } } } public void Add(Price price) { - lock(typeof(LocalPriceCache)) + if (price == null) return; + + lock (thisLock) { - if(null==price)return; - if(ContainsPrice(price.Symbol,price.Date))return; - PricesByDate pricesByDate=null; - if(!priceCache.ContainsKey(price.Symbol)) + if (!priceCache.TryGetValue(price.Symbol, out var pricesByDate)) { - pricesByDate=new PricesByDate(); - pricesByDate.Add(price.Date,price); - priceCache.Add(price.Symbol,pricesByDate); - return; + pricesByDate = new PricesByDate(); + priceCache[price.Symbol] = pricesByDate; + } + if (!pricesByDate.ContainsKey(price.Date)) + { + pricesByDate.Add(price.Date, price); // must use Add() to update MinDate/MaxDate } - pricesByDate=priceCache[price.Symbol]; - if(pricesByDate.ContainsKey(price.Date))return; - pricesByDate.Add(price.Date,price); } } - public DateTime GetMinCacheDate(String symbol) + public DateTime GetMinCacheDate(string symbol) { - if(!ContainsSymbol(symbol))return Utility.Epoch; - PricesByDate symbolPrices=priceCache[symbol]; - return symbolPrices.MinDate; + lock (thisLock) + { + if (!priceCache.TryGetValue(symbol, out var symbolPrices) || symbolPrices.Count == 0) + { + return Utility.Epoch; + } + return symbolPrices.MinDate; + } } public void RemoveDate(DateTime date) { - lock(typeof(LocalPriceCache)) + lock (thisLock) { - List symbols=new List(priceCache.Keys); - foreach(String key in symbols) + foreach (var kvp in priceCache) { - PricesByDate pricesByDate=priceCache[key]; - if(pricesByDate.ContainsKey(date))pricesByDate.Remove(date); + kvp.Value.Remove(date); } } } - - public Prices GetPrices(String symbol,DateTime date,int dayCount) - { - lock(typeof(LocalPriceCache)) - { - DateGenerator dateGenerator = new DateGenerator(); - List historicalDates = dateGenerator.GenerateHistoricalDates(date, dayCount); - Prices prices = new Prices(); - foreach(DateTime historicalDate in historicalDates) - { - Price price = GetPrice(symbol, historicalDate); - if(null!=price)prices.Add(price); - } - return prices; - } - } - - public Price GetPrice(String symbol,DateTime date) - { - lock(typeof(LocalPriceCache)) - { - if(!priceCache.ContainsKey(symbol))return null; - PricesByDate pricesByDate=priceCache[symbol]; - if(!pricesByDate.ContainsKey(date))return null; - return pricesByDate[date]; - } - } - public bool ContainsPrice(String symbol,DateTime date) - { - lock(typeof(LocalPriceCache)) - { - if(!priceCache.ContainsKey(symbol))return false; - PricesByDate pricesByDate=priceCache[symbol]; - if(!pricesByDate.ContainsKey(date))return false; - return true; - } - } - - public bool ContainsPrice(List symbols,DateTime date) - { - lock(typeof(LocalPriceCache)) - { - foreach(String symbol in symbols)if(!ContainsPrice(symbol,date))return false; - return true; - } - } - - public bool ContainsSymbol(String symbol) + public Prices GetPrices(string symbol, DateTime endDate, int dayCount) { - lock(typeof(LocalPriceCache)) + lock (thisLock) { - if(priceCache.ContainsKey(symbol))return true; - return false; + if (!priceCache.TryGetValue(symbol, out var pricesByDate)) return new Prices(); + + DateGenerator dateGenerator = new DateGenerator(); + List historicalDates = dateGenerator.GenerateHistoricalDates(endDate, dayCount); + + Prices result = new Prices(); + foreach (DateTime date in historicalDates) + { + if (pricesByDate.ContainsKey(date)) + { + result.Add(pricesByDate[date]); + } + } + + return result; } } - public long Count() + public Price GetPrice(string symbol, DateTime date) { - long count=0; - List symbols=priceCache.Keys.ToList(); - foreach(String symbol in symbols) + lock (thisLock) { - PricesByDate pricesByDate=priceCache[symbol]; - count+=pricesByDate.Count; + if (!priceCache.TryGetValue(symbol, out var pricesByDate)) return null; + return pricesByDate.TryGetValue(date, out var price) ? price : null; + } + } + + public bool ContainsPrice(string symbol, DateTime date) + { + lock (thisLock) + { + if (!priceCache.TryGetValue(symbol, out var pricesByDate)) return false; + return pricesByDate.ContainsKey(date); + } + } + + public bool ContainsPrice(List symbols, DateTime date) + { + if (symbols == null || symbols.Count == 0) return false; + + lock (thisLock) + { + foreach (string symbol in symbols) + { + if (!priceCache.TryGetValue(symbol, out var pricesByDate) || !pricesByDate.ContainsKey(date)) + { + return false; + } + } + return true; + } + } + + public bool ContainsSymbol(string symbol) + { + lock (thisLock) + { + return priceCache.ContainsKey(symbol); + } + } + + private long Count() + { + lock (thisLock) + { + long count = 0; + foreach (var pricesByDate in priceCache.Values) + { + count += pricesByDate.Count; + } + return count; } - return count; } private void ThreadProc() { - int quantums=0; - int quantumInterval=1000; - long lastCount=0; - while(threadRun) + int quantums = 0; + int quantumInterval = 1000; + long lastCount = 0; + + while (threadRun) { Thread.Sleep(quantumInterval); - quantums+=quantumInterval; - if(quantums>cacheCycle) + quantums += quantumInterval; + if (quantums > cacheCycle) { - quantums=0; - lock(thisLock) + quantums = 0; + lock (thisLock) { - lastCount=Count(); - MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[LocalPriceCache:ThreadProc] Symbols: {0}. Items in cache: {1}.",priceCache.Keys.Count,Utility.FormatNumber(lastCount,0,true))); + lastCount = Count(); + MDTrace.WriteLine(LogLevel.DEBUG, $"[LocalPriceCache:ThreadProc] Symbols: {priceCache.Keys.Count}. Items in cache: {Utility.FormatNumber(lastCount,0,true)}."); } } } - MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[LocalPriceCache:ThreadProc] Thread ended. Items in cache:{0}",Utility.FormatNumber(lastCount,0,true))); + + MDTrace.WriteLine(LogLevel.DEBUG, $"[LocalPriceCache:ThreadProc] Thread ended. Items in cache:{Utility.FormatNumber(lastCount,0,true)}"); } - } + } } diff --git a/MarketData/MarketDataLib/MarketDataModel/PricesByDate.cs b/MarketData/MarketDataLib/MarketDataModel/PricesByDate.cs index 0d68c4e..5040a4b 100755 --- a/MarketData/MarketDataLib/MarketDataModel/PricesByDate.cs +++ b/MarketData/MarketDataLib/MarketDataModel/PricesByDate.cs @@ -16,6 +16,12 @@ namespace MarketData.MarketDataModel public PricesByDate() { } + + public new Price this[DateTime key] + { + get => base[key]; + } + public new void Add(DateTime key,Price price) { if(Utility.IsEpoch(key))return; @@ -24,10 +30,12 @@ namespace MarketData.MarketDataModel if(Utility.IsEpoch(minDate))minDate=key; else if(key pricingMock = default!; + private readonly string symbol = "AAPL"; + private readonly DateTime today = DateTime.Today; + + // ------------------------ + // Helper to fully reset singleton + // ------------------------ + private void ResetCacheSingleton() + { + if (null != cache) cache.Dispose(); + FieldInfo field = typeof(GBPriceCache) + .GetField("priceCacheInstance", BindingFlags.Static | BindingFlags.NonPublic); + if (null != field) field.SetValue(null, null); + + cache = GBPriceCache.GetInstance(); + cache.Clear(); + } + + [TestInitialize] + public void Setup() + { + ResetCacheSingleton(); + pricingMock = new Mock(); + cache.PricingDataAccess = pricingMock.Object; + } + + [TestCleanup] + public void Cleanup() + { + cache.Dispose(); + } + + // ------------------------ + // Singleton Behavior + // ------------------------ + [TestMethod] + public void GetInstance_ReturnsSingleton() + { + GBPriceCache instance1 = GBPriceCache.GetInstance(); + GBPriceCache instance2 = GBPriceCache.GetInstance(); + Assert.AreSame(instance1, instance2); + } + + // ------------------------ + // Basic GetPrice + // ------------------------ + [TestMethod] + public void GetPrice_ReturnsNull_WhenNoData() + { + Price nullPrice = default; + pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(nullPrice); + Price result = cache.GetPrice(symbol, today); + Assert.IsNull(result); + } + + [TestMethod] + public void GetPrice_CachesResult_WhenDataExists() + { + Price price = new Price { Symbol = symbol, Date = today }; + pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(price); + Price first = cache.GetPrice(symbol, today); + Price second = cache.GetPrice(symbol, today); + Assert.AreSame(first, second); + Assert.AreEqual(symbol, first.Symbol); + } + + // ------------------------ + // ContainsPrice + // ------------------------ + [TestMethod] + public void ContainsPrice_BehavesCorrectly() + { + Price price = new Price { Symbol = symbol, Date = today }; + pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(price); + Assert.IsFalse(cache.ContainsPrice(symbol, today)); + cache.GetPrice(symbol, today); + Assert.IsTrue(cache.ContainsPrice(symbol, today)); + } + + // ------------------------ + // GetPriceOrLatestAvailable + // ------------------------ + [TestMethod] + public void GetPriceOrLatestAvailable_ReturnsLatestPrice() + { + DateTime earlier = today.AddDays(-1); + Price latestPrice = new Price { Symbol = symbol, Date = today }; + pricingMock.Setup(x => x.GetLatestDateOnOrBefore(symbol, earlier)).Returns(today); + pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(latestPrice); + Price result = cache.GetPriceOrLatestAvailable(symbol, earlier); + Assert.IsNotNull(result); + Assert.AreEqual(today, result.Date); + } + + // ------------------------ + // GetPrices (DateTime overload) + // ------------------------ + [TestMethod] + public void GetPrices_DateTimeOverload_ReturnsList() + { + DateGenerator generator = new DateGenerator(); + DateTime later = generator.GetPrevBusinessDay(DateTime.Today); + DateTime earlier = later; + for (int i = 0; i < 2; i++) earlier = generator.GetPrevBusinessDay(earlier.AddDays(-1)); + + List allDates = generator.GenerateHistoricalDates(later, 1000); + List allPrices = allDates.Select(d => new Price { Symbol = symbol, Date = d }).ToList(); + + pricingMock.Setup(x => x.GetPrices(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string s, DateTime max, DateTime min) => + { + List matched = allPrices.Where(p => p.Date >= min && p.Date <= max).ToList(); + return new Prices(matched); + }); + + int dayCount = generator.GenerateHistoricalDates(later, 100) + .Where(d => d <= later && d >= earlier) + .Count(); + + Prices prices = cache.GetPrices(symbol, earlier, later); + Assert.IsNotNull(prices); + Assert.AreEqual(dayCount, prices.Count); + Assert.IsTrue(prices.SequenceEqual(prices.OrderByDescending(p => p.Date))); + } + + // ------------------------ + // GetPrices (startDate + dayCount) + // ------------------------ + [TestMethod] + public void GetPrices_StartDateDayCount_Works() + { + int dayCount = 5; + List allPrices = new List(); + for (int i = 0; i < 100; i++) allPrices.Add(new Price { Symbol = symbol, Date = today.AddDays(-i) }); + + pricingMock.Setup(x => x.GetPrices(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((string s, DateTime max, DateTime min) => + { + List matched = allPrices.Where(p => p.Date >= min && p.Date <= max).ToList(); + return new Prices(matched); + }); + + Prices prices = cache.GetPrices(symbol, today, dayCount); + Assert.AreEqual(dayCount, prices.Count); + Assert.IsTrue(prices.SequenceEqual(prices.OrderByDescending(p => p.Date))); + } + + // ------------------------ + // Clear + // ------------------------ + [TestMethod] + public void Clear_RemovesAllData() + { + Price price = new Price { Symbol = symbol, Date = today }; + pricingMock.Setup(x => x.GetPrice(symbol, today)).Returns(price); + cache.GetPrice(symbol, today); + cache.Clear(); + Assert.IsFalse(cache.ContainsPrice(symbol, today)); + } + + // ------------------------ + // ClearCacheOnOrBefore + // ------------------------ + [TestMethod] + public void ClearCacheOnOrBefore_FiltersOldPrices() + { + Price oldPrice = new Price { Symbol = symbol, Date = today.AddDays(-5) }; + Price recentPrice = new Price { Symbol = symbol, Date = today }; + pricingMock.Setup(x => x.GetPrice(symbol, oldPrice.Date)).Returns(oldPrice); + pricingMock.Setup(x => x.GetPrice(symbol, recentPrice.Date)).Returns(recentPrice); + + cache.GetPrice(symbol, oldPrice.Date); + cache.GetPrice(symbol, recentPrice.Date); + + cache.ClearCacheOnOrBefore(today.AddDays(-1)); + + Assert.IsFalse(cache.ContainsPrice(symbol, oldPrice.Date)); + Assert.IsTrue(cache.ContainsPrice(symbol, recentPrice.Date)); + } + + // ------------------------ + // Concurrency Tests + // ------------------------ + [TestMethod] + public void Concurrent_ReadWrite_DoesNotThrow() + { + Price price = new Price { Symbol = symbol, Date = today }; + pricingMock.Setup(x => x.GetPrice(symbol, It.IsAny())).Returns(price); + Parallel.For(0, 100, i => + { + cache.GetPrice(symbol, today.AddDays(-i)); + cache.ContainsPrice(symbol, today); + }); + Assert.IsTrue(true); + } + + [TestMethod] + public void Concurrent_MultipleSymbols_DoesNotThrow() + { + List symbols = new List { "AAPL", "MSFT", "GOOG" }; + Price price = new Price { Symbol = "TEST", Date = today }; + pricingMock.Setup(x => x.GetPrice(It.IsAny(), It.IsAny())).Returns(price); + + Parallel.ForEach(symbols, s => + { + for (int i = 0; i < 50; i++) + { + cache.GetPrice(s, today.AddDays(-i)); + } + }); + + Assert.IsTrue(true); + } + + // ------------------------ + // Dispose + // ------------------------ + [TestMethod] + public void Dispose_CanBeCalledMultipleTimes() + { + cache.Dispose(); + cache.Dispose(); + Assert.IsTrue(true); + } +} diff --git a/MarketDataUnitTests/LocalPriceCacheUnitTestClass.cs b/MarketDataUnitTests/LocalPriceCacheUnitTestClass.cs new file mode 100755 index 0000000..677cb8e --- /dev/null +++ b/MarketDataUnitTests/LocalPriceCacheUnitTestClass.cs @@ -0,0 +1,471 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MarketData.Cache; +using MarketData.MarketDataModel; +using System; +using System.Collections.Generic; +using MarketData.Utils; + +namespace MarketDataUnitTests; + +[TestClass] +public class LocalPriceCacheTests +{ + private LocalPriceCache cache = default!; + + [TestInitialize] + public void Setup() + { + cache = LocalPriceCache.GetInstance(); + cache.Clear(); + } + + [TestCleanup] + public void Cleanup() + { + if (cache != null) + cache.Dispose(); + } + + private Price CreatePrice(string symbol, DateTime date, double close) + { + return new Price + { + Symbol = symbol, + Date = date, + Close = close, + Open = close - 1, + High = close + 1, + Low = close - 2, + AdjClose = close, + Volume = 1000, + Source = Price.PriceSource.Other + }; + } + + #region Basic Add/Get Tests + + [TestMethod] + public void AddAndGetPrice_SinglePrice_ShouldReturnPrice() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + Price price = CreatePrice(symbol, date, 250); + + cache.Add(price); + + Price cachedPrice = cache.GetPrice(symbol, date); + Assert.IsNotNull(cachedPrice); + Assert.AreEqual(250, cachedPrice.Close); + } + + [TestMethod] + public void AddAndGetPrices_List_ShouldReturnAllPrices() + { + DateTime date = new DateTime(2026, 2, 22); + List symbols = new List { "MSFT", "AAPL", "GOOG" }; + Prices prices = new Prices(); + + foreach (string symbol in symbols) + { + prices.Add(CreatePrice(symbol, date, 100 + symbols.IndexOf(symbol) * 10)); + } + + cache.Add(prices); + + foreach (string symbol in symbols) + { + Price cachedPrice = cache.GetPrice(symbol, date); + Assert.IsNotNull(cachedPrice); + } + } + + #endregion + + #region Cache State Tests + + [TestMethod] + public void ContainsPrice_ShouldReturnTrueForExistingPrice() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + Price price = CreatePrice(symbol, date, 250); + cache.Add(price); + + Assert.IsTrue(cache.ContainsPrice(symbol, date)); + } + + [TestMethod] + public void ContainsPrice_ShouldReturnFalseForNonExistingPrice() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + + Assert.IsFalse(cache.ContainsPrice(symbol, date)); + } + + [TestMethod] + public void GetMinCacheDate_ShouldReturnEpochIfNotPresent() + { + DateTime minDate = cache.GetMinCacheDate("UNKNOWN"); + Assert.AreEqual(DateTime.MinValue, minDate); // Utility.Epoch assumed to be DateTime.MinValue + } + + [TestMethod] + public void GetMinCacheDate_ShouldReturnCorrectMinDateForExistingSymbol() + { + string symbol = "MSFT"; + DateTime date1 = new DateTime(2026, 2, 22); + DateTime date2 = new DateTime(2026, 2, 23); + + cache.Add(CreatePrice(symbol, date1, 250)); + cache.Add(CreatePrice(symbol, date2, 260)); + + DateTime minDate = cache.GetMinCacheDate(symbol); + Assert.AreEqual(date1, minDate); + } + + #endregion + + #region Concurrency & Thread Safety Tests + + [TestMethod] + public void AddPricesConcurrently_ShouldNotThrowException() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + List prices = new List + { + CreatePrice(symbol, date, 250), + CreatePrice(symbol, date.AddDays(1), 260) + }; + + List tasks = new List(); + foreach (Price price in prices) + { + tasks.Add(System.Threading.Tasks.Task.Run(() => cache.Add(price))); + } + System.Threading.Tasks.Task.WhenAll(tasks).Wait(); + + Price cachedPrice = cache.GetPrice(symbol, date); + Assert.IsNotNull(cachedPrice); + } + + [TestMethod] + public void ThreadSafety_WithMultipleAdds_ShouldNotCorruptCache() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + List prices = new List(); + for (int i = 0; i < 1000; i++) + { + prices.Add(CreatePrice(symbol, date.AddDays(i), 250 + i)); + } + + List tasks = new List(); + foreach (Price price in prices) + { + tasks.Add(System.Threading.Tasks.Task.Run(() => cache.Add(price))); + } + System.Threading.Tasks.Task.WhenAll(tasks).Wait(); + + Price cachedPrice = cache.GetPrice(symbol, date.AddDays(500)); + Assert.IsNotNull(cachedPrice); + } + + #endregion + + #region Remove/Clear Tests + + [TestMethod] + public void RemoveDate_ShouldRemovePrice() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + cache.Add(CreatePrice(symbol, date, 250)); + + cache.RemoveDate(date); + Assert.IsFalse(cache.ContainsPrice(symbol, date)); + } + + [TestMethod] + public void Clear_ShouldRemoveAllPrices() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + cache.Add(CreatePrice(symbol, date, 250)); + + cache.Clear(); + Assert.IsFalse(cache.ContainsPrice(symbol, date)); + } + + #endregion + + #region Date & Range Tests + + [TestMethod] + public void GetPrices_ShouldReturnCorrectPricesInRange() + { + string symbol = "MSFT"; + DateTime endDate = new DateTime(2026, 2, 25); + int dayCount = 3; + + DateGenerator dateGenerator = new DateGenerator(); + List historicalDates = dateGenerator.GenerateHistoricalDates(endDate, dayCount); + + Prices prices = new Prices(); + int priceValue = 250; + foreach (DateTime date in historicalDates) + { + prices.Add(CreatePrice(symbol, date, priceValue)); + priceValue++; + } + + cache.Add(prices); + + Prices rangePrices = cache.GetPrices(symbol, endDate, dayCount); + + Assert.AreEqual(historicalDates.Count, rangePrices.Count); + for (int i = 0; i < historicalDates.Count; i++) + { + Assert.AreEqual(historicalDates[i], rangePrices[i].Date); + } + } + + [TestMethod] + public void GetPrices_ShouldReturnEmptyForNoPricesInRange() + { + string symbol = "MSFT"; + DateTime startDate = new DateTime(2026, 2, 22); + + Prices rangePrices = cache.GetPrices(symbol, startDate, 3); + Assert.AreEqual(0, rangePrices.Count); + } + + #endregion + + #region Duplicate & Edge Case Tests + + [TestMethod] + public void AddDuplicatePrice_ShouldNotDuplicateInCache() + { + string symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + + Price price1 = CreatePrice(symbol, date, 250); + Price price2 = CreatePrice(symbol, date, 250); + + cache.Add(price1); + cache.Add(price2); + + Price cachedPrice = cache.GetPrice(symbol, date); + Assert.IsNotNull(cachedPrice); + Assert.AreEqual(250, cachedPrice.Close); + } + + [TestMethod] + public void RemoveNonExistentPrice_ShouldNotThrowException() + { + DateTime date = new DateTime(2026, 2, 22); + cache.RemoveDate(date); + Assert.IsTrue(true); + } + + [TestMethod] + public void AddPortfolioTrades_WithNoNewPrices_ShouldNotFetch() + { + DateGenerator dateGenerator = new DateGenerator(); + string symbol = "MSFT"; + DateTime tradeDate = new DateTime(2026, 2, 22); + tradeDate = dateGenerator.GetPrevBusinessDay(tradeDate); + + // Add a price to the cache + cache.Add(CreatePrice(symbol, tradeDate, 250)); + + // Create PortfolioTrades manually + PortfolioTrades trades = new PortfolioTrades(); + trades.Add(new PortfolioTrade + { + Symbol = symbol, + TradeDate = tradeDate, + Shares = 1, + Price = 250, + Status = "OPEN" + }); + + // Add portfolio trades to the cache — should not fetch additional prices + cache.Add(trades); + + // Retrieve prices for the generated historical dates + Prices cachedPrices = cache.GetPrices(symbol, tradeDate, 1); + + Assert.AreEqual(1, cachedPrices.Count); + Assert.AreEqual(tradeDate, cachedPrices[0].Date); + Assert.AreEqual(250, cachedPrices[0].Close); + } + + [TestMethod] + public void AddListOfSymbols_EmptyList_ShouldNotThrow() + { + cache.Add(new List(), DateTime.Today); + Assert.IsTrue(true); + } + + [TestMethod] + public void AddListOfSymbols_PriceIsNull_ShouldNotThrow() + { + List symbols = new List { "MSFT" }; + cache.Add(symbols, new DateTime(1900, 1, 1)); + Assert.IsTrue(true); + } + + [TestMethod] + public void ContainsPrice_ListSymbols_NullOrEmpty_ShouldReturnFalse() + { + Assert.IsFalse(cache.ContainsPrice((List)null, DateTime.Today)); + Assert.IsFalse(cache.ContainsPrice(new List(), DateTime.Today)); + } + + [TestMethod] + public void GetPrice_SymbolExistsButDateMissing_ShouldReturnNull() + { + string symbol = "MSFT"; + cache.Add(CreatePrice(symbol, new DateTime(2026, 2, 22), 250)); + Price result = cache.GetPrice(symbol, new DateTime(2026, 2, 23)); + Assert.IsNull(result); + } + + [TestMethod] + public void RemoveDate_EmptyCache_ShouldNotThrow() + { + cache.RemoveDate(DateTime.Today); + Assert.IsTrue(true); + } + + [TestMethod] + public void Refresh_EmptyCache_ShouldNotThrow() + { + cache.Clear(); + cache.Refresh(); + Assert.IsTrue(true); + } + + [TestMethod] + public void Dispose_WhenAlreadyDisposed_ShouldNotThrow() + { + cache.Dispose(); + cache.Dispose(); + Assert.IsTrue(true); + } + + [TestMethod] + public void Refresh_WithExistingPrices_ShouldNotThrow() + { + DateGenerator dateGenerator = new DateGenerator(); + String symbol = "MSFT"; + DateTime date1 = new DateTime(2026, 2, 22); + DateTime date2 = new DateTime(2026, 2, 23); + date1=dateGenerator.FindPrevBusinessDay(date1); + date2=dateGenerator.FindNextBusinessDay(date2); + + Price price1 = CreatePrice(symbol, date1, 250); + Price price2 = CreatePrice(symbol, date2, 260); + + cache.Add(price1); + cache.Add(price2); + + // Refresh should process existing prices without throwing + cache.Refresh(); + + Prices cachedPrices1 = cache.GetPrices(symbol, date1, 1); + Prices cachedPrices2 = cache.GetPrices(symbol, date2, 1); + + Assert.AreEqual(1, cachedPrices1.Count); + Assert.AreEqual(1, cachedPrices2.Count); + } + + [TestMethod] + public void AddPrices_NullOrEmpty_ShouldNotThrow() + { + Prices emptyPrices = new Prices(); + cache.Add(emptyPrices); // Should not throw + + Prices emptyList = new Prices(); + cache.Add(emptyList); // Should not throw + + cache.Add((Price)null!); // Should not throw + + Assert.IsTrue(true); + } + + [TestMethod] + public void Dispose_WithPopulatedCache_ShouldNotThrow() + { + String symbol = "MSFT"; + DateTime date = new DateTime(2026, 2, 22); + + Price price = CreatePrice(symbol, date, 250); + cache.Add(price); + + // Dispose should handle non-empty cache gracefully + cache.Dispose(); + + // Subsequent calls to Dispose should still be safe + cache.Dispose(); + + Assert.IsTrue(true); + } + + [TestMethod] + public void ThreadSafety_WithMultipleAdds_ShouldNotCorruptCache_BusinessDays() + { + string symbol = "MSFT"; + DateGenerator dateGenerator = new DateGenerator(); + + DateTime baseDate = new DateTime(2026, 2, 22); + int numPrices = 1000; + + List prices = new List(); + HashSet usedDates = new HashSet(); + DateTime currentDate = baseDate; + + int priceCounter = 0; + while (prices.Count < numPrices) + { + // Advance one day + currentDate = currentDate.AddDays(1); + + // Adjust to previous business day + DateTime businessDate = dateGenerator.GetPrevBusinessDay(currentDate); + + // Only add if unique + if (!usedDates.Contains(businessDate)) + { + usedDates.Add(businessDate); + prices.Add(CreatePrice(symbol, businessDate, 250 + priceCounter)); + priceCounter++; + currentDate = businessDate; // continue from this business date + } + } + + // Multi-threaded addition + List tasks = new List(); + foreach (Price price in prices) + { + tasks.Add(System.Threading.Tasks.Task.Run(() => cache.Add(price))); + } + + System.Threading.Tasks.Task.WhenAll(tasks).Wait(); + + // Pick a middle sample + DateTime sampleDate = prices[numPrices / 2].Date; + Price cachedPrice = cache.GetPrice(symbol, sampleDate); + + Assert.IsNotNull(cachedPrice); + Assert.AreEqual(250 + (numPrices / 2), cachedPrice.Close); + + // Verify total count + Prices allCachedPrices = cache.GetPrices(symbol, prices[numPrices - 1].Date, numPrices); + Assert.AreEqual(numPrices, allCachedPrices.Count); + } + #endregion +} \ No newline at end of file diff --git a/MarketDataUnitTests/MarketDataUnitTests.csproj b/MarketDataUnitTests/MarketDataUnitTests.csproj index 951b1b3..5a8907b 100644 --- a/MarketDataUnitTests/MarketDataUnitTests.csproj +++ b/MarketDataUnitTests/MarketDataUnitTests.csproj @@ -3,7 +3,7 @@ net8.0 enable - enable + disable false true @@ -12,6 +12,7 @@ +