471 lines
13 KiB
C#
Executable File
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
|
|
} |