Files
Finlytic/FinlyticBackend/Controllers/AnalyzeController.cs
T

193 lines
7.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using FinlyticBackend.Util;
using FinlyticCore.Dtos;
using FinlyticCore.Dtos.Fundamentals;
using FinlyticCore.Dtos.Sentiment;
using FinlyticCore.Dtos.TechnicalAnalysis;
using FinlyticCore.Models.Analyzer;
using FinlyticCore.Models.Trades;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace FinlyticBackend.Controllers;
/// <summary>
/// Request payload for triggering manual analysis (AOT-compliant).
/// </summary>
public record AnalyzeRequest(
[property: JsonPropertyName("symbol")] string? Symbol,
[property: JsonPropertyName("isin")] string? Isin,
[property: JsonPropertyName("sector")] string? Sector,
[property: JsonPropertyName("headline")] string? Headline,
[property: JsonPropertyName("currentPrice")] decimal? CurrentPrice,
[property: JsonPropertyName("riskScore")] int? RiskScore,
[property: JsonPropertyName("minTimeframeValue")] int? MinTimeframeValue,
[property: JsonPropertyName("maxTimeframeValue")] int? MaxTimeframeValue,
[property: JsonPropertyName("timeframeUnit")] string? TimeframeUnit,
[property: JsonPropertyName("instrumentType")] string? InstrumentType,
[property: JsonPropertyName("userNotes")] string? UserNotes
);
[ApiController]
[Authorize]
[Route("api/v1/analyze")]
[EnableCors("AllowAll")]
public class AnalyzeController : ControllerBase
{
private readonly WebMqttClient _mqttClient;
private readonly ILogger<AnalyzeController> _logger;
public AnalyzeController(WebMqttClient mqttClient, ILogger<AnalyzeController> logger)
{
_mqttClient = mqttClient;
_logger = logger;
}
/// <summary>
/// Triggers a manual analysis for an asset by gathering context in parallel and dispatching to FinlyticAnalyzer.
/// </summary>
[HttpPost("manual")]
public async Task<IActionResult> TriggerManualAnalysis([FromBody] AnalyzeRequest request)
{
try
{
if (string.IsNullOrWhiteSpace(request?.Isin) && string.IsNullOrWhiteSpace(request?.Symbol))
{
return BadRequest(new { error = "ISIN or Symbol is required" });
}
if (!_mqttClient.IsConnected)
{
return StatusCode(503, new { error = "Analysis service is currently unavailable." });
}
string targetIsin = (request.Isin ?? request.Symbol ?? string.Empty).Trim().ToUpperInvariant();
string targetSymbol = (request.Symbol ?? request.Isin ?? string.Empty).Trim().ToUpperInvariant();
var isinReq = new IsinRequest(targetIsin);
// 1. Parallelisiertes Context-Gathering (TA, Fundamentals, Sentiment) für minimale Latenz
var taTask = FetchTaDataAsync(isinReq);
var fundTask = FetchFundamentalsDataAsync(isinReq);
var sentTask = FetchSentimentDataAsync(isinReq);
await Task.WhenAll(taTask, fundTask, sentTask);
var taData = await taTask;
var fundData = await fundTask;
var sentData = await sentTask;
decimal lastCandlePrice = taData?.Candles != null && taData.Candles.Count > 0 ? (decimal)taData.Candles.Last().Close : 0m;
decimal resolvedPrice = request.CurrentPrice ??
(lastCandlePrice > 0 ? lastCandlePrice :
(fundData != null && fundData.CurrentPrice > 0 ? fundData.CurrentPrice : 100.0m));
var rpcRequest = new ManualAnalysisRpcRequest(
Isin: targetIsin,
Symbol: targetSymbol,
Sector: !string.IsNullOrWhiteSpace(request.Sector) ? request.Sector : "Technology",
Headline: !string.IsNullOrWhiteSpace(request.Headline) ? request.Headline : "Manual Analysis Triggered by User",
CurrentPrice: resolvedPrice,
RiskScore: request.RiskScore ?? 50,
MinTimeframeValue: request.MinTimeframeValue ?? 4,
MaxTimeframeValue: request.MaxTimeframeValue ?? 6,
TimeframeUnit: !string.IsNullOrWhiteSpace(request.TimeframeUnit) ? request.TimeframeUnit : "Tage",
InstrumentType: !string.IsNullOrWhiteSpace(request.InstrumentType) ? request.InstrumentType : "Stock",
UserNotes: request.UserNotes ?? string.Empty,
TaData: taData,
FundamentalsData: fundData,
SentimentData: sentData
);
// 2. Ausführen des RPC Triggers am FinlyticAnalyzer (mit typisierter Response)
try
{
var response = await _mqttClient.SendRpcRequestAsync<ManualAnalysisResponseDto, ManualAnalysisRpcRequest>(
"analyzer_TriggerManual", rpcRequest, TimeSpan.FromSeconds(10));
if (response != null)
{
return Ok(response);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[AnalyzeController] RPC analyzer_TriggerManual timed out or failed for ISIN '{Isin}'.", targetIsin);
}
return Ok(new
{
status = "AnalysisTriggered",
isin = rpcRequest.Isin,
message = "Manuelle KI-Analyse wurde gestartet, verarbeitet Ergebnisse im Hintergrund."
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to trigger manual analysis via MQTT RPC.");
return StatusCode(500, new { error = "Internal server error triggering analysis." });
}
}
/// <summary>
/// Fetches the currently active trade proposals from the FinlyticTrades service.
/// </summary>
[HttpGet("proposals")]
public async Task<IActionResult> GetActiveProposals()
{
try
{
if (!_mqttClient.IsConnected)
{
return StatusCode(503, new { error = "Analysis service is currently unavailable." });
}
// AOT-sicherer RPC-Aufruf an FinlyticTrades für vorgeschlagene Trades
var request = new GetTradesRequest(Isin: null, Status: "Proposed", UserId: null);
var proposals = await _mqttClient.SendRpcRequestAsync<List<TradeProposalDto>, GetTradesRequest>(
"trades_Get", request, TimeSpan.FromSeconds(4));
return Ok(proposals ?? new List<TradeProposalDto>());
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch active proposals via MQTT RPC.");
return StatusCode(500, new { error = "Internal server error fetching proposals." });
}
}
private async Task<TechnicalAnalysisDto?> FetchTaDataAsync(IsinRequest request)
{
try
{
return await _mqttClient.SendRpcRequestAsync<TechnicalAnalysisDto, IsinRequest>(
"ta_GetAnalysis", request, TimeSpan.FromSeconds(2));
}
catch { return null; }
}
private async Task<AssetFundamentalsDto?> FetchFundamentalsDataAsync(IsinRequest request)
{
try
{
return await _mqttClient.SendRpcRequestAsync<AssetFundamentalsDto, IsinRequest>(
"fundamentals_Get", request, TimeSpan.FromSeconds(2));
}
catch { return null; }
}
private async Task<IsinSentimentSummaryDto?> FetchSentimentDataAsync(IsinRequest request)
{
try
{
return await _mqttClient.SendRpcRequestAsync<IsinSentimentSummaryDto, IsinRequest>(
"sentiment_GetIsin", request, TimeSpan.FromSeconds(2));
}
catch { return null; }
}
}