Files
ARM64/MarketDataUnitTests/LocalPriceCacheUnitTestClass.cs
2026-02-23 21:25:49 -05:00

471 lines
13 KiB
C#
Executable File

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<string> symbols = new List<string> { "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<Price> prices = new List<Price>
{
CreatePrice(symbol, date, 250),
CreatePrice(symbol, date.AddDays(1), 260)
};
List<System.Threading.Tasks.Task> tasks = new List<System.Threading.Tasks.Task>();
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<Price> prices = new List<Price>();
for (int i = 0; i < 1000; i++)
{
prices.Add(CreatePrice(symbol, date.AddDays(i), 250 + i));
}
List<System.Threading.Tasks.Task> tasks = new List<System.Threading.Tasks.Task>();
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<DateTime> 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<string>(), DateTime.Today);
Assert.IsTrue(true);
}
[TestMethod]
public void AddListOfSymbols_PriceIsNull_ShouldNotThrow()
{
List<string> symbols = new List<string> { "MSFT" };
cache.Add(symbols, new DateTime(1900, 1, 1));
Assert.IsTrue(true);
}
[TestMethod]
public void ContainsPrice_ListSymbols_NullOrEmpty_ShouldReturnFalse()
{
Assert.IsFalse(cache.ContainsPrice((List<string>)null, DateTime.Today));
Assert.IsFalse(cache.ContainsPrice(new List<string>(), 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<Price> prices = new List<Price>();
HashSet<DateTime> usedDates = new HashSet<DateTime>();
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<System.Threading.Tasks.Task> tasks = new List<System.Threading.Tasks.Task>();
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
}