using System.Reflection; using Xunit; using QuantEngine.Core.Interfaces; using QuantEngine.Infrastructure.Repositories; namespace QuantEngine.Core.Tests; public class PriceHistoryReaderTests { [Fact] public void IPriceHistoryReader_InterfaceExists() { var interfaceType = typeof(IPriceHistoryReader); Assert.NotNull(interfaceType); Assert.True(interfaceType.IsInterface); } [Fact] public void IPriceHistoryReader_HasGetBarsAsOfMethod() { var interfaceType = typeof(IPriceHistoryReader); var method = interfaceType.GetMethod("GetBarsAsOf"); Assert.NotNull(method); Assert.True(method.IsPublic); } [Fact] public void GetBarsAsOf_MethodSignatureIsCorrect() { var method = typeof(IPriceHistoryReader).GetMethod("GetBarsAsOf"); Assert.NotNull(method); var parameters = method.GetParameters(); Assert.Equal(3, parameters.Length); Assert.Equal("ticker", parameters[0].Name); Assert.Equal(typeof(string), parameters[0].ParameterType); Assert.Equal("asOfDate", parameters[1].Name); Assert.Equal(typeof(DateOnly), parameters[1].ParameterType); Assert.Equal("lookback", parameters[2].Name); Assert.Equal(typeof(int), parameters[2].ParameterType); var returnType = method!.ReturnType; Assert.True(returnType.IsGenericType); Assert.Contains("Task", returnType.Name); } [Fact] public void PriceHistoryReader_ImplementsIPriceHistoryReader() { var readerType = typeof(PriceHistoryReader); var interfaceType = typeof(IPriceHistoryReader); Assert.True(interfaceType.IsAssignableFrom(readerType)); } [Fact] public void PriceHistoryReader_HasPublicGetBarsAsOfMethod() { var method = typeof(PriceHistoryReader).GetMethod("GetBarsAsOf", BindingFlags.Public | BindingFlags.Instance); Assert.NotNull(method); Assert.Equal("GetBarsAsOf", method.Name); } [Fact] public void PriceHistoryReader_SourceCode_ContainsNoLookaheadGuarantee() { var repoRoot = FindRepositoryRoot(); var sourceFile = Path.Combine( repoRoot, "src", "dotnet", "QuantEngine.Infrastructure", "Repositories", "PriceHistoryReader.cs"); Assert.True(File.Exists(sourceFile), $"Source file not found at {sourceFile}"); var sourceCode = File.ReadAllText(sourceFile); // The no-lookahead guarantee is: WHERE trade_date <= @AsOfDate // This clause MUST be present in the SQL query to ensure no future data leaks. Assert.True(sourceCode.Contains("trade_date <= @AsOfDate"), "PriceHistoryReader must enforce trade_date <= @AsOfDate in SQL WHERE clause " + "to prevent lookahead bias. Future bars (trade_date > asOfDate) must never be returned."); } [Fact] public void PriceHistoryReader_SourceCode_DoesNotContainTradeDate_LessThan_AsOfDate() { var repoRoot = FindRepositoryRoot(); var sourceFile = Path.Combine( repoRoot, "src", "dotnet", "QuantEngine.Infrastructure", "Repositories", "PriceHistoryReader.cs"); var sourceCode = File.ReadAllText(sourceFile); // Strict check: The query must use <= (inclusive), not < (exclusive). // If someone later changes this to < by mistake, this test catches it. Assert.False(sourceCode.Contains("trade_date < @AsOfDate"), "trade_date < @AsOfDate (exclusive) is incorrect. Use trade_date <= @AsOfDate (inclusive) " + "to include bars on the exact asOfDate."); } [Fact] public void PriceHistoryReader_SourceCode_QueryOrdersDescending() { var repoRoot = FindRepositoryRoot(); var sourceFile = Path.Combine( repoRoot, "src", "dotnet", "QuantEngine.Infrastructure", "Repositories", "PriceHistoryReader.cs"); var sourceCode = File.ReadAllText(sourceFile); // Ensure most-recent-first ordering by checking for DESC in ORDER BY Assert.True(sourceCode.Contains("ORDER BY trade_date DESC"), "Query must order by trade_date DESC to return most-recent bars first."); } [Fact] public void PriceHistoryReader_SourceCode_AppliesLookbackLimit() { var repoRoot = FindRepositoryRoot(); var sourceFile = Path.Combine( repoRoot, "src", "dotnet", "QuantEngine.Infrastructure", "Repositories", "PriceHistoryReader.cs"); var sourceCode = File.ReadAllText(sourceFile); // Ensure LIMIT clause is present to avoid unbounded result sets Assert.True(sourceCode.Contains("LIMIT @Lookback"), "Query must apply LIMIT @Lookback to constrain result set size."); } private static string FindRepositoryRoot() { var current = new DirectoryInfo(AppContext.BaseDirectory); while (current != null) { if (File.Exists(Path.Combine(current.FullName, "CLAUDE.md"))) { return current.FullName; } current = current.Parent; } throw new InvalidOperationException("Could not find repository root (CLAUDE.md)"); } [Fact] public void GetBarsAsOf_ReturnsTask() { var method = typeof(IPriceHistoryReader).GetMethod("GetBarsAsOf"); Assert.NotNull(method); var returnType = method!.ReturnType; Assert.NotNull(returnType); Assert.True(returnType.IsGenericType, $"Return type {returnType} must be a generic Task"); var listType = returnType.GetGenericArguments()[0]; Assert.True(listType.IsGenericType); var recordType = listType.GetGenericArguments()[0]; Assert.Equal(typeof(PriceHistoryDailyRecord), recordType); } [Fact] public void PriceHistoryDailyRecord_ContainsAllRequiredFields() { var recordType = typeof(PriceHistoryDailyRecord); var properties = recordType.GetProperties(); var fieldNames = new[] { "Ticker", "TradeDate", "Open", "High", "Low", "Close", "Volume", "Source", "ProvenanceJson" }; foreach (var fieldName in fieldNames) { var prop = properties.FirstOrDefault(p => p.Name == fieldName); Assert.True(prop != null, $"PriceHistoryDailyRecord must have property {fieldName}"); } } [Fact] public void IPriceHistoryReader_Constructor_AcceptsIDbConnectionFactory() { var ctor = typeof(PriceHistoryReader).GetConstructors(); Assert.NotEmpty(ctor); var singleParamCtor = ctor.FirstOrDefault(c => c.GetParameters().Length == 1); Assert.NotNull(singleParamCtor!); var param = singleParamCtor.GetParameters()[0]; var paramTypeName = param.ParameterType.Name; Assert.Equal("IDbConnectionFactory", paramTypeName); } }