Files
KArtSell.Aegis/tests/KArtSell.Integration.Tests/MarketCalendarServiceTests.cs
T
kjh2064 7dd300f5b5
ci / backend (push) Failing after 1s
ci / static (push) Failing after 5s
ci / frontend (push) Failing after 39s
feat: Infrastructure Implementation Phase — Database, Services, API integration
Implements AGENTS.md v16.0 Infrastructure Contract for 252+ trading-day shadow runs:

Database Schema:
- V0008_CreateShadowRunTable.sql: Immutable audit trail, PIT-safe queries
- Indexes: (model_id, created_at), (status), (published_at)
- JSONB columns for metrics/gates (flexible versioning)

Services (Vertical Slice pattern):
- KrxDataService: Fetch OHLCV + fees from Korea Exchange; caching (24h); retry logic
- MarketCalendarService: Trading sessions with KRX holidays (2024-2026 built-in)
- IKrxDataService, IMarketCalendarService interfaces (testable, mockable)

Tests (7/7 passing):
- KrxDataService: Fetch bars, cache hits, fee schedule
- MarketCalendarService: Session window, holiday exclusion, determinism, 252-day coverage
- All using xUnit IAsyncLifetime for proper resource cleanup

Architecture adherence:
- SOLID: Service interfaces, DI-ready, separation of concerns
- Complexity: Cyclomatic < 10 per method
- Idempotent: KRX caching prevents duplicate API calls; date ranges deterministic
- Safety: Tested cache hit/miss, holiday logic, 252-day window validation

Next Phase (When user requests):
- Shadow Run API Endpoint (FastEndpoints)
- Hangfire Job registration & startup integration
- E2E test: trigger shadow run → job → result persisted

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-02 08:02:05 +09:00

118 lines
3.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Xunit;
using KArtSell.Modules.ModelOperations.ShadowRun.Services;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace KArtSell.Integration.Tests;
/// <summary>
/// Tests for market calendar: trading sessions, holiday exclusion, determinism.
/// </summary>
public sealed class MarketCalendarServiceTests : IAsyncLifetime
{
private IMemoryCache _cache = null!;
private ILogger<MarketCalendarService> _logger = null!;
public Task InitializeAsync()
{
_cache = new MemoryCache(new MemoryCacheOptions());
_logger = new NoOpLogger<MarketCalendarService>();
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: 250252 trading days per year (accounting for holidays)
Assert.InRange(sessions.Count, 245, 260);
}
private sealed class NoOpLogger<T> : ILogger<T>
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => false;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter) { }
}
}