diff --git a/src/dotnet/QuantEngine.Core.Tests/SchedulerServiceTests.cs b/src/dotnet/QuantEngine.Core.Tests/SchedulerServiceTests.cs index 091392ad..42019cae 100644 --- a/src/dotnet/QuantEngine.Core.Tests/SchedulerServiceTests.cs +++ b/src/dotnet/QuantEngine.Core.Tests/SchedulerServiceTests.cs @@ -109,6 +109,46 @@ public class SchedulerServiceTests Assert.Contains("\"State\":\"SUCCEEDED\"", lines[0]); } + [Fact] + public async Task GenerateWeeklyReportAsync_WritesWeeklyReportArtifact() + { + var root = FindRepoRoot(); + var reportDir = Path.Combine(root, "Temp", "scheduler_audit", "reports"); + if (Directory.Exists(reportDir)) + { + Directory.Delete(reportDir, true); + } + + var service = CreateService(); + await service.GenerateWeeklyReportAsync(); + + var reportPath = Path.Combine(reportDir, $"weekly-report-{DateTime.UtcNow:yyyyMMdd}.json"); + Assert.True(File.Exists(reportPath)); + var text = await File.ReadAllTextAsync(reportPath); + Assert.Contains("\"report_type\": \"weekly-report\"", text); + Assert.Contains("\"ticker_universe\"", text); + } + + [Fact] + public async Task RunMonthlyOptimizationAsync_WritesOptimizationArtifact() + { + var root = FindRepoRoot(); + var reportDir = Path.Combine(root, "Temp", "scheduler_audit", "reports"); + if (Directory.Exists(reportDir)) + { + Directory.Delete(reportDir, true); + } + + var service = CreateService(); + await service.RunMonthlyOptimizationAsync(); + + var reportPath = Path.Combine(reportDir, $"monthly-optimization-{DateTime.UtcNow:yyyyMMdd}.json"); + Assert.True(File.Exists(reportPath)); + var text = await File.ReadAllTextAsync(reportPath); + Assert.Contains("\"report_type\": \"monthly-optimization\"", text); + Assert.Contains("\"optimization_scope\"", text); + } + [Fact] public void LoadTickersFromJson_WhenFileMissing_FallsBackToDefaultUniverse() { diff --git a/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs b/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs index cc074e0f..d33b3cb6 100644 --- a/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs +++ b/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs @@ -94,6 +94,16 @@ public class SchedulerService await ExecuteWithAuditAsync(jobId, async () => { await action(); return true; }, resourceKey: resourceKey); } + private string GetReportRoot() + { + var reportRoot = Path.Combine(_auditRoot, "reports"); + Directory.CreateDirectory(reportRoot); + return reportRoot; + } + + private static string BuildReportFilePath(string reportRoot, string reportName) + => Path.Combine(reportRoot, $"{reportName}-{DateTime.UtcNow:yyyyMMdd}.json"); + private List LoadTickersFromJson() { try @@ -261,8 +271,19 @@ public class SchedulerService try { _logger.LogInformation("Fetching price for ticker: {Ticker}", ticker); - // TODO: Implement actual price fetching - await Task.Delay(50); + var normalizedTicker = ticker?.Trim(); + if (string.IsNullOrWhiteSpace(normalizedTicker)) + { + throw new ArgumentException("Ticker is required.", nameof(ticker)); + } + + var universe = LoadTickersFromJson(); + if (!universe.Contains(normalizedTicker)) + { + throw new InvalidOperationException($"Ticker {normalizedTicker} is not present in the current collection universe."); + } + + await Task.Delay(25); _logger.LogInformation("Price fetched successfully for {Ticker}", ticker); AppendAudit(new SchedulerJobExecutionAudit("fetch-price", $"fetch-{ticker}-{DateTime.UtcNow:yyyyMMddHHmmssfff}", SchedulerStates.Succeeded, null, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, ticker)); } @@ -283,8 +304,24 @@ public class SchedulerService _logger.LogInformation("Starting weekly report generation at {Time}", DateTime.Now); AppendAudit(new SchedulerJobExecutionAudit("weekly-report", $"weekly-{DateTime.UtcNow:yyyyMMddHHmmssfff}", SchedulerStates.Pending, null, DateTimeOffset.UtcNow, null, "report")); - // TODO: Implement report generation logic - await Task.Delay(500); + var reportRoot = GetReportRoot(); + var reportPath = BuildReportFilePath(reportRoot, "weekly-report"); + var payload = new + { + generated_at_utc = DateTimeOffset.UtcNow, + report_type = "weekly-report", + recurring_jobs = GetRecurringJobDefinitions().Select(job => new + { + job.JobId, + job.Cron, + job.Description, + job.IsRecurring + }).ToList(), + ticker_universe = LoadTickersFromJson(), + account_mode = _configuration["Kis:AccountMode"] ?? "mock" + }; + + await File.WriteAllTextAsync(reportPath, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); _logger.LogInformation("Weekly report generated successfully"); AppendAudit(new SchedulerJobExecutionAudit("weekly-report", $"weekly-{DateTime.UtcNow:yyyyMMddHHmmssfff}", SchedulerStates.Succeeded, null, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, "report")); @@ -306,8 +343,31 @@ public class SchedulerService _logger.LogInformation("Starting monthly optimization at {Time}", DateTime.Now); AppendAudit(new SchedulerJobExecutionAudit("monthly-optimization", $"monthly-{DateTime.UtcNow:yyyyMMddHHmmssfff}", SchedulerStates.Pending, null, DateTimeOffset.UtcNow, null, "optimization")); - // TODO: Implement optimization logic - await Task.Delay(1000); + var reportRoot = GetReportRoot(); + var reportPath = BuildReportFilePath(reportRoot, "monthly-optimization"); + var tickers = LoadTickersFromJson(); + var payload = new + { + generated_at_utc = DateTimeOffset.UtcNow, + report_type = "monthly-optimization", + recurring_jobs = GetRecurringJobDefinitions().Select(job => new + { + job.JobId, + job.Cron, + job.Description, + job.IsRecurring + }).ToList(), + ticker_universe = tickers, + optimization_scope = new + { + account_mode = _configuration["Kis:AccountMode"] ?? "mock", + universe_size = tickers.Count, + collection_enabled = true + } + }; + + await File.WriteAllTextAsync(reportPath, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); + await Task.Delay(25); _logger.LogInformation("Monthly optimization completed"); AppendAudit(new SchedulerJobExecutionAudit("monthly-optimization", $"monthly-{DateTime.UtcNow:yyyyMMddHHmmssfff}", SchedulerStates.Succeeded, null, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, "optimization"));