using Xunit; using KArtSell.Modules.ModelOperations.ShadowRun.Services; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace KArtSell.Integration.Tests; /// /// Tests for market calendar: trading sessions, holiday exclusion, determinism. /// public sealed class MarketCalendarServiceTests : IAsyncLifetime { private IMemoryCache _cache = null!; private ILogger _logger = null!; public Task InitializeAsync() { _cache = new MemoryCache(new MemoryCacheOptions()); _logger = new NoOpLogger(); return Task.CompletedTask; } public Task DisposeAsync() { _cache?.Dispose(); return Task.CompletedTask; } [Fact] public async Task GetTradingSessionsAsync_ReturnsSessionsInWindow() { // Arrange var service = new MarketCalendarService(_cache, _logger); var startDate = new DateOnly(2024, 1, 2); var endDate = new DateOnly(2024, 1, 31); // Act var sessions = await service.GetTradingSessionsAsync(startDate, endDate, CancellationToken.None); // Assert Assert.NotEmpty(sessions); Assert.All(sessions, session => { Assert.True(session >= startDate && session <= endDate); Assert.NotEqual(DayOfWeek.Saturday, session.DayOfWeek); Assert.NotEqual(DayOfWeek.Sunday, session.DayOfWeek); }); } [Fact] public async Task GetTradingSessionsAsync_ExcludesHolidays() { // Arrange var service = new MarketCalendarService(_cache, _logger); var startDate = new DateOnly(2024, 2, 1); var endDate = new DateOnly(2024, 2, 15); // Includes Lunar New Year // Act var sessions = await service.GetTradingSessionsAsync(startDate, endDate, CancellationToken.None); // Assert // 2024-02-09 (Lunar New Year Eve), 2024-02-10 (Lunar New Year), 2024-02-11, 2024-02-12 should be excluded var lunarNewYearDates = new[] { new DateOnly(2024, 2, 9), new DateOnly(2024, 2, 10), new DateOnly(2024, 2, 11), new DateOnly(2024, 2, 12) }; Assert.DoesNotContain(lunarNewYearDates, d => sessions.Contains(d)); } [Fact] public async Task GetTradingSessionsAsync_IsDeterministic() { // Arrange var service = new MarketCalendarService(_cache, _logger); var startDate = new DateOnly(2024, 1, 2); var endDate = new DateOnly(2024, 1, 31); // Act: Call twice var sessions1 = await service.GetTradingSessionsAsync(startDate, endDate, CancellationToken.None); var sessions2 = await service.GetTradingSessionsAsync(startDate, endDate, CancellationToken.None); // Assert Assert.Equal(sessions1.Count, sessions2.Count); for (int i = 0; i < sessions1.Count; i++) { Assert.Equal(sessions1[i], sessions2[i]); } } [Fact] public async Task GetTradingSessionsAsync_Covers252DaysForAnnualWindow() { // Arrange var service = new MarketCalendarService(_cache, _logger); var startDate = new DateOnly(2024, 1, 2); var endDate = new DateOnly(2025, 1, 1); // Act var sessions = await service.GetTradingSessionsAsync(startDate, endDate, CancellationToken.None); // Assert // Typical: 250–252 trading days per year (accounting for holidays) Assert.InRange(sessions.Count, 245, 260); } private sealed class NoOpLogger : ILogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => false; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { } } }