247 lines
8.6 KiB
C#
247 lines
8.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using FinlyticBackend.Util;
|
|
using FinlyticCore.Dtos;
|
|
using FinlyticCore.Dtos.News;
|
|
using FinlyticCore.Dtos.Sentiment;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Cors;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace FinlyticBackend.Controllers;
|
|
|
|
[ApiController]
|
|
[Authorize]
|
|
[Route("api/v1/news")]
|
|
public class NewsController : ControllerBase
|
|
{
|
|
private readonly WebMqttClient _mqttClient;
|
|
private readonly ILogger<NewsController> _logger;
|
|
|
|
public NewsController(WebMqttClient mqttClient, ILogger<NewsController> logger)
|
|
{
|
|
_mqttClient = mqttClient;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves paginated news articles with optional filters and enriched sentiment data.
|
|
/// </summary>
|
|
[HttpGet]
|
|
public async Task<IActionResult> GetNews(
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 20,
|
|
[FromQuery] string? symbol = null,
|
|
[FromQuery] string? isin = null,
|
|
[FromQuery] string? date = null,
|
|
[FromQuery] string? status = null,
|
|
[FromQuery] string? query = null,
|
|
[FromQuery] bool? hasSentiment = null)
|
|
{
|
|
string? activeSymbol = !string.IsNullOrWhiteSpace(symbol) ? symbol.Trim() : isin?.Trim();
|
|
string? effectiveStatus = status;
|
|
|
|
if (hasSentiment == true && string.IsNullOrEmpty(effectiveStatus))
|
|
{
|
|
effectiveStatus = "Analyzed";
|
|
}
|
|
|
|
string? dateStr = !string.IsNullOrWhiteSpace(date) ? date.Trim() : null;
|
|
if (string.Equals(dateStr, "today", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
dateStr = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (_mqttClient.IsConnected)
|
|
{
|
|
var payload = new DailyNewsRequest(
|
|
Limit: pageSize,
|
|
Offset: (page - 1) * pageSize,
|
|
Isin: activeSymbol,
|
|
Date: dateStr,
|
|
Status: effectiveStatus,
|
|
Query: query,
|
|
HasSentiment: hasSentiment
|
|
);
|
|
|
|
var articles = await _mqttClient.SendRpcRequestAsync<List<NewsArticleDto>, DailyNewsRequest>(
|
|
"news_Get",
|
|
payload,
|
|
TimeSpan.FromSeconds(10)
|
|
);
|
|
|
|
if (articles != null && articles.Count > 0)
|
|
{
|
|
// Parallelisierte Anreichung fehlender Sentiment-Daten
|
|
if (hasSentiment == true || string.Equals(effectiveStatus, "Analyzed", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var enrichmentTasks = articles.Select(async article =>
|
|
{
|
|
if (article.FinbertResult != null)
|
|
{
|
|
return article;
|
|
}
|
|
|
|
try
|
|
{
|
|
var sentimentEntry = await _mqttClient.SendRpcRequestAsync<IsinAnalysisEntry, ArticleRequest>(
|
|
"sentiment_GetArticle",
|
|
new ArticleRequest(article.Id.ToString(), article.Id.ToString()),
|
|
TimeSpan.FromSeconds(2)
|
|
);
|
|
|
|
if (sentimentEntry?.FinbertResult != null)
|
|
{
|
|
var result = sentimentEntry.FinbertResult;
|
|
return article with
|
|
{
|
|
Sentiment = result.Label,
|
|
SentimentScore = result.CompoundScore,
|
|
Confidence = result.Confidence,
|
|
FinbertResult = result
|
|
};
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// RPC Timeout oder nicht analysiert -> Un-angereicherten Artikel zurückgeben
|
|
}
|
|
|
|
return article;
|
|
});
|
|
|
|
var enrichedArticles = await Task.WhenAll(enrichmentTasks);
|
|
return Ok(enrichedArticles);
|
|
}
|
|
|
|
return Ok(articles);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("[GetNews] Cannot send MQTT RPC - MQTT client is NOT connected to broker.");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "[GetNews] MQTT RPC news_Get request failed for symbol '{Symbol}'", activeSymbol);
|
|
}
|
|
|
|
return Ok(new List<NewsArticleDto>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves sentiment summary for a specific ISIN.
|
|
/// </summary>
|
|
[HttpGet("sentiment/isin/{isin}")]
|
|
public async Task<IActionResult> GetIsinSentimentSummary(string isin)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(isin)) return NotFound(new { message = "ISIN is required." });
|
|
|
|
var isinCode = isin.Trim().ToUpperInvariant();
|
|
|
|
try
|
|
{
|
|
if (_mqttClient.IsConnected)
|
|
{
|
|
var rpcResult = await _mqttClient.SendRpcRequestAsync<IsinSentimentSummaryDto, IsinRequest>(
|
|
"sentiment_GetIsin",
|
|
new IsinRequest(isinCode),
|
|
TimeSpan.FromSeconds(5)
|
|
);
|
|
|
|
if (rpcResult != null)
|
|
{
|
|
return Ok(rpcResult);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "MQTT RPC sentiment_GetIsin failed for ISIN '{Isin}'", isinCode);
|
|
}
|
|
|
|
// File-System Fallback
|
|
var candidates = new[]
|
|
{
|
|
Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "isin", $"{isinCode}.json"),
|
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "isin", $"{isinCode}.json"),
|
|
Path.Combine(Directory.GetCurrentDirectory(), "..", "data", "summaries", "isin", $"{isinCode}.json")
|
|
};
|
|
|
|
foreach (var path in candidates)
|
|
{
|
|
if (System.IO.File.Exists(path))
|
|
{
|
|
try
|
|
{
|
|
var json = await System.IO.File.ReadAllTextAsync(path);
|
|
return Content(json, "application/json");
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
return NotFound(new { message = $"No sentiment summary found for ISIN {isinCode}." });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves sentiment analysis for a specific article.
|
|
/// </summary>
|
|
[HttpGet("sentiment/article/{articleId}")]
|
|
public async Task<IActionResult> GetArticleSentimentAnalysis(string articleId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(articleId)) return NotFound(new { message = "ArticleId is required." });
|
|
|
|
var targetId = articleId.Trim();
|
|
|
|
try
|
|
{
|
|
if (_mqttClient.IsConnected)
|
|
{
|
|
var rpcResult = await _mqttClient.SendRpcRequestAsync<IsinAnalysisEntry, ArticleRequest>(
|
|
"sentiment_GetArticle",
|
|
new ArticleRequest(targetId, targetId),
|
|
TimeSpan.FromSeconds(5)
|
|
);
|
|
|
|
if (rpcResult != null)
|
|
{
|
|
return Ok(rpcResult);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "MQTT RPC sentiment_GetArticle failed for articleId '{ArticleId}'", targetId);
|
|
}
|
|
|
|
// File-System Fallback
|
|
var articleCandidates = new[]
|
|
{
|
|
Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "articles", $"{targetId}.json"),
|
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "summaries", "articles", $"{targetId}.json")
|
|
};
|
|
|
|
foreach (var path in articleCandidates)
|
|
{
|
|
if (System.IO.File.Exists(path))
|
|
{
|
|
try
|
|
{
|
|
var json = await System.IO.File.ReadAllTextAsync(path);
|
|
return Content(json, "application/json");
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
return NotFound(new { message = $"No sentiment analysis found for article {articleId}." });
|
|
}
|
|
} |