From 49637f1b02a7de887e9587d2a2e59b9fb5dde450 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 12 Jul 2026 11:55:17 +0900 Subject: [PATCH] feat: export PostgreSQL training dataset as JSON --- .../Services/LearningDatasetService.cs | 29 +++++++++++++++++++ .../Interfaces/ILearningDatasetReader.cs | 6 ++++ .../Repositories/LearningDatasetReader.cs | 24 +++++++++++++++ src/dotnet/QuantEngine.Web/Program.cs | 2 ++ 4 files changed, 61 insertions(+) create mode 100644 src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs create mode 100644 src/dotnet/QuantEngine.Core/Interfaces/ILearningDatasetReader.cs create mode 100644 src/dotnet/QuantEngine.Infrastructure/Repositories/LearningDatasetReader.cs diff --git a/src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs b/src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs new file mode 100644 index 00000000..c98383bc --- /dev/null +++ b/src/dotnet/QuantEngine.Application/Services/LearningDatasetService.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using QuantEngine.Core.Interfaces; + +namespace QuantEngine.Application.Services; + +public sealed class LearningDatasetService +{ + private readonly ILearningDatasetReader _reader; + + public LearningDatasetService(ILearningDatasetReader reader) => _reader = reader; + + public async Task ExportJsonAsync(string outputPath, int limit = 1000) + { + var rows = await _reader.ReadTrainingExamplesAsync(limit); + var payload = new + { + formula_id = "ENGINE_HISTORY_TRAINING_DATASET_V1", + gate = rows.Count > 0 ? "PASS" : "DATA_MISSING", + generated_at = DateTimeOffset.UtcNow, + sample_count = rows.Count, + source = "engine_history.training_example_v1", + rows + }; + var path = Path.GetFullPath(outputPath); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + await File.WriteAllTextAsync(path, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); + return path; + } +} diff --git a/src/dotnet/QuantEngine.Core/Interfaces/ILearningDatasetReader.cs b/src/dotnet/QuantEngine.Core/Interfaces/ILearningDatasetReader.cs new file mode 100644 index 00000000..ce719868 --- /dev/null +++ b/src/dotnet/QuantEngine.Core/Interfaces/ILearningDatasetReader.cs @@ -0,0 +1,6 @@ +namespace QuantEngine.Core.Interfaces; + +public interface ILearningDatasetReader +{ + Task>> ReadTrainingExamplesAsync(int limit = 1000); +} diff --git a/src/dotnet/QuantEngine.Infrastructure/Repositories/LearningDatasetReader.cs b/src/dotnet/QuantEngine.Infrastructure/Repositories/LearningDatasetReader.cs new file mode 100644 index 00000000..1e29a03f --- /dev/null +++ b/src/dotnet/QuantEngine.Infrastructure/Repositories/LearningDatasetReader.cs @@ -0,0 +1,24 @@ +using Dapper; +using QuantEngine.Core.Interfaces; +using QuantEngine.Infrastructure.Data; + +namespace QuantEngine.Infrastructure.Repositories; + +public sealed class LearningDatasetReader : ILearningDatasetReader +{ + private readonly IDbConnectionFactory _connectionFactory; + + public LearningDatasetReader(IDbConnectionFactory connectionFactory) => _connectionFactory = connectionFactory; + + public async Task>> ReadTrainingExamplesAsync(int limit = 1000) + { + if (limit is < 1 or > 10000) + throw new ArgumentOutOfRangeException(nameof(limit)); + + using var connection = _connectionFactory.CreateConnection(); + var rows = await connection.QueryAsync( + "SELECT * FROM engine_history.training_example_v1 ORDER BY decided_at DESC LIMIT @Limit", + new { Limit = limit }); + return rows.Select(row => (IDictionary)row).ToList(); + } +} diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index 19de1372..ec2b9ce7 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -97,7 +97,9 @@ try builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped();