using KArtSell.Modules.ModelOperations.Domain; using Xunit; namespace KArtSell.ModelOperations.UnitTests; public class VS03_MarketDataPolicyTests { [Fact] public void ValidatePrice_ValidPrice_ReturnsPass() { var price = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: 70000m, HighPrice: 71000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 1000000, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); Assert.True(result.IsValid); Assert.Empty(result.Errors); Assert.True(result.QualityScore >= 85); } [Fact] public void ValidatePrice_NegativePrice_ReturnsFail() { var price = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: -100m, HighPrice: 71000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 1000000, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); Assert.False(result.IsValid); Assert.Contains("Open price must be > 0", result.Errors); } [Fact] public void ValidatePrice_HighLowerThanLow_ReturnsFail() { var price = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: 70000m, HighPrice: 68000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 1000000, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); Assert.False(result.IsValid); Assert.Contains("High must be >= Low", result.Errors); } [Fact] public void ValidatePrice_FutureDate_ReturnsFail() { var price = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)), OpenPrice: 70000m, HighPrice: 71000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 1000000, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); Assert.False(result.IsValid); Assert.Contains("cannot be in the future", result.Errors[0]); } [Fact] public void ValidatePrice_ZeroVolume_LowersScore() { var price = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: 70000m, HighPrice: 71000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 0, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var result = MarketDataPolicy.ValidatePrice(price, DateOnly.FromDateTime(DateTime.UtcNow)); Assert.True(result.IsValid); Assert.True(result.QualityScore < 80); // Quality degraded but still valid } [Fact] public void IsDuplicate_IdenticalPrice_ReturnsTrue() { var price1 = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: 70000m, HighPrice: 71000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 1000000, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var price2 = price1 with { PriceId = Guid.NewGuid(), Revision = 2 }; var isDuplicate = MarketDataPolicy.IsDuplicate(price2, new List { price1 }); Assert.True(isDuplicate); } [Fact] public void IsDuplicate_DifferentSymbol_ReturnsFalse() { var price1 = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: 70000m, HighPrice: 71000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 1000000, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var price2 = price1 with { PriceId = Guid.NewGuid(), Symbol = "000660" }; var isDuplicate = MarketDataPolicy.IsDuplicate(price2, new List { price1 }); Assert.False(isDuplicate); } [Fact] public void NormalizePrice_ValidPrice_RoundsTo2Decimals() { var price = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: 70000.123m, HighPrice: 71000.456m, LowPrice: 69000.789m, ClosePrice: 70500.999m, Volume: 1000000, PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var normalized = MarketDataPolicy.NormalizePrice(price); Assert.NotNull(normalized); Assert.Equal(70000.12m, normalized!.OpenPrice); Assert.Equal(71000.46m, normalized.HighPrice); } [Fact] public void NormalizePrice_LowVolume_ReturnsNull() { var price = new DailyPrice( PriceId: Guid.NewGuid(), Symbol: "005930", TradingDate: DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-1)), OpenPrice: 70000m, HighPrice: 71000m, LowPrice: 69000m, ClosePrice: 70500m, Volume: 50, // Suspiciously low PublishedAt: DateTime.UtcNow, Revision: 1, DataSource: "KRX", CorrelationId: "test-123"); var normalized = MarketDataPolicy.NormalizePrice(price); Assert.Null(normalized); } [Fact] public void ClassifyQualityIssue_HighScore_ReturnsAccept() { var result = new ValidationResult(IsValid: true, Errors: new(), QualityScore: 95); var decision = MarketDataPolicy.ClassifyQualityIssue(result); Assert.Equal(DataQualityDecision.Accept, decision); } [Fact] public void ClassifyQualityIssue_MediumScore_ReturnsAcceptWithWarning() { var result = new ValidationResult(IsValid: true, Errors: new(), QualityScore: 75); var decision = MarketDataPolicy.ClassifyQualityIssue(result); Assert.Equal(DataQualityDecision.AcceptWithWarning, decision); } [Fact] public void ClassifyQualityIssue_LowScore_ReturnsQuarantine() { var result = new ValidationResult(IsValid: true, Errors: new(), QualityScore: 60); var decision = MarketDataPolicy.ClassifyQualityIssue(result); Assert.Equal(DataQualityDecision.Quarantine, decision); } }