using FastEndpoints; using Hangfire; using QuantEngine.Application.Interfaces; using QuantEngine.Core.Interfaces; namespace QuantEngine.Web.Endpoints; public class GetCollectionStateEndpoint : EndpointWithoutRequest { private readonly ICollectionReadModelService _readModelService; public GetCollectionStateEndpoint(ICollectionReadModelService readModelService) { _readModelService = readModelService; } public override void Configure() { Get("/api/collection/state"); AllowAnonymous(); Description(d => d .Produces(200) .Produces(500)); } public override async Task HandleAsync(CancellationToken ct) { try { var state = await _readModelService.GetDashboardStateAsync(); await SendOkAsync(state, ct); } catch { await SendErrorsAsync(500, ct); } } } public class GetRecentRunsRequest { public int Limit { get; set; } = 20; } public class GetRecentRunsResponse { public List Runs { get; set; } = new(); public int Count { get; set; } } public class GetRecentRunsEndpoint : Endpoint { private readonly ICollectionReadModelService _readModelService; public GetRecentRunsEndpoint(ICollectionReadModelService readModelService) { _readModelService = readModelService; } public override void Configure() { Get("/api/collection/runs"); AllowAnonymous(); Description(d => d .Produces(200) .Produces(500)); } public override async Task HandleAsync(GetRecentRunsRequest req, CancellationToken ct) { try { var runs = await _readModelService.GetRecentRunsAsync(req.Limit); await SendOkAsync(new GetRecentRunsResponse { Runs = runs, Count = runs.Count }, ct); } catch { await SendErrorsAsync(500, ct); } } } public class GetRunSnapshotsRequest { public string RunId { get; set; } = ""; } public class GetRunSnapshotsResponse { public string RunId { get; set; } = ""; public List Snapshots { get; set; } = new(); public int Count { get; set; } } public class GetRunSnapshotsEndpoint : Endpoint { private readonly ICollectionReadModelService _readModelService; public GetRunSnapshotsEndpoint(ICollectionReadModelService readModelService) { _readModelService = readModelService; } public override void Configure() { Get("/api/collection/runs/{RunId}/snapshots"); AllowAnonymous(); Description(d => d .Produces(200) .Produces(404) .Produces(500)); } public override async Task HandleAsync(GetRunSnapshotsRequest req, CancellationToken ct) { try { var snapshots = await _readModelService.GetRunSnapshotsAsync(req.RunId); await SendOkAsync(new GetRunSnapshotsResponse { RunId = req.RunId, Snapshots = snapshots, Count = snapshots.Count }, ct); } catch { await SendErrorsAsync(500, ct); } } } public class GetRunErrorsRequest { public string RunId { get; set; } = ""; public int Limit { get; set; } = 50; } public class GetRunErrorsResponse { public string RunId { get; set; } = ""; public List Errors { get; set; } = new(); public int Count { get; set; } } public class GetRunErrorsEndpoint : Endpoint { private readonly ICollectionReadModelService _readModelService; public GetRunErrorsEndpoint(ICollectionReadModelService readModelService) { _readModelService = readModelService; } public override void Configure() { Get("/api/collection/runs/{RunId}/errors"); AllowAnonymous(); Description(d => d .Produces(200) .Produces(404) .Produces(500)); } public override async Task HandleAsync(GetRunErrorsRequest req, CancellationToken ct) { try { var errors = await _readModelService.GetRunErrorsAsync(req.RunId, req.Limit); await SendOkAsync(new GetRunErrorsResponse { RunId = req.RunId, Errors = errors, Count = errors.Count }, ct); } catch { await SendErrorsAsync(500, ct); } } } public class GetLatestSnapshotsRequest { public string Ticker { get; set; } = ""; public int Limit { get; set; } = 10; } public class GetLatestSnapshotsResponse { public string Ticker { get; set; } = ""; public List Snapshots { get; set; } = new(); public int Count { get; set; } } public class GetLatestSnapshotsEndpoint : Endpoint { private readonly ICollectionReadModelService _readModelService; public GetLatestSnapshotsEndpoint(ICollectionReadModelService readModelService) { _readModelService = readModelService; } public override void Configure() { Get("/api/collection/latest/{Ticker}"); AllowAnonymous(); Description(d => d .Produces(200) .Produces(500)); } public override async Task HandleAsync(GetLatestSnapshotsRequest req, CancellationToken ct) { try { var snapshots = await _readModelService.GetLatestSnapshotsForTickerAsync(req.Ticker, req.Limit); await SendOkAsync(new GetLatestSnapshotsResponse { Ticker = req.Ticker, Snapshots = snapshots, Count = snapshots.Count }, ct); } catch { await SendErrorsAsync(500, ct); } } } public class GetPriceHistorySummaryResponse { public List Tickers { get; set; } = new(); } public class GetPriceHistorySummaryEndpoint : EndpointWithoutRequest { private readonly ICollectionReadModelService _readModelService; private readonly ILogger _logger; public GetPriceHistorySummaryEndpoint(ICollectionReadModelService readModelService, ILogger logger) { _readModelService = readModelService; _logger = logger; } public override void Configure() { Get("/api/collection/history-summary"); AllowAnonymous(); Description(d => d .Produces(200) .Produces(500)); } public override async Task HandleAsync(CancellationToken ct) { try { var summary = await _readModelService.GetPriceHistorySummaryAsync(); await SendOkAsync(new GetPriceHistorySummaryResponse { Tickers = summary }, ct); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to fetch price history summary"); await SendErrorsAsync(500, ct); } } } public class StartCollectionRunResponse { public string RunId { get; set; } = ""; } public class StartCollectionRunEndpoint : EndpointWithoutRequest { private readonly IBackgroundJobClient _jobClient; private readonly IConfiguration _configuration; private readonly ILogger _logger; public StartCollectionRunEndpoint( IBackgroundJobClient jobClient, IConfiguration configuration, ILogger logger) { _jobClient = jobClient; _configuration = configuration; _logger = logger; } public override void Configure() { Post("/api/collection/run"); Description(d => d .Produces(202) .Produces(500)); } public override async Task HandleAsync(CancellationToken ct) { try { var runId = $"api-{DateTime.Now:yyyyMMdd-HHmmss}"; var accountMode = _configuration["Kis:AccountMode"] ?? "mock"; var tickers = new[] { "005930", "000660", "051910", "005380", "010140", "005490" }.ToList(); _jobClient.Enqueue(o => o.RunCollectionAsync(runId, accountMode, tickers)); _logger.LogInformation("Collection run {RunId} enqueued", runId); await SendAsync(new StartCollectionRunResponse { RunId = runId }, 202, ct); } catch { await SendErrorsAsync(500, ct); } } } public class FactorVersionDto { public string VersionId { get; set; } = string.Empty; public string CreatedAt { get; set; } = string.Empty; public string Status { get; set; } = "ACTIVE"; public string Description { get; set; } = string.Empty; } public class GetFactorVersionsResponse { public List Versions { get; set; } = new(); } public class GetFactorVersionsEndpoint : EndpointWithoutRequest { public override void Configure() { Get("/api/factors/versions"); AllowAnonymous(); Description(d => d.Produces(200)); } public override async Task HandleAsync(CancellationToken ct) { var versions = new List { new() { VersionId = "FACTOR-V4.2", CreatedAt = DateTime.UtcNow.AddDays(-2).ToString("yyyy-MM-dd"), Status = "ACTIVE", Description = "최신 팩터 산출 공식 V4.2" }, new() { VersionId = "FACTOR-V4.1", CreatedAt = DateTime.UtcNow.AddDays(-12).ToString("yyyy-MM-dd"), Status = "ARCHIVED", Description = "이전 보정 공식 V4.1" }, new() { VersionId = "FACTOR-V4.0", CreatedAt = DateTime.UtcNow.AddDays(-30).ToString("yyyy-MM-dd"), Status = "ARCHIVED", Description = "기초 팩터 공식 V4.0" } }; await SendOkAsync(new GetFactorVersionsResponse { Versions = versions }, ct); } }