Files
Finlytic/FinlyticBackend/Controllers/AdminEvaluationHistoryController.cs

172 lines
7.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FinlyticBackend.Util;
using FinlyticCore.Dtos.TechnicalAnalysis;
using FinlyticCore.Dtos.Trading;
using FinlyticCore.Util;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace FinlyticBackend.Controllers;
/// <summary>
/// Admin-only Web UI endpoint over FinlyticEngine's persisted evaluation history
/// (<c>engine_evaluation_snapshots</c>), answering "why hasn't a new trade proposal appeared" by exposing every
/// evaluation the engine ever ran - approved or not, automatic or manual - with the real, already-computed
/// scores/gates/outcome for each. Mirrors the <see cref="AdminUserController"/>/<see cref="AdminSettingsController"/>
/// pattern: <c>[Authorize(Roles = "Admin")]</c> at the controller level, a thin pass-through to FinlyticEngine
/// over MQTT RPC, and <see cref="ControllerBase.Problem"/> (never an anonymous object) on failure.
/// </summary>
[ApiController]
[Route("api/v1/admin/evaluations")]
[Authorize(Roles = "Admin")]
[EnableCors("AllowAll")]
public class AdminEvaluationHistoryController : ControllerBase
{
private readonly WebMqttClient _mqttClient;
private readonly ILogger<AdminEvaluationHistoryController> _logger;
public AdminEvaluationHistoryController(WebMqttClient mqttClient, ILogger<AdminEvaluationHistoryController> logger)
{
_mqttClient = mqttClient;
_logger = logger;
}
/// <summary>
/// Returns a filtered, paginated page of evaluation-history rows plus a summary of the same (unpaginated)
/// filtered set - see <see cref="GetEvaluationHistoryRequest"/>/<see cref="GetEvaluationHistoryResponse"/>
/// for the exact filter and aggregation semantics. All query parameters are optional; omitting a filter
/// means "do not restrict on this field".
/// </summary>
/// <param name="fromUtc">Inclusive lower bound on <c>EvaluatedAtUtc</c>.</param>
/// <param name="toUtc">Inclusive upper bound on <c>EvaluatedAtUtc</c>.</param>
/// <param name="outcome">Restricts results to a single <see cref="OutcomeReason"/>.</param>
/// <param name="triggerSource">Restricts results to a single <see cref="TriggerSource"/>.</param>
/// <param name="search">Case-sensitive substring search against both ISIN and Symbol.</param>
/// <param name="page">1-based page number (defaults to 1; values below 1 are treated as 1 server-side).</param>
/// <param name="pageSize">
/// Requested page size (defaults to 50; server-side clamped to at most 200 by FinlyticEngine to prevent an
/// unbounded response).
/// </param>
[HttpGet]
public async Task<IActionResult> GetEvaluationHistory(
[FromQuery] DateTime? fromUtc = null,
[FromQuery] DateTime? toUtc = null,
[FromQuery] OutcomeReason? outcome = null,
[FromQuery] TriggerSource? triggerSource = null,
[FromQuery] string? search = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 50)
{
if (!_mqttClient.IsConnected)
{
return Problem(title: "FinlyticEngine is currently unreachable.", statusCode: StatusCodes.Status503ServiceUnavailable);
}
try
{
var request = new GetEvaluationHistoryRequest(
FromUtc: fromUtc,
ToUtc: toUtc,
OutcomeFilter: outcome,
TriggerSourceFilter: triggerSource,
IsinOrSymbolSearch: search,
Page: page,
PageSize: pageSize
);
var result = await _mqttClient.SendRpcRequestAsync<GetEvaluationHistoryResponse, GetEvaluationHistoryRequest>(
MqttTopics.Channels.EngineGetEvaluationHistory, request, TimeSpan.FromSeconds(10));
if (result == null)
{
// Null means the RPC call itself got no response (transport failure) - not a legitimate "zero
// results" outcome, which is always a populated response with an empty Entries list.
return Problem(title: "FinlyticEngine did not respond to the evaluation history request.", statusCode: StatusCodes.Status502BadGateway);
}
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "[AdminEvaluationHistory] Failed to fetch evaluation history via MQTT RPC.");
return Problem(title: "Internal server error fetching evaluation history.", statusCode: StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// Returns FinlyticTechnicals' currently monitored scan universe ("watchlist") - the assets the background
/// scanner is actively evaluating every cycle, independent of whether any of them have cleared the
/// engine's opportunity-poller score threshold yet.
/// </summary>
[HttpGet("watchlist")]
public async Task<IActionResult> GetWatchlist()
{
if (!_mqttClient.IsConnected)
{
return Problem(title: "FinlyticTechnicals is currently unreachable.", statusCode: StatusCodes.Status503ServiceUnavailable);
}
try
{
var result = await _mqttClient.SendRpcRequestAsync<List<WatchlistEntryDto>, object>(
MqttTopics.Channels.TaGetWatchlist, new object(), TimeSpan.FromSeconds(10));
if (result == null)
{
return Problem(title: "FinlyticTechnicals did not respond to the watchlist request.", statusCode: StatusCodes.Status502BadGateway);
}
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "[AdminEvaluationHistory] Failed to fetch watchlist via MQTT RPC.");
return Problem(title: "Internal server error fetching the watchlist.", statusCode: StatusCodes.Status500InternalServerError);
}
}
/// <summary>
/// Returns the last <paramref name="limit"/> technical-analysis setups computed for <paramref name="isin"/>
/// (most recent first), so the admin UI can show whether its quality score is trending up or down across
/// recent scan cycles - including setups too weak to ever have reached the engine.
/// </summary>
[HttpGet("watchlist/{isin}/history")]
public async Task<IActionResult> GetWatchlistEntryHistory([FromRoute] string isin, [FromQuery] int limit = 8)
{
if (string.IsNullOrWhiteSpace(isin))
{
return BadRequest(new { message = "Eine gültige ISIN ist erforderlich." });
}
if (!_mqttClient.IsConnected)
{
return Problem(title: "FinlyticTechnicals is currently unreachable.", statusCode: StatusCodes.Status503ServiceUnavailable);
}
try
{
var result = await _mqttClient.SendRpcRequestAsync<List<StrategyResultDto>, GetRecentSetupHistoryRequest>(
MqttTopics.Channels.TaGetRecentSetupHistory,
new GetRecentSetupHistoryRequest(isin.Trim().ToUpperInvariant(), limit),
TimeSpan.FromSeconds(10));
if (result == null)
{
return Problem(title: "FinlyticTechnicals did not respond to the setup history request.", statusCode: StatusCodes.Status502BadGateway);
}
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "[AdminEvaluationHistory] Failed to fetch setup history for ISIN '{Isin}' via MQTT RPC.", isin);
return Problem(title: "Internal server error fetching the setup history.", statusCode: StatusCodes.Status500InternalServerError);
}
}
}