199 lines
6.9 KiB
C#
199 lines
6.9 KiB
C#
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<T>");
|
|
|
|
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);
|
|
}
|
|
}
|