172 lines
7.7 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|