From 4eb23a41cae30d0c939eb3e7e3fe911b8fda1afd Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 12 Jul 2026 13:17:37 +0900 Subject: [PATCH] feat(web): implement market-close caching mechanism for collection data --- .../Services/KisDataCollectionOrchestrator.cs | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) 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; + } }