diff --git a/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs b/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs index 6b3f6764..c6da4209 100644 --- a/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs +++ b/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs @@ -56,26 +56,53 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator try { _logger.LogInformation("Collecting ticker {Ticker} (run {RunId})", ticker, runId); - var kisResult = await kisSource.GetPriceDataAsync(ticker, account); - var seedRow = new Dictionary { { "Ticker", ticker } }; - var (normalized, provenance) = _normalizer.NormalizeCollectionRow(seedRow, kisResult, null, false); + CollectionSnapshotRecord? cachedSnapshot = null; + if (IsMarketClosed()) + { + var latest = await _repository.GetLatestSnapshotsForTickerAsync(ticker, 1); + var todayPrefix = DateTime.UtcNow.AddHours(9).ToString("yyyy-MM-dd"); + if (latest.Count > 0 && latest[0].CapturedAt.StartsWith(todayPrefix)) + { + cachedSnapshot = latest[0]; + } + } + + Dictionary normalized; + string sourceName; + + if (cachedSnapshot != null) + { + _logger.LogInformation("Cache hit for ticker {Ticker} (run {RunId})", ticker, runId); + normalized = JsonSerializer.Deserialize>(cachedSnapshot.PayloadJson) + ?? new Dictionary(); + sourceName = cachedSnapshot.SourceName.EndsWith(" (Cached)") + ? cachedSnapshot.SourceName + : cachedSnapshot.SourceName + " (Cached)"; + } + else + { + var kisResult = await kisSource.GetPriceDataAsync(ticker, account); + var seedRow = new Dictionary { { "Ticker", ticker } }; + var (norm, provenance) = _normalizer.NormalizeCollectionRow(seedRow, kisResult, null, false); + normalized = norm; + sourceName = (string)(provenance.GetValueOrDefault("source") ?? "kis_open_api"); + } // Save to DB await _repository.SaveSnapshotAsync(new CollectionSnapshotRecord( RunId: runId, DatasetName: "data_feed", Ticker: ticker, - SourceName: (string)(provenance.GetValueOrDefault("source") ?? "kis_open_api"), + SourceName: sourceName, PayloadJson: JsonSerializer.Serialize(normalized), CapturedAt: DataNormalizationHelper.KstNowIso() )); // Track source - var source = (string)(provenance.GetValueOrDefault("source") ?? "kis_open_api"); - if (!sourceCounts.ContainsKey(source)) - sourceCounts[source] = 0; - sourceCounts[source]++; + if (!sourceCounts.ContainsKey(sourceName)) + sourceCounts[sourceName] = 0; + sourceCounts[sourceName]++; rows.Add(normalized); result.SuccessCount++; @@ -176,6 +203,23 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator return Path.Combine(Path.GetTempPath(), "kis_dotnet_collection_v1.json"); } + + private static bool IsMarketClosed() + { + // KST Time conversion (UTC+9) + var kst = DateTime.UtcNow.AddHours(9); + + // Weekend check + if (kst.DayOfWeek == DayOfWeek.Saturday || kst.DayOfWeek == DayOfWeek.Sunday) + return true; + + // Market hours check (09:00 - 15:30) + var time = kst.TimeOfDay; + if (time < new TimeSpan(9, 0, 0) || time > new TimeSpan(15, 30, 0)) + return true; + + return false; + } }