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);
}
}
}