SoftTraderBackend/ServiceHandler/NewsSentimentService.cs
2026-03-20 10:42:48 -04:00

112 lines
3.1 KiB
C#

using Backend.DatabaseHandler;
using Backend.Interface;
using System.Security.Cryptography;
using System.Text;
using System.Xml.Linq;
namespace Backend.ServiceHander
{
public sealed class NewsSentimentService
{
private readonly INewsSentimentStore _store;
private readonly FinBertScoringService _scorer;
private static readonly HttpClient Http = new()
{
DefaultRequestHeaders = { { "User-Agent", "SoftTrader/1.0" } }
};
public NewsSentimentService(INewsSentimentStore store, FinBertScoringService scorer)
{
_store = store;
_scorer = scorer;
}
public async Task<List<NewsSentimentRecord>> AnalyzeAsync(
string keyword,
CancellationToken cancellationToken = default)
{
keyword = keyword.Trim();
if (string.IsNullOrEmpty(keyword))
throw new ArgumentException("Keyword cannot be empty.");
var articles = await FetchRssAsync(keyword, cancellationToken);
var existingHashes = await _store.GetExistingHashesAsync(keyword, cancellationToken);
var newRecords = new List<NewsSentimentRecord>();
foreach (var (title, url, publishedAt) in articles)
{
string titleHash = ComputeHash(title);
if (existingHashes.Contains(titleHash))
continue;
var (headline, publisher) = ParsePublisher(title);
var (score, label) = _scorer.Score(headline);
newRecords.Add(new NewsSentimentRecord
{
Source = "google_rss",
Keyword = keyword,
PublishedAt = publishedAt,
Title = headline,
Score = score,
SentimentLabel = label,
TitleHash = titleHash,
Publisher = publisher,
Url = url
});
}
if (newRecords.Count > 0)
await _store.InsertAsync(newRecords, cancellationToken);
return await _store.GetByKeywordAsync(keyword, cancellationToken);
}
private static async Task<List<(string Title, string Url, DateTimeOffset PublishedAt)>> FetchRssAsync(
string keyword,
CancellationToken cancellationToken)
{
string encodedKeyword = Uri.EscapeDataString(keyword);
string url = $"https://news.google.com/rss/search?q={encodedKeyword}&hl=en-US&gl=US&ceid=US:en";
string xml = await Http.GetStringAsync(url, cancellationToken);
var doc = XDocument.Parse(xml);
var items = new List<(string, string, DateTimeOffset)>();
foreach (var item in doc.Descendants("item"))
{
string? title = item.Element("title")?.Value;
string? link = item.Element("link")?.Value;
string? pubDate = item.Element("pubDate")?.Value;
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(link))
continue;
DateTimeOffset published = DateTimeOffset.TryParse(pubDate, out var dt)
? dt
: DateTimeOffset.UtcNow;
items.Add((title, link, published));
}
return items;
}
private static (string Headline, string Publisher) ParsePublisher(string title)
{
int idx = title.LastIndexOf(" - ");
if (idx < 0)
return (title, "Unknown");
return (title[..idx].Trim(), title[(idx + 3)..].Trim());
}
private static string ComputeHash(string text)
{
byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
return Convert.ToHexStringLower(bytes);
}
}
}