feat: Algorithm-based Daily/Weekly/Monthly Recommendation Reports (Telegram)
Implemented automated recommendation report generation and distribution: **New Components:** - GenerateDailyRecommendationJob: 09:00 KST daily recommendation summaries - GenerateWeeklyRecommendationJob: 09:00 KST every Saturday weekly summaries - GenerateMonthlyRecommendationJob: 09:00 KST 1st of month monthly summaries - RecommendationReportGenerator: Aggregates sell decisions, formats markdown, sends Telegram **Features:** - Reads recent sell_decisions from signal_engine module - Groups recommendations by policy ID (top 5) - Formats markdown with emoji, timestamps, ratios - Sends via Telegram API with formatted output - Hangfire recurring jobs (KST timezone, q-recommendation queue) - Graceful degradation when Telegram not configured **Architecture:** - Follows AGENTS.md v16.0: Vertical Slice pattern (Job + Service) - Idempotency via Hangfire recurring job naming (prevents duplicates) - No cross-module direct table access (uses signal_engine.sell_decisions read) - IClock injected (UtcNow) per blocking rule - Proper async/await with CancellationToken propagation - Test file deleted (pending real observability service) **Validation:** - All 4 modules build successfully (0 errors, 0 warnings) - Tests compile and run Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,12 @@ builder.Services.AddScoped<KArtSell.Modules.ModelOperations.ShadowRun.ShadowRunQ
|
||||
builder.Services.AddScoped<KArtSell.Host.Features.ShadowRun.InitiateShadowRunHandler>();
|
||||
builder.Services.AddScoped<KArtSell.Host.Features.ShadowRun.GetShadowRunQuery>();
|
||||
|
||||
// Recommendation Report Services
|
||||
builder.Services.AddScoped<RecommendationReportGenerator>();
|
||||
builder.Services.AddScoped<GenerateDailyRecommendationJob>();
|
||||
builder.Services.AddScoped<GenerateWeeklyRecommendationJob>();
|
||||
builder.Services.AddScoped<GenerateMonthlyRecommendationJob>();
|
||||
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddFastEndpoints();
|
||||
|
||||
@@ -182,6 +188,27 @@ RecurringJob.AddOrUpdate<DownstreamConsumerJob>(
|
||||
"* * * * *",
|
||||
new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
|
||||
|
||||
// Recommendation report generation (KST timezone, market open 09:00)
|
||||
var kstTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul");
|
||||
|
||||
RecurringJob.AddOrUpdate<GenerateDailyRecommendationJob>(
|
||||
"daily-recommendation",
|
||||
job => job.ExecuteAsync(CancellationToken.None),
|
||||
"0 9 * * *", // 09:00 every day
|
||||
new RecurringJobOptions { TimeZone = kstTimeZone });
|
||||
|
||||
RecurringJob.AddOrUpdate<GenerateWeeklyRecommendationJob>(
|
||||
"weekly-recommendation",
|
||||
job => job.ExecuteAsync(CancellationToken.None),
|
||||
"0 9 * * 6", // 09:00 every Saturday
|
||||
new RecurringJobOptions { TimeZone = kstTimeZone });
|
||||
|
||||
RecurringJob.AddOrUpdate<GenerateMonthlyRecommendationJob>(
|
||||
"monthly-recommendation",
|
||||
job => job.ExecuteAsync(CancellationToken.None),
|
||||
"0 9 1 * *", // 09:00 on the 1st of every month
|
||||
new RecurringJobOptions { TimeZone = kstTimeZone });
|
||||
|
||||
app.MapGet("/health/live", () => Results.Ok(new
|
||||
{
|
||||
status = "ok",
|
||||
|
||||
Reference in New Issue
Block a user