namespace QuantEngine.Infrastructure.Validators; using QuantEngine.Core.Validators; using System.Data; using System.Linq; /// /// 데이터 품질 검증 구현 (5-포인트) /// PostgreSQL 기반 검증 로직 /// public class DataQualityValidator : IDataQualityValidator { private readonly IDbConnection _connection; private const string SchemaName = "quantengine"; public DataQualityValidator(IDbConnection connection) { _connection = connection ?? throw new ArgumentNullException(nameof(connection)); } public async Task ValidateAsync( int stockId, DateTime start, DateTime end) { var completeness = await CheckCompletenessAsync(stockId, start, end); var freshness = await CheckFreshnessAsync(stockId); var consistency = await CheckConsistencyAsync(stockId, start, end); var outliers = await CheckOutliersAsync(stockId, start, end); var duplicates = await CheckDuplicatesAsync(stockId, start, end); return new DataQualityReport { StockId = stockId, EvaluatedAt = DateTime.UtcNow, Completeness = completeness, Freshness = freshness, Consistency = consistency, Outliers = outliers, Duplicates = duplicates, }; } public async Task CheckCompletenessAsync( int stockId, DateTime start, DateTime end) { // 거래일만 고려 (주말/휴장일 제외) var sql = $@" SELECT COUNT(DISTINCT DATE(recorded_at))::int as actual_records FROM {SchemaName}.kis_collection_snapshots WHERE stock_id = @stock_id AND recorded_at >= @start AND recorded_at < @end AND EXTRACT(dow FROM recorded_at) NOT IN (0, 6) "; var result = new CompletenessCheckResult { IsValid = true, Score = 1.0, MissingDates = new(), Message = "No missing data detected", }; // TODO: 실제 구현 (거래일 조회, 결측 계산) return result; } public async Task CheckFreshnessAsync(int stockId) { var sql = $@" SELECT MAX(recorded_at) as latest_record FROM {SchemaName}.kis_collection_snapshots WHERE stock_id = @stock_id "; var latestTime = DateTime.UtcNow.AddDays(-1); // 예시 var staleness = DateTime.UtcNow - latestTime; return new FreshnessCheckResult { IsValid = staleness.TotalHours < 25, Score = Math.Max(0, 1.0 - (staleness.TotalHours / 24.0)), LatestRecordTime = latestTime, StalenessAge = staleness, Message = $"Latest data: {staleness.TotalHours:F1}h ago", }; } public async Task CheckConsistencyAsync( int stockId, DateTime start, DateTime end) { var violations = new List(); // 규칙 1: high >= close >= low >= 0 var sql = $@" SELECT id, recorded_at, high_price, close_price, low_price FROM {SchemaName}.kis_collection_snapshots WHERE stock_id = @stock_id AND recorded_at >= @start AND recorded_at < @end AND (high_price < close_price OR close_price < low_price OR low_price < 0) "; // TODO: 실제 구현 return new ConsistencyCheckResult { IsValid = !violations.Any(), Score = 1.0 - (violations.Count / 100.0), Violations = violations, Message = violations.Any() ? $"Found {violations.Count} consistency violations" : "No consistency violations", }; } public async Task CheckOutliersAsync( int stockId, DateTime start, DateTime end, double stdDevThreshold = 3.0) { // Z-score 기반 이상치 감지 var sql = $@" WITH stats AS ( SELECT AVG(close_price) as mean_price, STDDEV_POP(close_price) as std_price FROM {SchemaName}.kis_collection_snapshots WHERE stock_id = @stock_id AND recorded_at >= @start AND recorded_at < @end ) SELECT id, recorded_at, close_price, ABS((close_price - stats.mean_price) / NULLIF(stats.std_price, 0)) as z_score FROM {SchemaName}.kis_collection_snapshots, stats WHERE stock_id = @stock_id AND recorded_at >= @start AND recorded_at < @end AND ABS((close_price - stats.mean_price) / NULLIF(stats.std_price, 0)) > @threshold "; return new OutlierCheckResult { IsValid = true, Score = 1.0, Outliers = new(), Message = "No outliers detected", }; } public async Task CheckDuplicatesAsync( int stockId, DateTime start, DateTime end) { var sql = $@" SELECT array_agg(id) as ids, recorded_at, COUNT(*) as dup_count FROM {SchemaName}.kis_collection_snapshots WHERE stock_id = @stock_id AND recorded_at >= @start AND recorded_at < @end GROUP BY recorded_at, close_price, volume HAVING COUNT(*) > 1 "; return new DuplicateCheckResult { IsValid = true, Score = 1.0, Duplicates = new(), Message = "No duplicates detected", }; } }