Integrated news headline functions.
This commit is contained in:
parent
7f8d6a8131
commit
f85779fde8
@ -15,6 +15,7 @@ builder.Services.AddSingleton<IUserInterestStore, PostgresUserInterestStore>();
|
||||
builder.Services.AddSingleton<FinBertScoringService>();
|
||||
builder.Services.AddSingleton<INewsSentimentStore, PostgresNewsSentimentStore>();
|
||||
builder.Services.AddSingleton<NewsSentimentService>();
|
||||
builder.Services.AddHostedService<NewsSentimentRefreshService>();
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using Backend.Interface;
|
||||
using Backend.ServiceHander;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -7,11 +8,20 @@ namespace Backend.Controller
|
||||
[Route("api/news-sentiment")]
|
||||
public sealed class NewsSentimentController : ControllerBase
|
||||
{
|
||||
private static DateTime _lastRefresh = DateTime.MinValue;
|
||||
private static readonly Lock _refreshLock = new();
|
||||
private readonly NewsSentimentService _service;
|
||||
private readonly IUserInterestStore _interests;
|
||||
private readonly INewsSentimentStore _store;
|
||||
|
||||
public NewsSentimentController(NewsSentimentService service)
|
||||
public NewsSentimentController(
|
||||
NewsSentimentService service,
|
||||
IUserInterestStore interests,
|
||||
INewsSentimentStore store)
|
||||
{
|
||||
_service = service;
|
||||
_interests = interests;
|
||||
_store = store;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -30,6 +40,7 @@ namespace Backend.Controller
|
||||
title = r.Title,
|
||||
score = r.Score,
|
||||
sentimentLabel = r.SentimentLabel,
|
||||
publisher = r.Publisher,
|
||||
url = r.Url
|
||||
}));
|
||||
}
|
||||
@ -42,5 +53,91 @@ namespace Backend.Controller
|
||||
return StatusCode(500, new { error = ex.ToString() });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("feed")]
|
||||
public async Task<IActionResult> Feed(
|
||||
[FromQuery] string user,
|
||||
[FromQuery] string? keyword = null,
|
||||
[FromQuery(Name = "request-amount")] int requestAmount = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(user))
|
||||
return BadRequest("User cannot be empty.");
|
||||
|
||||
if (requestAmount < 1)
|
||||
requestAmount = 1;
|
||||
|
||||
List<string> keywords;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
keywords = [keyword.Trim()];
|
||||
}
|
||||
else
|
||||
{
|
||||
var symbols = await _interests.FetchAllAsync(user, "symbol");
|
||||
var topics = await _interests.FetchAllAsync(user, "topic");
|
||||
keywords = symbols.Concat(topics).Distinct().ToList();
|
||||
}
|
||||
|
||||
if (keywords.Count == 0)
|
||||
return Ok(Array.Empty<object>());
|
||||
|
||||
var results = await _store.GetFeedAsync(keywords, requestAmount, cancellationToken);
|
||||
|
||||
return Ok(results.Select(r => new
|
||||
{
|
||||
title = r.Title,
|
||||
publisher = r.Publisher,
|
||||
score = r.Score,
|
||||
publishedAt = r.PublishedAt.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
||||
url = r.Url
|
||||
}));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { error = ex.ToString() });
|
||||
}
|
||||
}
|
||||
[HttpGet("refresh")]
|
||||
public async Task<ActionResult<BoolResponse>> Refresh(
|
||||
[FromQuery] string user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(user))
|
||||
return BadRequest("User cannot be empty.");
|
||||
|
||||
// 5 second refresh cooldown - abuse protection
|
||||
lock (_refreshLock)
|
||||
{
|
||||
if ((DateTime.UtcNow - _lastRefresh).TotalSeconds < 5)
|
||||
return Ok(new BoolResponse { Success = false });
|
||||
_lastRefresh = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
var symbols = await _interests.FetchAllAsync(user, "symbol");
|
||||
var topics = await _interests.FetchAllAsync(user, "topic");
|
||||
var keywords = symbols.Concat(topics).Distinct().ToList();
|
||||
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
await _service.AnalyzeAsync(keyword, cancellationToken);
|
||||
}
|
||||
|
||||
return Ok(new BoolResponse { Success = true });
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return Ok(new BoolResponse { Success = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,13 +35,15 @@ namespace Backend.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("add")]
|
||||
[HttpGet("add")]
|
||||
public async Task<ActionResult<BoolResponse>> Add(
|
||||
[FromBody] UserInterestModifyRequest request)
|
||||
[FromQuery] string user,
|
||||
[FromQuery] string type,
|
||||
[FromQuery] string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool success = await _store.AddAsync(request.User, request.Type, request.Value);
|
||||
bool success = await _store.AddAsync(user, type, value);
|
||||
return Ok(new BoolResponse { Success = success });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
@ -50,13 +52,15 @@ namespace Backend.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("remove")]
|
||||
[HttpGet("remove")]
|
||||
public async Task<ActionResult<BoolResponse>> Remove(
|
||||
[FromBody] UserInterestModifyRequest request)
|
||||
[FromQuery] string user,
|
||||
[FromQuery] string type,
|
||||
[FromQuery] string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool success = await _store.RemoveAsync(request.User, request.Type, request.Value);
|
||||
bool success = await _store.RemoveAsync(user, type, value);
|
||||
return Ok(new BoolResponse { Success = success });
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
|
||||
@ -94,11 +94,14 @@ Console.WriteLine(raw);
|
||||
// Intraday fraction: (6.5h × 5 days) / (24h × 7 days) ≈ 0.194
|
||||
// Daily/weekly fraction (weekdays only): 5 / 7 ≈ 0.714
|
||||
// Monthly bars already map 1:1 to calendar months, no adjustment needed.
|
||||
// NYSE has ~252 trading days per year (weekdays minus ~10 holidays).
|
||||
// Using 252/365 instead of 5/7 avoids overestimating for 3+ month windows
|
||||
// where accumulated holidays cause the cache check to fail.
|
||||
double marketFraction = barSize switch
|
||||
{
|
||||
HistoricalBarSize.OneMonth => 1.0,
|
||||
HistoricalBarSize.OneDay or HistoricalBarSize.FiveDays => 5.0 / 7.0,
|
||||
_ => (6.5 * 5.0) / (24.0 * 7.0)
|
||||
HistoricalBarSize.OneDay or HistoricalBarSize.FiveDays => 252.0 / 365.0,
|
||||
_ => (6.5 * 252.0) / (24.0 * 365.0)
|
||||
};
|
||||
|
||||
return Math.Max(1, (int)Math.Floor(duration.TotalSeconds * marketFraction / secondsPerBar));
|
||||
|
||||
@ -9,6 +9,7 @@ namespace Backend.DatabaseHandler
|
||||
public double Score { get; init; }
|
||||
public string SentimentLabel { get; init; } = string.Empty;
|
||||
public string TitleHash { get; init; } = string.Empty;
|
||||
public string Publisher { get; init; } = string.Empty;
|
||||
public string Url { get; init; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,17 +29,17 @@ namespace Backend.DatabaseHandler
|
||||
await conn.OpenAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT symbol, bar_size, what_to_show, use_rth, timestamp_utc,
|
||||
open, high, low, close, volume
|
||||
FROM historical_equity_bars
|
||||
WHERE symbol = @symbol
|
||||
AND bar_size = @bar_size
|
||||
AND what_to_show = @what_to_show
|
||||
AND use_rth = @use_rth
|
||||
AND timestamp_utc >= @start_utc
|
||||
AND timestamp_utc <= @end_utc
|
||||
ORDER BY timestamp_utc ASC;
|
||||
""";
|
||||
SELECT symbol, bar_size, what_to_show, use_rth, timestamp_utc,
|
||||
open, high, low, close, volume
|
||||
FROM historical_equity_bars
|
||||
WHERE symbol = @symbol
|
||||
AND bar_size = @bar_size
|
||||
AND what_to_show = @what_to_show
|
||||
AND use_rth = @use_rth
|
||||
AND timestamp_utc >= @start_utc
|
||||
AND timestamp_utc <= @end_utc
|
||||
ORDER BY timestamp_utc ASC;
|
||||
""";
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("symbol", symbol);
|
||||
@ -84,24 +84,24 @@ namespace Backend.DatabaseHandler
|
||||
await using var tx = await conn.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO historical_equity_bars
|
||||
(
|
||||
symbol, bar_size, what_to_show, use_rth, timestamp_utc,
|
||||
open, high, low, close, volume
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@symbol, @bar_size, @what_to_show, @use_rth, @timestamp_utc,
|
||||
@open, @high, @low, @close, @volume
|
||||
)
|
||||
ON CONFLICT (symbol, bar_size, what_to_show, use_rth, timestamp_utc)
|
||||
DO UPDATE SET
|
||||
open = EXCLUDED.open,
|
||||
high = EXCLUDED.high,
|
||||
low = EXCLUDED.low,
|
||||
close = EXCLUDED.close,
|
||||
volume = EXCLUDED.volume;
|
||||
""";
|
||||
INSERT INTO historical_equity_bars
|
||||
(
|
||||
symbol, bar_size, what_to_show, use_rth, timestamp_utc,
|
||||
open, high, low, close, volume
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@symbol, @bar_size, @what_to_show, @use_rth, @timestamp_utc,
|
||||
@open, @high, @low, @close, @volume
|
||||
)
|
||||
ON CONFLICT (symbol, bar_size, what_to_show, use_rth, timestamp_utc)
|
||||
DO UPDATE SET
|
||||
open = EXCLUDED.open,
|
||||
high = EXCLUDED.high,
|
||||
low = EXCLUDED.low,
|
||||
close = EXCLUDED.close,
|
||||
volume = EXCLUDED.volume;
|
||||
""";
|
||||
|
||||
foreach (var bar in barList)
|
||||
{
|
||||
|
||||
@ -23,12 +23,12 @@ namespace Backend.DatabaseHandler
|
||||
await conn.OpenAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT source, keyword, published_at, title, score,
|
||||
sentiment_label, title_hash, url
|
||||
FROM news_sentiment_cache
|
||||
WHERE keyword = @keyword
|
||||
ORDER BY published_at DESC;
|
||||
""";
|
||||
SELECT source, keyword, published_at, title, score,
|
||||
sentiment_label, title_hash, publisher, url
|
||||
FROM news_sentiment_cache
|
||||
WHERE keyword = @keyword
|
||||
ORDER BY published_at DESC;
|
||||
""";
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("keyword", keyword);
|
||||
@ -45,7 +45,55 @@ namespace Backend.DatabaseHandler
|
||||
Score = reader.GetDouble(4),
|
||||
SentimentLabel = reader.GetString(5),
|
||||
TitleHash = reader.GetString(6).TrimEnd(),
|
||||
Url = reader.GetString(7)
|
||||
Publisher = reader.GetString(7),
|
||||
Url = reader.GetString(8)
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<List<NewsSentimentRecord>> GetFeedAsync(
|
||||
IEnumerable<string> keywords,
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var keywordList = keywords.ToList();
|
||||
if (keywordList.Count == 0)
|
||||
return [];
|
||||
|
||||
var results = new List<NewsSentimentRecord>();
|
||||
|
||||
await using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT source, keyword, published_at, title, score,
|
||||
sentiment_label, title_hash, publisher, url
|
||||
FROM news_sentiment_cache
|
||||
WHERE keyword = ANY(@keywords)
|
||||
ORDER BY published_at DESC
|
||||
LIMIT @limit;
|
||||
""";
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("keywords", keywordList.ToArray());
|
||||
cmd.Parameters.AddWithValue("limit", limit);
|
||||
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
results.Add(new NewsSentimentRecord
|
||||
{
|
||||
Source = reader.GetString(0),
|
||||
Keyword = reader.GetString(1),
|
||||
PublishedAt = reader.GetFieldValue<DateTimeOffset>(2),
|
||||
Title = reader.GetString(3),
|
||||
Score = reader.GetDouble(4),
|
||||
SentimentLabel = reader.GetString(5),
|
||||
TitleHash = reader.GetString(6).TrimEnd(),
|
||||
Publisher = reader.GetString(7),
|
||||
Url = reader.GetString(8)
|
||||
});
|
||||
}
|
||||
|
||||
@ -66,14 +114,14 @@ namespace Backend.DatabaseHandler
|
||||
await using var tx = await conn.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO news_sentiment_cache
|
||||
(source, keyword, published_at, title, score,
|
||||
sentiment_label, title_hash, url)
|
||||
VALUES
|
||||
(@source, @keyword, @published_at, @title, @score,
|
||||
@sentiment_label, @title_hash, @url)
|
||||
ON CONFLICT (title_hash) DO NOTHING;
|
||||
""";
|
||||
INSERT INTO news_sentiment_cache
|
||||
(source, keyword, published_at, title, score,
|
||||
sentiment_label, title_hash, publisher, url)
|
||||
VALUES
|
||||
(@source, @keyword, @published_at, @title, @score,
|
||||
@sentiment_label, @title_hash, @publisher, @url)
|
||||
ON CONFLICT (title_hash) DO NOTHING;
|
||||
""";
|
||||
|
||||
foreach (var record in recordList)
|
||||
{
|
||||
@ -85,6 +133,7 @@ namespace Backend.DatabaseHandler
|
||||
cmd.Parameters.AddWithValue("score", record.Score);
|
||||
cmd.Parameters.AddWithValue("sentiment_label", record.SentimentLabel);
|
||||
cmd.Parameters.AddWithValue("title_hash", record.TitleHash);
|
||||
cmd.Parameters.AddWithValue("publisher", record.Publisher);
|
||||
cmd.Parameters.AddWithValue("url", record.Url);
|
||||
|
||||
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
||||
@ -103,10 +152,10 @@ namespace Backend.DatabaseHandler
|
||||
await conn.OpenAsync(cancellationToken);
|
||||
|
||||
const string sql = """
|
||||
SELECT title_hash
|
||||
FROM news_sentiment_cache
|
||||
WHERE keyword = @keyword;
|
||||
""";
|
||||
SELECT title_hash
|
||||
FROM news_sentiment_cache
|
||||
WHERE keyword = @keyword;
|
||||
""";
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("keyword", keyword);
|
||||
|
||||
@ -20,17 +20,17 @@ public sealed class PostgresUserInterestStore : IUserInterestStore
|
||||
string normalizedType = NormalizeType(type);
|
||||
string sql = normalizedType == "symbol"
|
||||
? """
|
||||
SELECT symbol
|
||||
FROM user_interest_symbols
|
||||
WHERE username = @user
|
||||
ORDER BY symbol ASC;
|
||||
"""
|
||||
SELECT symbol
|
||||
FROM user_interest_symbols
|
||||
WHERE username = @user
|
||||
ORDER BY symbol ASC;
|
||||
"""
|
||||
: """
|
||||
SELECT topic
|
||||
FROM user_interest_topics
|
||||
WHERE username = @user
|
||||
ORDER BY topic ASC;
|
||||
""";
|
||||
SELECT topic
|
||||
FROM user_interest_topics
|
||||
WHERE username = @user
|
||||
ORDER BY topic ASC;
|
||||
""";
|
||||
|
||||
var results = new List<string>();
|
||||
|
||||
@ -49,6 +49,29 @@ public sealed class PostgresUserInterestStore : IUserInterestStore
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<List<string>> FetchAllDistinctValuesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT symbol AS value FROM user_interest_symbols
|
||||
UNION
|
||||
SELECT topic AS value FROM user_interest_topics;
|
||||
""";
|
||||
|
||||
var results = new List<string>();
|
||||
|
||||
await using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync(cancellationToken);
|
||||
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken);
|
||||
while (await reader.ReadAsync(cancellationToken))
|
||||
{
|
||||
results.Add(reader.GetString(0));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(string user, string type, string value)
|
||||
{
|
||||
ValidateUser(user);
|
||||
@ -61,15 +84,15 @@ public sealed class PostgresUserInterestStore : IUserInterestStore
|
||||
|
||||
string sql = normalizedType == "symbol"
|
||||
? """
|
||||
INSERT INTO user_interest_symbols (username, symbol)
|
||||
VALUES (@user, @value)
|
||||
ON CONFLICT (username, symbol) DO NOTHING;
|
||||
"""
|
||||
INSERT INTO user_interest_symbols (username, symbol)
|
||||
VALUES (@user, @value)
|
||||
ON CONFLICT (username, symbol) DO NOTHING;
|
||||
"""
|
||||
: """
|
||||
INSERT INTO user_interest_topics (username, topic)
|
||||
VALUES (@user, @value)
|
||||
ON CONFLICT (username, topic) DO NOTHING;
|
||||
""";
|
||||
INSERT INTO user_interest_topics (username, topic)
|
||||
VALUES (@user, @value)
|
||||
ON CONFLICT (username, topic) DO NOTHING;
|
||||
""";
|
||||
|
||||
await using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
@ -94,13 +117,13 @@ public sealed class PostgresUserInterestStore : IUserInterestStore
|
||||
|
||||
string sql = normalizedType == "symbol"
|
||||
? """
|
||||
DELETE FROM user_interest_symbols
|
||||
WHERE username = @user AND symbol = @value;
|
||||
"""
|
||||
DELETE FROM user_interest_symbols
|
||||
WHERE username = @user AND symbol = @value;
|
||||
"""
|
||||
: """
|
||||
DELETE FROM user_interest_topics
|
||||
WHERE username = @user AND topic = @value;
|
||||
""";
|
||||
DELETE FROM user_interest_topics
|
||||
WHERE username = @user AND topic = @value;
|
||||
""";
|
||||
|
||||
await using var conn = new NpgsqlConnection(_connectionString);
|
||||
await conn.OpenAsync();
|
||||
|
||||
@ -8,6 +8,11 @@ namespace Backend.Interface
|
||||
string keyword,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<NewsSentimentRecord>> GetFeedAsync(
|
||||
IEnumerable<string> keywords,
|
||||
int limit,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task InsertAsync(
|
||||
IEnumerable<NewsSentimentRecord> records,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@ -3,6 +3,7 @@ namespace Backend.Interface
|
||||
public interface IUserInterestStore
|
||||
{
|
||||
Task<List<string>> FetchAllAsync(string user, string type);
|
||||
Task<List<string>> FetchAllDistinctValuesAsync(CancellationToken cancellationToken = default);
|
||||
Task<bool> AddAsync(string user, string type, string value);
|
||||
Task<bool> RemoveAsync(string user, string type, string value);
|
||||
}
|
||||
|
||||
78
ServiceHandler/NewsSentimentRefreshService.cs
Normal file
78
ServiceHandler/NewsSentimentRefreshService.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using Backend.Interface;
|
||||
|
||||
namespace Backend.ServiceHander
|
||||
{
|
||||
public sealed class NewsSentimentRefreshService : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan PollInterval = TimeSpan.FromMinutes(15);
|
||||
private static readonly TimeZoneInfo Pacific = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles");
|
||||
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger<NewsSentimentRefreshService> _logger;
|
||||
|
||||
public NewsSentimentRefreshService(
|
||||
IServiceProvider services,
|
||||
ILogger<NewsSentimentRefreshService> logger)
|
||||
{
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (IsWithinActiveWindow())
|
||||
{
|
||||
await RefreshAsync(stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Outside active window (weekdays 07:00-20:00 PST), skipping refresh.");
|
||||
}
|
||||
|
||||
await Task.Delay(PollInterval, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsWithinActiveWindow()
|
||||
{
|
||||
var pacificNow = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, Pacific);
|
||||
|
||||
if (pacificNow.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
|
||||
return false;
|
||||
|
||||
var hour = pacificNow.Hour;
|
||||
return hour >= 7 && hour < 20;
|
||||
}
|
||||
|
||||
private async Task RefreshAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var interests = _services.GetRequiredService<IUserInterestStore>();
|
||||
var sentiment = _services.GetRequiredService<NewsSentimentService>();
|
||||
|
||||
var keywords = await interests.FetchAllDistinctValuesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Refreshing news for {Count} keywords.", keywords.Count);
|
||||
|
||||
foreach (var keyword in keywords)
|
||||
{
|
||||
try
|
||||
{
|
||||
await sentiment.AnalyzeAsync(keyword, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to refresh news for keyword '{Keyword}'.", keyword);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "News sentiment refresh cycle failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,17 +40,19 @@ namespace Backend.ServiceHander
|
||||
if (existingHashes.Contains(titleHash))
|
||||
continue;
|
||||
|
||||
var (score, label) = _scorer.Score(title);
|
||||
var (headline, publisher) = ParsePublisher(title);
|
||||
var (score, label) = _scorer.Score(headline);
|
||||
|
||||
newRecords.Add(new NewsSentimentRecord
|
||||
{
|
||||
Source = "google_rss",
|
||||
Keyword = keyword,
|
||||
PublishedAt = publishedAt,
|
||||
Title = title,
|
||||
Title = headline,
|
||||
Score = score,
|
||||
SentimentLabel = label,
|
||||
TitleHash = titleHash,
|
||||
Publisher = publisher,
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
@ -92,6 +94,14 @@ namespace Backend.ServiceHander
|
||||
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));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user