From bea5462c5eba2dd404d156eb7a0667589d89b8fa Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 13 Jul 2026 01:15:25 +0900 Subject: [PATCH] feat(dotnet): add collection bootstrap hosted service --- .../QuantEngine.Application.csproj | 1 + .../CollectionBootstrapHostedService.cs | 110 ++++++++++++++++++ .../CollectionBootstrapHostedServiceTests.cs | 45 +++++++ src/dotnet/QuantEngine.Web/Program.cs | 1 + 4 files changed, 157 insertions(+) create mode 100644 src/dotnet/QuantEngine.Application/Services/CollectionBootstrapHostedService.cs create mode 100644 src/dotnet/QuantEngine.Core.Tests/CollectionBootstrapHostedServiceTests.cs diff --git a/src/dotnet/QuantEngine.Application/QuantEngine.Application.csproj b/src/dotnet/QuantEngine.Application/QuantEngine.Application.csproj index f9926a95..6cd0ba63 100644 --- a/src/dotnet/QuantEngine.Application/QuantEngine.Application.csproj +++ b/src/dotnet/QuantEngine.Application/QuantEngine.Application.csproj @@ -6,6 +6,7 @@ + diff --git a/src/dotnet/QuantEngine.Application/Services/CollectionBootstrapHostedService.cs b/src/dotnet/QuantEngine.Application/Services/CollectionBootstrapHostedService.cs new file mode 100644 index 00000000..7578f6e2 --- /dev/null +++ b/src/dotnet/QuantEngine.Application/Services/CollectionBootstrapHostedService.cs @@ -0,0 +1,110 @@ +using System.Text.Json; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace QuantEngine.Application.Services; + +/// +/// Lightweight startup bootstrap for collection scheduling/readiness. +/// Writes a deterministic artifact so deployment can verify the collection +/// pipeline entry point without forcing a live collection run. +/// +public sealed class CollectionBootstrapHostedService : IHostedService +{ + private readonly ILogger _logger; + private readonly GatherTradingDataParser _parser; + + public CollectionBootstrapHostedService( + ILogger logger, + GatherTradingDataParser parser) + { + _logger = logger; + _parser = parser; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + try + { + var repoRoot = FindRepoRoot(); + var outputPath = Path.Combine(repoRoot, "Temp", "collection_bootstrap_v1.json"); + var tickers = LoadBootstrapTickers(); + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); + File.WriteAllText(outputPath, JsonSerializer.Serialize(new + { + gate = "PASS", + generated_at_utc = DateTimeOffset.UtcNow, + bootstrap = "collection-scheduling-ready", + ticker_count = tickers.Count, + tickers + }, new JsonSerializerOptions { WriteIndented = true })); + + _logger.LogInformation("Collection bootstrap artifact written to {Path}", outputPath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Collection bootstrap artifact generation failed"); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private List LoadBootstrapTickers() + { + try + { + var jsonPath = FindGatherTradingDataJson(); + if (jsonPath is null) + { + return ["005930"]; + } + + var data = _parser.ParseGatherTradingData(jsonPath); + return data + .Select(row => row.TryGetValue("Ticker", out var value) ? value?.ToString()?.Trim('"') : null) + .Where(ticker => !string.IsNullOrWhiteSpace(ticker)) + .Distinct() + .Take(10) + .ToList()!; + } + catch + { + return ["005930"]; + } + } + + private static string FindRepoRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current != null) + { + if (Directory.Exists(Path.Combine(current.FullName, ".git"))) + { + return current.FullName; + } + current = current.Parent; + } + + return Directory.GetCurrentDirectory(); + } + + private static string? FindGatherTradingDataJson() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current != null) + { + var candidate = Path.Combine(current.FullName, "GatherTradingData.json"); + if (File.Exists(candidate)) + { + return candidate; + } + + current = current.Parent; + } + + return null; + } +} diff --git a/src/dotnet/QuantEngine.Core.Tests/CollectionBootstrapHostedServiceTests.cs b/src/dotnet/QuantEngine.Core.Tests/CollectionBootstrapHostedServiceTests.cs new file mode 100644 index 00000000..ff4c4182 --- /dev/null +++ b/src/dotnet/QuantEngine.Core.Tests/CollectionBootstrapHostedServiceTests.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Logging; +using Moq; +using QuantEngine.Application.Services; + +namespace QuantEngine.Core.Tests; + +public class CollectionBootstrapHostedServiceTests +{ + [Fact] + public async Task StartAsync_WritesBootstrapArtifact() + { + var root = FindRepoRoot(); + var artifact = Path.Combine(root, "Temp", "collection_bootstrap_v1.json"); + if (File.Exists(artifact)) + { + File.Delete(artifact); + } + + var service = new CollectionBootstrapHostedService( + new Mock>().Object, + new GatherTradingDataParser()); + + await service.StartAsync(CancellationToken.None); + + Assert.True(File.Exists(artifact)); + var text = await File.ReadAllTextAsync(artifact); + Assert.Contains("\"gate\": \"PASS\"", text); + Assert.Contains("\"bootstrap\": \"collection-scheduling-ready\"", text); + } + + private static string FindRepoRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current != null) + { + if (Directory.Exists(Path.Combine(current.FullName, ".git"))) + { + return current.FullName; + } + current = current.Parent; + } + + throw new InvalidOperationException("Repository root not found."); + } +} diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index 02130ba2..894bc6bb 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -122,6 +122,7 @@ try builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddOptions(); + builder.Services.AddHostedService(); // Hangfire Background Jobs try