diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs
index e24c274..154a8a6 100644
--- a/src/dotnet/QuantEngine.Web/Program.cs
+++ b/src/dotnet/QuantEngine.Web/Program.cs
@@ -21,6 +21,9 @@ using Microsoft.AspNetCore.Authentication;
using System.Text.Encodings.Web;
using Microsoft.Extensions.Options;
using MudBlazor.Services;
+using QuantEngine.Web.Services;
+using Hangfire;
+using Hangfire.SqlServer;
// Serilog Configuration with Telegram Sink
Log.Logger = new LoggerConfiguration()
@@ -47,6 +50,17 @@ builder.Services.AddAuthorizationCore();
builder.Services.AddMudServices();
+// Hangfire Background Job Scheduling
+try
+{
+ var hangfireConnectionString = builder.Configuration.GetConnectionString("HangfireConnection") ?? connectionString;
+ builder.Services.AddHangfireServices(hangfireConnectionString);
+}
+catch (Exception ex)
+{
+ Log.Warning("Hangfire initialization failed: {Message}", ex.Message);
+}
+
// PostgreSQL Dapper Setup
var configuredConnectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var fallbackConnectionString = "Host=127.0.0.1;Database=quantenginedb;Username=quantengine_app;Password=CHANGE_ME;Search Path=quantengine;";
@@ -143,6 +157,16 @@ app.UseAntiforgery();
app.UseAuthentication();
app.UseAuthorization();
+// Initialize Hangfire (dashboard and schedules)
+try
+{
+ app.UseHangfireSetup(app.Services);
+}
+catch (Exception ex)
+{
+ Log.Warning("Hangfire setup failed: {Message}", ex.Message);
+}
+
app.MapStaticAssets();
app.MapGet("/", () => Results.Redirect("/login"));
diff --git a/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs b/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs
new file mode 100644
index 0000000..70a102d
--- /dev/null
+++ b/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs
@@ -0,0 +1,294 @@
+using Hangfire;
+using QuantEngine.Application.Services;
+using QuantEngine.Infrastructure.Data;
+
+namespace QuantEngine.Web.Services;
+
+///
+/// Scheduler Service for managing background jobs with Hangfire
+///
+public class SchedulerService
+{
+ private readonly ILogger _logger;
+ private readonly IBackgroundJobClient _jobClient;
+ private readonly IRecurringJobManager _recurringJobManager;
+ private readonly IKisApiPriceSource _kisApi;
+
+ public SchedulerService(
+ ILogger logger,
+ IBackgroundJobClient jobClient,
+ IRecurringJobManager recurringJobManager,
+ IKisApiPriceSource kisApi)
+ {
+ _logger = logger;
+ _jobClient = jobClient;
+ _recurringJobManager = recurringJobManager;
+ _kisApi = kisApi;
+ }
+
+ ///
+ /// Initialize scheduled jobs
+ ///
+ public void InitializeSchedules()
+ {
+ try
+ {
+ _logger.LogInformation("Initializing Hangfire schedules...");
+
+ // Daily data collection at 9:00 AM
+ _recurringJobManager.AddOrUpdate(
+ "daily-collection",
+ () => RunDailyCollectionAsync(),
+ "0 9 * * *", // Every day at 9:00 AM
+ new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }
+ );
+
+ // Hourly price update (during market hours 9 AM - 4 PM)
+ _recurringJobManager.AddOrUpdate(
+ "hourly-price-update",
+ () => UpdatePricesAsync(),
+ "0 9-15 * * 1-5", // Every hour, 9 AM to 3 PM, Mon-Fri
+ new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }
+ );
+
+ // Weekly report generation (Friday at 5:00 PM)
+ _recurringJobManager.AddOrUpdate(
+ "weekly-report",
+ () => GenerateWeeklyReportAsync(),
+ "0 17 * * 5", // Every Friday at 5:00 PM
+ new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }
+ );
+
+ // Monthly optimization (First day of month at 2:00 AM)
+ _recurringJobManager.AddOrUpdate(
+ "monthly-optimization",
+ () => RunMonthlyOptimizationAsync(),
+ "0 2 1 * *", // First day of month at 2:00 AM
+ new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }
+ );
+
+ _logger.LogInformation("Hangfire schedules initialized successfully");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error initializing Hangfire schedules");
+ }
+ }
+
+ ///
+ /// Run daily data collection
+ ///
+ public async Task RunDailyCollectionAsync()
+ {
+ try
+ {
+ _logger.LogInformation("Starting daily data collection job at {Time}", DateTime.Now);
+
+ // List of tickers to collect
+ var tickers = new[] { "005930", "000660", "051910", "005380", "010140", "005490" };
+
+ foreach (var ticker in tickers)
+ {
+ // Simulate data collection
+ await Task.Delay(100);
+ _logger.LogInformation("Collected data for ticker: {Ticker}", ticker);
+ }
+
+ _logger.LogInformation("Daily data collection completed at {Time}", DateTime.Now);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during daily collection");
+ }
+ }
+
+ ///
+ /// Update prices hourly
+ ///
+ public async Task UpdatePricesAsync()
+ {
+ try
+ {
+ _logger.LogInformation("Starting hourly price update at {Time}", DateTime.Now);
+
+ var tickers = new[] { "005930", "000660", "051910" };
+
+ foreach (var ticker in tickers)
+ {
+ try
+ {
+ // Enqueue price update as background job
+ _jobClient.Enqueue(() => FetchPriceAsync(ticker));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Failed to enqueue price update for {Ticker}", ticker);
+ }
+ }
+
+ _logger.LogInformation("Hourly price update completed");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during price update");
+ }
+ }
+
+ ///
+ /// Fetch price for specific ticker
+ ///
+ public async Task FetchPriceAsync(string ticker)
+ {
+ try
+ {
+ _logger.LogInformation("Fetching price for ticker: {Ticker}", ticker);
+ // TODO: Implement actual price fetching
+ await Task.Delay(50);
+ _logger.LogInformation("Price fetched successfully for {Ticker}", ticker);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error fetching price for {Ticker}", ticker);
+ }
+ }
+
+ ///
+ /// Generate weekly report
+ ///
+ public async Task GenerateWeeklyReportAsync()
+ {
+ try
+ {
+ _logger.LogInformation("Starting weekly report generation at {Time}", DateTime.Now);
+
+ // TODO: Implement report generation logic
+ await Task.Delay(500);
+
+ _logger.LogInformation("Weekly report generated successfully");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error generating weekly report");
+ }
+ }
+
+ ///
+ /// Run monthly optimization
+ ///
+ public async Task RunMonthlyOptimizationAsync()
+ {
+ try
+ {
+ _logger.LogInformation("Starting monthly optimization at {Time}", DateTime.Now);
+
+ // TODO: Implement optimization logic
+ await Task.Delay(1000);
+
+ _logger.LogInformation("Monthly optimization completed");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during monthly optimization");
+ }
+ }
+
+ ///
+ /// Enqueue one-time job
+ ///
+ public string EnqueueJob(string jobName, Func job)
+ {
+ var jobId = _jobClient.Enqueue(job);
+ _logger.LogInformation("Enqueued job {JobName} with ID {JobId}", jobName, jobId);
+ return jobId;
+ }
+
+ ///
+ /// Get job status
+ ///
+ public JobState GetJobStatus(string jobId)
+ {
+ return JobStorage.Current.GetConnection().GetJobData(jobId)?.State;
+ }
+
+ ///
+ /// Cancel scheduled job
+ ///
+ public void CancelScheduledJob(string jobName)
+ {
+ _recurringJobManager.RemoveIfExists(jobName);
+ _logger.LogInformation("Cancelled scheduled job: {JobName}", jobName);
+ }
+}
+
+///
+/// Extension methods for Hangfire registration
+///
+public static class HangfireServiceExtensions
+{
+ ///
+ /// Register Hangfire with SQL Server storage
+ ///
+ public static IServiceCollection AddHangfireServices(
+ this IServiceCollection services,
+ string connectionString)
+ {
+ // Add Hangfire services
+ services.AddHangfire(configuration => configuration
+ .SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
+ .UseSimpleAssemblyNameTypeSerializer()
+ .UseRecommendedSerializerSettings()
+ .UseSqlServerStorage(connectionString, new SqlServerStorageOptions
+ {
+ CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
+ SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
+ QueuePollInterval = TimeSpan.FromSeconds(15),
+ UsePageLocks = true,
+ DisableGlobalLocks = true
+ }));
+
+ // Add Hangfire server
+ services.AddHangfireServer(options =>
+ {
+ options.WorkerCount = Environment.ProcessorCount * 2;
+ options.Queues = new[] { "default" };
+ });
+
+ // Register scheduler service
+ services.AddScoped();
+
+ return services;
+ }
+
+ ///
+ /// Use Hangfire dashboard and initialize schedules
+ ///
+ public static IApplicationBuilder UseHangfireSetup(
+ this IApplicationBuilder app,
+ IServiceProvider serviceProvider)
+ {
+ // Use Hangfire Dashboard
+ app.UseHangfireDashboard("/hangfire", new DashboardOptions
+ {
+ Authorization = new[] { new HangfireAuthorizationFilter() }
+ });
+
+ // Initialize schedules
+ var schedulerService = serviceProvider.GetRequiredService();
+ schedulerService.InitializeSchedules();
+
+ return app;
+ }
+}
+
+///
+/// Simple authorization filter for Hangfire Dashboard
+///
+public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
+{
+ public bool Authorize(DashboardContext context)
+ {
+ // TODO: Implement proper authorization check
+ // For now, allow all in development
+ return true;
+ }
+}