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; /// /// Admin-only Web UI endpoint over FinlyticEngine's persisted evaluation history /// (engine_evaluation_snapshots), 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 / /// pattern: [Authorize(Roles = "Admin")] at the controller level, a thin pass-through to FinlyticEngine /// over MQTT RPC, and (never an anonymous object) on failure. /// [ApiController] [Route("api/v1/admin/evaluations")] [Authorize(Roles = "Admin")] [EnableCors("AllowAll")] public class AdminEvaluationHistoryController : ControllerBase { private readonly WebMqttClient _mqttClient; private readonly ILogger _logger; public AdminEvaluationHistoryController(WebMqttClient mqttClient, ILogger logger) { _mqttClient = mqttClient; _logger = logger; } /// /// Returns a filtered, paginated page of evaluation-history rows plus a summary of the same (unpaginated) /// filtered set - see / /// for the exact filter and aggregation semantics. All query parameters are optional; omitting a filter /// means "do not restrict on this field". /// /// Inclusive lower bound on EvaluatedAtUtc. /// Inclusive upper bound on EvaluatedAtUtc. /// Restricts results to a single . /// Restricts results to a single . /// Case-sensitive substring search against both ISIN and Symbol. /// 1-based page number (defaults to 1; values below 1 are treated as 1 server-side). /// /// Requested page size (defaults to 50; server-side clamped to at most 200 by FinlyticEngine to prevent an /// unbounded response). /// [HttpGet] public async Task 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( 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); } } /// /// 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. /// [HttpGet("watchlist")] public async Task GetWatchlist() { if (!_mqttClient.IsConnected) { return Problem(title: "FinlyticTechnicals is currently unreachable.", statusCode: StatusCodes.Status503ServiceUnavailable); } try { var result = await _mqttClient.SendRpcRequestAsync, 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); } } /// /// Returns the last technical-analysis setups computed for /// (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. /// [HttpGet("watchlist/{isin}/history")] public async Task 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, 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); } } }