feat(Backend): update API gateway and websocket hubs
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user