From 0d370d09e7246ff2a7d3a60ed1b927038e612e77 Mon Sep 17 00:00:00 2001 From: Kleidukos Date: Sat, 15 Aug 2026 21:30:16 +0200 Subject: [PATCH] feat(analyzer): dynamic settings, IFinlyticLogger, live log streaming, and EF migration --- .../Controllers/ManualAnalysisController.cs | 11 +- .../Database/AnalyzerDbContext.cs | 16 +- ...60815184017_AddDynamicSettings.Designer.cs | 308 ++++++++++++++++++ .../20260815184017_AddDynamicSettings.cs | 43 +++ .../AnalyzerDbContextModelSnapshot.cs | 31 ++ FinlyticAnalyzer/Program.cs | 16 +- .../Services/ActiveTradeMonitorWorker.cs | 51 +-- .../Services/N8nEvaluationService.cs | 41 ++- .../Services/ThreeLayerFilterEngine.cs | 33 +- .../Services/VixTrackerService.cs | 23 +- .../Services/WinRateCalculator.cs | 35 +- FinlyticAnalyzer/Util/AnalyzerMqttClient.cs | 264 +++++++++------ FinlyticAnalyzer/Util/SettingKeys.cs | 30 ++ 13 files changed, 687 insertions(+), 215 deletions(-) create mode 100644 FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.Designer.cs create mode 100644 FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.cs create mode 100644 FinlyticAnalyzer/Util/SettingKeys.cs diff --git a/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs b/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs index 68062f6..a05ae4a 100644 --- a/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs +++ b/FinlyticAnalyzer/Controllers/ManualAnalysisController.cs @@ -5,11 +5,12 @@ using System.Threading.Tasks; using FinlyticAnalyzer.Database; using FinlyticAnalyzer.Entities; using FinlyticAnalyzer.Services; +using FinlyticAnalyzer.Util; using FinlyticCore.Models.Analyzer; using FinlyticCore.Models.Trades; +using FinlyticCore.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; namespace FinlyticAnalyzer.Controllers; @@ -36,20 +37,20 @@ public class ManualAnalysisController : ControllerBase private readonly IN8nEvaluationService _n8nService; private readonly IWinRateCalculator _winRateCalculator; private readonly AnalyzerDbContext _dbContext; - private readonly ILogger _logger; + private readonly IFinlyticLogger _finlyticLogger; public ManualAnalysisController( IVixTrackerService vixTracker, IN8nEvaluationService n8nService, IWinRateCalculator winRateCalculator, AnalyzerDbContext dbContext, - ILogger logger) + IFinlyticLogger finlyticLogger) { _vixTracker = vixTracker; _n8nService = n8nService; _winRateCalculator = winRateCalculator; _dbContext = dbContext; - _logger = logger; + _finlyticLogger = finlyticLogger; } /// @@ -181,6 +182,8 @@ public class ManualAnalysisController : ControllerBase _dbContext.Analyses.Add(analysisEntity); await _dbContext.SaveChangesAsync(cancellationToken); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalysisController] Manual analysis completed for {Symbol} (TradeProposed: {Proposed})", request.Symbol, shouldProceed); + if (!shouldProceed) { return Ok(new diff --git a/FinlyticAnalyzer/Database/AnalyzerDbContext.cs b/FinlyticAnalyzer/Database/AnalyzerDbContext.cs index 81811c8..087e2c1 100644 --- a/FinlyticAnalyzer/Database/AnalyzerDbContext.cs +++ b/FinlyticAnalyzer/Database/AnalyzerDbContext.cs @@ -1,10 +1,12 @@ using FinlyticAnalyzer.Entities; +using FinlyticCore.Database; using FinlyticCore.Entities.Settings; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; namespace FinlyticAnalyzer.Database; -public class AnalyzerDbContext : DbContext +public class AnalyzerDbContext : DbContext, ISettingsDbContext { public AnalyzerDbContext(DbContextOptions options) : base(options) { } @@ -20,7 +22,7 @@ public class AnalyzerDbContext : DbContext modelBuilder.Entity(entity => { entity.HasKey(e => e.Id); - entity.HasIndex(e => e.Key); + entity.HasIndex(e => e.Key).IsUnique(); }); modelBuilder.Entity(entity => @@ -39,3 +41,13 @@ public class AnalyzerDbContext : DbContext }); } } + +public class AnalyzerDbContextFactory : IDesignTimeDbContextFactory +{ + public AnalyzerDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + optionsBuilder.UseNpgsql("Host=localhost;Database=analyzer;Username=postgres;Password=postgres"); + return new AnalyzerDbContext(optionsBuilder.Options); + } +} diff --git a/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.Designer.cs b/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.Designer.cs new file mode 100644 index 0000000..21f265b --- /dev/null +++ b/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.Designer.cs @@ -0,0 +1,308 @@ +// +using System; +using FinlyticAnalyzer.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FinlyticAnalyzer.Migrations +{ + [DbContext(typeof(AnalyzerDbContext))] + [Migration("20260815184017_AddDynamicSettings")] + partial class AddDynamicSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalysisEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AiOutputJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("AnalysisId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EventId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ImpactScore") + .HasColumnType("double precision"); + + b.Property("IsTradeProposed") + .HasColumnType("boolean"); + + b.Property("Isin") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("N8nDecision") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("N8nEvalScore") + .HasColumnType("double precision"); + + b.Property("N8nResponseJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RawDataJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Sector") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("VixRegime") + .HasColumnType("integer"); + + b.Property("VixValue") + .HasColumnType("numeric"); + + b.Property("WinRate") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("AnalysisId") + .IsUnique(); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EventId"); + + b.HasIndex("Isin"); + + b.HasIndex("Sector"); + + b.ToTable("analyses"); + }); + + modelBuilder.Entity("FinlyticAnalyzer.Entities.AnalyzerSettingsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EnableLogAnalyzerAuto") + .HasColumnType("boolean"); + + b.Property("EnableLogAnalyzerManual") + .HasColumnType("boolean"); + + b.Property("EnableLogDatabaseOps") + .HasColumnType("boolean"); + + b.Property("EnableLogMqttGeneral") + .HasColumnType("boolean"); + + b.Property("EnableLogMqttHealthPing") + .HasColumnType("boolean"); + + b.Property("MinSignalScore") + .HasColumnType("double precision"); + + b.Property("ScanCronSchedule") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("FinlyticAnalyzer.Entities.TradeProposalEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalysisId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ConfidenceScore") + .HasColumnType("double precision"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntryPrice") + .HasColumnType("decimal(18,4)"); + + b.Property("EntryZoneMax") + .HasColumnType("decimal(18,4)"); + + b.Property("EntryZoneMin") + .HasColumnType("decimal(18,4)"); + + b.Property("EventId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FundamentalRationale") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstrumentType") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("Isin") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("MaxLeverage") + .HasColumnType("decimal(18,4)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("ProposedAction") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ReasonSummary") + .IsRequired() + .HasColumnType("text"); + + b.Property("RiskRewardRatio") + .HasColumnType("decimal(18,4)"); + + b.Property("RiskTolerance") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("RiskWarning") + .IsRequired() + .HasColumnType("text"); + + b.Property("Sector") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StopLoss") + .HasColumnType("decimal(18,4)"); + + b.Property("Symbol") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("TakeProfit") + .HasColumnType("decimal(18,4)"); + + b.Property("TakeProfitTargets") + .HasColumnType("text"); + + b.Property("TechnicalRationale") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timeframe") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("VixRegime") + .HasColumnType("integer"); + + b.Property("VixValue") + .HasColumnType("decimal(18,4)"); + + b.Property("WinRate") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("Isin"); + + b.ToTable("trade_proposals"); + }); + + modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastUpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ServiceIdentifier") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ValueJson") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("DynamicSettings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.cs b/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.cs new file mode 100644 index 0000000..539c266 --- /dev/null +++ b/FinlyticAnalyzer/Migrations/20260815184017_AddDynamicSettings.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FinlyticAnalyzer.Migrations +{ + /// + public partial class AddDynamicSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DynamicSettings", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Key = table.Column(type: "character varying(150)", maxLength: 150, nullable: false), + ValueJson = table.Column(type: "text", nullable: false), + ServiceIdentifier = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + LastUpdatedUtc = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DynamicSettings", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_DynamicSettings_Key", + table: "DynamicSettings", + column: "Key", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DynamicSettings"); + } + } +} diff --git a/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs b/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs index 6820609..373d5a7 100644 --- a/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs +++ b/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs @@ -268,6 +268,37 @@ namespace FinlyticAnalyzer.Migrations b.ToTable("trade_proposals"); }); + + modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastUpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ServiceIdentifier") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ValueJson") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("DynamicSettings"); + }); #pragma warning restore 612, 618 } } diff --git a/FinlyticAnalyzer/Program.cs b/FinlyticAnalyzer/Program.cs index a1154b3..1350195 100644 --- a/FinlyticAnalyzer/Program.cs +++ b/FinlyticAnalyzer/Program.cs @@ -2,14 +2,24 @@ using System; using FinlyticAnalyzer.Database; using FinlyticAnalyzer.Services; using FinlyticAnalyzer.Util; +using FinlyticCore.Database; +using FinlyticCore.Services; using FinlyticCore.Services.Yahoo; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; var builder = Host.CreateApplicationBuilder(args); // Register DB Context builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); +builder.Services.AddScoped(sp => sp.GetRequiredService()); + +// Register Core Services +builder.Services.AddSingleton(); +builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>)); // Register HTTP Clients for external webhooks (HttpClientFactory manages pool) builder.Services.AddHttpClient(); @@ -38,14 +48,10 @@ using (var scope = host.Services.CreateScope()) var context = scope.ServiceProvider.GetRequiredService(); await context.Database.MigrateAsync(); Console.WriteLine("Database migrations successfully executed for FinlyticAnalyzer."); - - var settingsService = scope.ServiceProvider.GetRequiredService(); - await settingsService.GetSettingsAsync(); } catch (Exception ex) { - var logger = scope.ServiceProvider.GetRequiredService>(); - logger.LogError(ex, "An error occurred during database migration for FinlyticAnalyzer on startup."); + Console.WriteLine($"Critical error during database migration for FinlyticAnalyzer: {ex.Message}"); } } diff --git a/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs b/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs index 2780345..2c3962f 100644 --- a/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs +++ b/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs @@ -9,30 +9,32 @@ using FinlyticCore.Dtos; using FinlyticCore.Dtos.TechnicalAnalysis; using FinlyticCore.Models.Analyzer; using FinlyticCore.Models.Trades; +using FinlyticCore.Services; using FinlyticCore.Util; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace FinlyticAnalyzer.Services; public class ActiveTradeMonitorWorker : BackgroundService { - private readonly ILogger _logger; + private readonly IFinlyticLogger _finlyticLogger; private readonly IServiceScopeFactory _scopeFactory; private readonly AnalyzerMqttClient _mqttClient; - public ActiveTradeMonitorWorker(ILogger logger, IServiceScopeFactory scopeFactory, + public ActiveTradeMonitorWorker( + IFinlyticLogger finlyticLogger, + IServiceScopeFactory scopeFactory, AnalyzerMqttClient mqttClient) { - _logger = logger; + _finlyticLogger = finlyticLogger; _scopeFactory = scopeFactory; _mqttClient = mqttClient; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker started.", "AnalyzerChannel"); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker started."); try { @@ -51,7 +53,7 @@ public class ActiveTradeMonitorWorker : BackgroundService } catch (Exception ex) when (!stoppingToken.IsCancellationRequested) { - _logger.LogError(ex, "[{Channel}] Error in ActiveTradeMonitorWorker loop.", "AnalyzerChannel"); + await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Error in ActiveTradeMonitorWorker loop."); } try @@ -64,24 +66,22 @@ public class ActiveTradeMonitorWorker : BackgroundService } } - _logger.LogInformation("[{Channel}] ActiveTradeMonitorWorker stopped.", "AnalyzerChannel"); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] ActiveTradeMonitorWorker stopped."); } private async Task MonitorActiveTradesAsync(CancellationToken cancellationToken) { if (!_mqttClient.IsConnected) { - _logger.LogWarning("[{Channel}] Skipping trade monitoring. RPC client not connected.", "AnalyzerChannel"); + await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Skipping trade monitoring. RPC client not connected."); return; } - // Fetch active trades var activeTrades = await _mqttClient.SendRpcRequestAsync, GetTradesRequest>( "trades_Get", new GetTradesRequest(null, "Active"), TimeSpan.FromSeconds(10)); - // Fetch proposed global trades var proposedTrades = await _mqttClient.SendRpcRequestAsync, GetTradesRequest>( "trades_Get", new GetTradesRequest(null, "Proposed"), @@ -93,13 +93,11 @@ public class ActiveTradeMonitorWorker : BackgroundService if (trades.Count == 0) { - _logger.LogInformation("[{Channel}] No active or proposed global trades found to monitor.", - "AnalyzerChannel"); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] No active or proposed global trades found to monitor."); return; } - _logger.LogInformation("[{Channel}] Found {Count} trades to monitor. Starting evaluation...", "AnalyzerChannel", - trades.Count); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Found {Count} trades to monitor. Starting evaluation...", trades.Count); using var scope = _scopeFactory.CreateScope(); var n8nService = scope.ServiceProvider.GetRequiredService(); @@ -115,8 +113,7 @@ public class ActiveTradeMonitorWorker : BackgroundService } catch (Exception ex) { - _logger.LogError(ex, "[{Channel}] Failed to monitor trade {TradeId} ({Symbol}).", "AnalyzerChannel", - trade.TradeId, trade.Symbol); + await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[ActiveTradeMonitorWorker] Failed to monitor trade {TradeId} ({Symbol}).", trade.TradeId, trade.Symbol); } } } @@ -124,22 +121,18 @@ public class ActiveTradeMonitorWorker : BackgroundService private async Task ProcessTradeAsync(TradeProposalDto trade, IN8nEvaluationService n8nService, IVixTrackerService vixService, CancellationToken cancellationToken) { - // 1. Get Live Price var livePriceReq = new IsinRequest(trade.Isin); var livePriceDto = await _mqttClient.SendRpcRequestAsync( "tr_GetLivePrice", livePriceReq, TimeSpan.FromSeconds(3)); decimal currentPrice = livePriceDto?.CurrentPrice > 0 ? livePriceDto.CurrentPrice : trade.EntryPrice; - // 2. Evaluate Hard Stops (StopLoss / TakeProfit / TimeStop) bool isLong = string.Equals(trade.SignalType, "BUY", StringComparison.OrdinalIgnoreCase) || string.Equals(trade.SignalType, "LONG", StringComparison.OrdinalIgnoreCase); - // Time-Stop Evaluierung int maxHoldingDays = EstimateMaxHoldingDays(trade.Timeframe); double daysOpen = (DateTime.UtcNow - trade.CreatedAt).TotalDays; - // 50% Grace Period. Bei z.B. 10 Tagen max. Haltedauer wird nach 15 Tagen ohne Zielerreichung glattgestellt. if (daysOpen > (maxHoldingDays * 1.5)) { await SendUpdateAsync(trade, currentPrice, "Close", @@ -176,7 +169,6 @@ public class ActiveTradeMonitorWorker : BackgroundService } } - // 3. Run AI evaluation for soft/dynamic updates var taResult = await _mqttClient.SendRpcRequestAsync( "ta_GetAnalysis", livePriceReq, TimeSpan.FromSeconds(5)); @@ -225,8 +217,7 @@ public class ActiveTradeMonitorWorker : BackgroundService var aiResponse = await n8nService.EvaluateAssetAsync(n8nReq, cancellationToken); if (aiResponse == null) { - _logger.LogWarning("[{Channel}] AI evaluation returned null for {TradeId}. Skipping update.", - "AnalyzerChannel", trade.TradeId); + await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] AI evaluation returned null for {TradeId}. Skipping update.", trade.TradeId); return; } @@ -235,7 +226,6 @@ public class ActiveTradeMonitorWorker : BackgroundService decimal? newStopLoss = trade.StopLoss; decimal? newTakeProfit = trade.TakeProfit; - // Check for trend reversal bool aiSuggestsShort = string.Equals(aiResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) || string.Equals(aiResponse.SuggestedDirection, "Sell", StringComparison.OrdinalIgnoreCase); @@ -256,13 +246,11 @@ public class ActiveTradeMonitorWorker : BackgroundService } else if (aiResponse.ExecutionPlan != null) { - // Ratchet / Trailing Logic: StopLoss darf das Risiko nicht vergrößern! if (aiResponse.ExecutionPlan.StopLoss > 0) { var proposedSl = aiResponse.ExecutionPlan.StopLoss; if (isLong) { - // Bei Long darf der StopLoss nur NACH OBEN angepasst werden if (trade.StopLoss <= 0 || proposedSl > trade.StopLoss) { newStopLoss = proposedSl; @@ -271,7 +259,6 @@ public class ActiveTradeMonitorWorker : BackgroundService } else { - // Bei Short darf der StopLoss nur NACH UNTEN angepasst werden if (trade.StopLoss <= 0 || proposedSl < trade.StopLoss) { newStopLoss = proposedSl; @@ -310,18 +297,16 @@ public class ActiveTradeMonitorWorker : BackgroundService Timestamp = DateTime.UtcNow }; - // Direktes Objekt-Publishing nutzen (ManagedMqttClient serialisiert typgerecht) string topic = $"finlytic/trades/updates/{trade.Isin}"; await _mqttClient.PublishAsync(topic, update); - _logger.LogInformation( - "[{Channel}] Published trade update for {TradeId} to topic {Topic}. Recommendation: {Rec}, Reasoning: {Reason}", - "AnalyzerChannel", trade.TradeId, topic, recommendation, reasoning); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ActiveTradeMonitorWorker] Published trade update for {TradeId} to topic {Topic}. Recommendation: {Rec}, Reasoning: {Reason}", + trade.TradeId, topic, recommendation, reasoning); } private static int EstimateMaxHoldingDays(string timeframe) { - if (string.IsNullOrWhiteSpace(timeframe)) return 14; // Default + if (string.IsNullOrWhiteSpace(timeframe)) return 14; string tfLower = timeframe.ToLowerInvariant(); int multiplier = 1; @@ -351,7 +336,7 @@ public class ActiveTradeMonitorWorker : BackgroundService int maxNum = numbers.Count > 0 ? numbers.Max() : 14; if (maxNum == 0) maxNum = 14; - if (multiplier == 1 && maxNum < 3) maxNum = 3; // Mindestens 3 Tage Kulanz + if (multiplier == 1 && maxNum < 3) maxNum = 3; return maxNum * multiplier; } diff --git a/FinlyticAnalyzer/Services/N8nEvaluationService.cs b/FinlyticAnalyzer/Services/N8nEvaluationService.cs index 842ada9..aa90760 100644 --- a/FinlyticAnalyzer/Services/N8nEvaluationService.cs +++ b/FinlyticAnalyzer/Services/N8nEvaluationService.cs @@ -1,26 +1,33 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using FinlyticAnalyzer.Util; using FinlyticCore.Models.Analyzer; +using FinlyticCore.Services; using FinlyticCore.Util; +using Microsoft.Extensions.Configuration; namespace FinlyticAnalyzer.Services; public class N8nEvaluationService : IN8nEvaluationService { private readonly HttpClient _httpClient; - private readonly ILogger _logger; + private readonly IFinlyticLogger _finlyticLogger; private readonly string _webhookUrl; - public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, ILogger logger) + public N8nEvaluationService(HttpClient httpClient, IConfiguration configuration, IFinlyticLogger finlyticLogger) { _httpClient = httpClient; - _logger = logger; + _finlyticLogger = finlyticLogger; _webhookUrl = configuration["N8N:WebhookUrl"] ?? configuration["N8N__WebhookUrl"] ?? string.Empty; if (string.IsNullOrWhiteSpace(_webhookUrl)) { - _logger.LogWarning("[{Channel}] N8N:WebhookUrl configuration is missing or empty.", "AnalyzerChannel"); + _ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] N8N:WebhookUrl configuration is missing or empty."); } - // Timeout auf 45 Sekunden erhöht für komplexere LLM/Gemini Chains in n8n _httpClient.Timeout = TimeSpan.FromSeconds(45); } @@ -31,16 +38,15 @@ public class N8nEvaluationService : IN8nEvaluationService { if (string.IsNullOrWhiteSpace(_webhookUrl)) { - _logger.LogError("[{Channel}] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured.", "AnalyzerChannel", request.TargetAsset.Symbol); + await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured.", request.TargetAsset.Symbol); return null; } try { - _logger.LogInformation("[{Channel}] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...", - "AnalyzerChannel", request.RequestId, request.TargetAsset.Symbol, request.TargetAsset.Isin, _webhookUrl); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...", + request.RequestId, request.TargetAsset.Symbol, request.TargetAsset.Isin, _webhookUrl); - // Typsichere AOT-Serialisierung verwenden using var content = JsonContent.Create( request, FinlyticJsonSerializerContext.Default.N8nAnalysisRequestDto); @@ -53,11 +59,10 @@ public class N8nEvaluationService : IN8nEvaluationService if (string.IsNullOrWhiteSpace(contentStr) || contentStr.Trim() == "{}" || contentStr.Trim() == "[]") { - _logger.LogWarning("[{Channel}] n8n Webhook returned an EMPTY response for Request {RequestId}. Flagging as AI Rejection (Too Risky).", "AnalyzerChannel", request.RequestId); + await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned an EMPTY response for Request {RequestId}. Flagging as AI Rejection (Too Risky).", request.RequestId); return CreateRejectionFallback(request, "Die KI (n8n/Gemini) stuft den Trade als zu riskant ein und empfiehlt keine Positionierung."); } - // N8n schickt Ergebnisse manchmal als JSON-Array [{...}] zurück string jsonToDeserialize = contentStr.Trim(); if (jsonToDeserialize.StartsWith('[') && jsonToDeserialize.EndsWith(']')) { @@ -74,28 +79,28 @@ public class N8nEvaluationService : IN8nEvaluationService if (responseDto != null && !string.IsNullOrWhiteSpace(responseDto.AiDecision)) { - _logger.LogInformation("[{Channel}] Received n8n AI Response for Request {RequestId}: Decision={Decision}, Score={Score:F2}, Direction={Direction}, Timeframe={Timeframe}", - "AnalyzerChannel", request.RequestId, responseDto.AiDecision, responseDto.EvalScore, responseDto.SuggestedDirection, responseDto.SuggestedTimeframe); + await _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] Received n8n AI Response for Request {RequestId}: Decision={Decision}, Score={Score:F2}, Direction={Direction}, Timeframe={Timeframe}", + request.RequestId, responseDto.AiDecision, responseDto.EvalScore, responseDto.SuggestedDirection, responseDto.SuggestedTimeframe); return responseDto; } } else { - _logger.LogWarning("[{Channel}] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}", - "AnalyzerChannel", response.StatusCode, request.RequestId); + await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[N8nEvaluationService] n8n Webhook returned HTTP {StatusCode} for Request {RequestId}", + response.StatusCode, request.RequestId); } } catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) { - _logger.LogError(ex, "[{Channel}] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", "AnalyzerChannel", request.RequestId); + await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Timeout waiting for n8n AI Evaluation Webhook (Request {RequestId}). Consider increasing timeout.", request.RequestId); } catch (Exception ex) { - _logger.LogError(ex, "[{Channel}] Error calling n8n AI Evaluation Webhook for Request {RequestId}", "AnalyzerChannel", request.RequestId); + await _finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[N8nEvaluationService] Error calling n8n AI Evaluation Webhook for Request {RequestId}", request.RequestId); } - return null; // Signals RPC/Service failure to caller + return null; } private static N8nAnalysisResponseDto CreateRejectionFallback(N8nAnalysisRequestDto request, string reasoning) diff --git a/FinlyticAnalyzer/Services/ThreeLayerFilterEngine.cs b/FinlyticAnalyzer/Services/ThreeLayerFilterEngine.cs index 8d88f66..7ae69e6 100644 --- a/FinlyticAnalyzer/Services/ThreeLayerFilterEngine.cs +++ b/FinlyticAnalyzer/Services/ThreeLayerFilterEngine.cs @@ -1,21 +1,22 @@ using System; using System.Collections.Concurrent; +using FinlyticAnalyzer.Util; using FinlyticCore.Dtos.News; using FinlyticCore.Models.Analyzer; -using Microsoft.Extensions.Logging; +using FinlyticCore.Services; namespace FinlyticAnalyzer.Services; public class ThreeLayerFilterEngine : IThreeLayerFilterEngine { - private readonly ILogger _logger; + private readonly IFinlyticLogger _finlyticLogger; private readonly ConcurrentDictionary _seenEvents = new(); private readonly object _cleanupLock = new(); private DateTime _lastCleanupTime = DateTime.UtcNow; - public ThreeLayerFilterEngine(ILogger logger) + public ThreeLayerFilterEngine(IFinlyticLogger finlyticLogger) { - _logger = logger; + _finlyticLogger = finlyticLogger; } /// @@ -25,9 +26,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine { var result = new FilterResult(); - // ------------------------------------------------------------- - // Layer 1: Relevance, ISIN & Deduplication - // ------------------------------------------------------------- if (newsEvent == null || newsEvent.Id == Guid.Empty) { result.Passed = false; @@ -38,7 +36,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine string eventId = newsEvent.Id.ToString(); var now = DateTime.UtcNow; - // Safely clean up dictionary every 30 minutes (thread-safe lock) if ((now - _lastCleanupTime).TotalMinutes > 30 || _seenEvents.Count > 10000) { lock (_cleanupLock) @@ -50,7 +47,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine } } - // Deduplication check (keep history for 12 hours) if (_seenEvents.TryGetValue(eventId, out var prevTime) && (now - prevTime).TotalHours < 12.0) { result.Passed = false; @@ -63,7 +59,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine string isin = string.Empty; string assetName = string.Empty; - // Extract parameters strictly from MatchedAssets if (newsEvent.MatchedAssets != null && newsEvent.MatchedAssets.Count > 0) { var firstAsset = newsEvent.MatchedAssets[0]; @@ -71,7 +66,6 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine assetName = !string.IsNullOrWhiteSpace(firstAsset.Name) ? firstAsset.Name.Trim() : string.Empty; } - // Mandatory check: Must have a valid ISIN if (string.IsNullOrWhiteSpace(isin)) { result.Passed = false; @@ -80,13 +74,9 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine } result.Isin = isin; - // Asset-Symbol fallback to ISIN, Name is mapped appropriately later result.Symbol = isin; - result.Sector = "General"; // Will be enriched downstream via Fundamentals RPC if available + result.Sector = "General"; - // ------------------------------------------------------------- - // Layer 2: Impact & Dynamic VIX Threshold - // ------------------------------------------------------------- double impactScore = newsEvent.Confidence ?? 0.75; if (impactScore <= 0) impactScore = 0.75; @@ -106,14 +96,11 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine { result.Passed = false; result.RejectReason = $"Layer 2: Impact score ({impactScore:F2}) below dynamic VIX threshold ({requiredThreshold:F2}) for regime {regime}"; - _logger.LogInformation("[{Channel}] Event {EventId} (ISIN: {Isin}) rejected by Layer 2 filter. Impact: {Impact:F2}, Threshold: {Threshold:F2}, Regime: {Regime}", - "AnalyzerChannel", eventId, isin, impactScore, requiredThreshold, regime); + _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ThreeLayerFilterEngine] Event {EventId} (ISIN: {Isin}) rejected by Layer 2 filter. Impact: {Impact:F2}, Threshold: {Threshold:F2}, Regime: {Regime}", + eventId, isin, impactScore, requiredThreshold, regime); return result; } - // ------------------------------------------------------------- - // Layer 3: Dynamic Parameter & Risk Engine - // ------------------------------------------------------------- result.RiskTolerance = regime switch { VixMarketRegime.Panic => "Conservative", @@ -125,8 +112,8 @@ public class ThreeLayerFilterEngine : IThreeLayerFilterEngine result.InstrumentType = regime == VixMarketRegime.Panic ? "Option" : "Stock"; result.Passed = true; - _logger.LogInformation("[{Channel}] Event {EventId} passed 3-Layer Filter for ISIN {Isin}. Impact: {Impact:F2}, Regime: {Regime}", - "AnalyzerChannel", eventId, result.Isin, impactScore, regime); + _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ThreeLayerFilterEngine] Event {EventId} passed 3-Layer Filter for ISIN {Isin}. Impact: {Impact:F2}, Regime: {Regime}", + eventId, result.Isin, impactScore, regime); return result; } diff --git a/FinlyticAnalyzer/Services/VixTrackerService.cs b/FinlyticAnalyzer/Services/VixTrackerService.cs index ea1665f..f6597a3 100644 --- a/FinlyticAnalyzer/Services/VixTrackerService.cs +++ b/FinlyticAnalyzer/Services/VixTrackerService.cs @@ -1,25 +1,26 @@ using System; using System.Threading; using System.Threading.Tasks; +using FinlyticAnalyzer.Util; using FinlyticCore.Models.Analyzer; +using FinlyticCore.Services; using FinlyticCore.Services.Yahoo; -using Microsoft.Extensions.Logging; namespace FinlyticAnalyzer.Services; public class VixTrackerService : IVixTrackerService { private readonly YahooFinanceClient _yahooClient; - private readonly ILogger _logger; + private readonly IFinlyticLogger _finlyticLogger; - private decimal _currentVix = 18.5m; // Default: Normal Regime + private decimal _currentVix = 18.5m; private VixMarketRegime _currentRegime = VixMarketRegime.Normal; private readonly object _lock = new(); - public VixTrackerService(YahooFinanceClient yahooClient, ILogger logger) + public VixTrackerService(YahooFinanceClient yahooClient, IFinlyticLogger finlyticLogger) { _yahooClient = yahooClient; - _logger = logger; + _finlyticLogger = finlyticLogger; } public decimal GetCurrentVix() @@ -52,13 +53,13 @@ public class VixTrackerService : IVixTrackerService if (oldRegime != _currentRegime) { - _logger.LogWarning("[{Channel}] [VIX REGIME SHIFT] Markt-Regime gewechselt: {OldRegime} -> {NewRegime} (VIX: {Vix:F2})", - "AnalyzerChannel", oldRegime, _currentRegime, _currentVix); + _ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[VixTrackerService] [VIX REGIME SHIFT] Markt-Regime gewechselt: {OldRegime} -> {NewRegime} (VIX: {Vix:F2})", + oldRegime, _currentRegime, _currentVix); } else if (Math.Abs(oldVix - vixValue) >= 0.5m) { - _logger.LogInformation("[{Channel}] VIX aktualisiert: {Vix:F2} (Regime: {Regime})", - "AnalyzerChannel", _currentVix, _currentRegime); + _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[VixTrackerService] VIX aktualisiert: {Vix:F2} (Regime: {Regime})", + _currentVix, _currentRegime); } } } @@ -77,12 +78,10 @@ public class VixTrackerService : IVixTrackerService } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { - // Graceful shutdown } catch (Exception ex) { - _logger.LogWarning(ex, "[{Channel}] Fehler beim Abfragen von ^VIX über YahooFinanceClient. Nutze gecachten Wert {Vix}.", - "AnalyzerChannel", GetCurrentVix()); + await _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[VixTrackerService] Fehler beim Abfragen von ^VIX über YahooFinanceClient. Nutze gecachten Wert {Vix}.", GetCurrentVix()); } return GetCurrentVix(); diff --git a/FinlyticAnalyzer/Services/WinRateCalculator.cs b/FinlyticAnalyzer/Services/WinRateCalculator.cs index 9dc5dcf..cdab052 100644 --- a/FinlyticAnalyzer/Services/WinRateCalculator.cs +++ b/FinlyticAnalyzer/Services/WinRateCalculator.cs @@ -3,15 +3,16 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; +using FinlyticAnalyzer.Util; using FinlyticCore.Models.Analyzer; using FinlyticCore.Models.Trades; -using Microsoft.Extensions.Logging; +using FinlyticCore.Services; namespace FinlyticAnalyzer.Services; public class WinRateCalculator : IWinRateCalculator { - private readonly ILogger _logger; + private readonly IFinlyticLogger _finlyticLogger; private readonly string _feedbackDir; private readonly object _cacheLock = new(); @@ -19,9 +20,9 @@ public class WinRateCalculator : IWinRateCalculator private DateTime _lastCacheTime = DateTime.MinValue; private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(3); - public WinRateCalculator(ILogger logger) + public WinRateCalculator(IFinlyticLogger finlyticLogger) { - _logger = logger; + _finlyticLogger = finlyticLogger; _feedbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data", "feedback"); if (!Directory.Exists(_feedbackDir)) { @@ -31,7 +32,6 @@ public class WinRateCalculator : IWinRateCalculator /// /// Calculates the win rate for a given sector and symbol under the specified market regime. - /// Uses cached feedback records (3-minute TTL) to prevent disk I/O bottlenecks. /// public double CalculateWinRate(string sector, string symbol, VixMarketRegime regime) { @@ -53,27 +53,23 @@ public class WinRateCalculator : IWinRateCalculator { try { - // 1. N8n AI Confidence Score (Weight: 40%) double n8nComponent = 62.0; if (n8nEvalScore.HasValue && n8nEvalScore.Value > 0) { n8nComponent = n8nEvalScore.Value <= 1.0 ? n8nEvalScore.Value * 100.0 : n8nEvalScore.Value; } - // 2. Technical Score (Weight: 30%) double taComponent = 60.0; if (technicalScore.HasValue && technicalScore.Value > 0) { taComponent = technicalScore.Value <= 1.0 ? technicalScore.Value * 100.0 : technicalScore.Value; } - // 3. Sentiment Score (Weight: 15%) double sentComponent = 58.0; if (sentimentScore.HasValue) { if (sentimentScore.Value >= -1.0 && sentimentScore.Value <= 1.0) { - // Map sentiment from -1.0..+1.0 into 35.0..85.0 sentComponent = 50.0 + (sentimentScore.Value * 25.0); } else @@ -82,29 +78,25 @@ public class WinRateCalculator : IWinRateCalculator } } - // 4. Fundamental Score (Weight: 15%) double fundComponent = 60.0; if (fundamentalScore.HasValue && fundamentalScore.Value > 0) { fundComponent = fundamentalScore.Value <= 1.0 ? fundamentalScore.Value * 100.0 : fundamentalScore.Value; } - // Multi-factor weighted composite double composite = (n8nComponent * 0.40) + (taComponent * 0.30) + (sentComponent * 0.15) + (fundComponent * 0.15); - // 5. Market Regime & Volatility Adjustment double vixAdjustment = regime switch { - VixMarketRegime.LowVol => +4.0, // Calm trending market - VixMarketRegime.Normal => +1.5, // Normal conditions - VixMarketRegime.HighVol => -3.5, // Increased whipsaws - VixMarketRegime.Panic => -8.0, // High panic / uncertainty + VixMarketRegime.LowVol => +4.0, + VixMarketRegime.Normal => +1.5, + VixMarketRegime.HighVol => -3.5, + VixMarketRegime.Panic => -8.0, _ => 0.0 }; composite += vixAdjustment; - // 6. Historical track record calibration (if available in feedback records) var records = GetCachedOrLoadRecords(); if (records.Count > 0) { @@ -120,17 +112,16 @@ public class WinRateCalculator : IWinRateCalculator } } - // Clamp between realistic financial statistical bounds (45.0% to 92.0%) double finalWinRate = Math.Clamp(Math.Round(composite, 1), 45.0, 92.0); - _logger.LogInformation("[{Channel}] Dynamic Win-Rate for {Symbol} ({Sector}): {WinRate:F1}% [AI: {N8n:F1}%, TA: {TA:F1}%, Sent: {Sent:F1}%, Regime: {Regime}]", - "AnalyzerChannel", symbol, sector, finalWinRate, n8nComponent, taComponent, sentComponent, regime); + _ = _finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[WinRateCalculator] Dynamic Win-Rate for {Symbol} ({Sector}): {WinRate:F1}% [AI: {N8n:F1}%, TA: {TA:F1}%, Sent: {Sent:F1}%, Regime: {Regime}]", + symbol, sector, finalWinRate, n8nComponent, taComponent, sentComponent, regime); return finalWinRate; } catch (Exception ex) { - _logger.LogWarning(ex, "[{Channel}] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", "AnalyzerChannel", symbol); + _ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Error calculating dynamic win-rate for {Symbol}. Fallback applied.", symbol); return 65.0; } } @@ -162,7 +153,7 @@ public class WinRateCalculator : IWinRateCalculator } catch (Exception ex) { - _logger.LogWarning(ex, "[{Channel}] Failed to read or parse feedback file '{File}'", "AnalyzerChannel", file); + _ = _finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[WinRateCalculator] Failed to read or parse feedback file '{File}'", file); } } } diff --git a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs b/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs index 5a8762a..b7c5aa6 100644 --- a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs +++ b/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs @@ -8,9 +8,11 @@ using FinlyticAnalyzer.Database; using FinlyticAnalyzer.Entities; using FinlyticAnalyzer.Services; using FinlyticCore.Dtos; +using FinlyticCore.Dtos.Settings; using FinlyticCore.Models; using FinlyticCore.Models.Analyzer; using FinlyticCore.Models.Trades; +using FinlyticCore.Services; using FinlyticCore.Util; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -64,19 +66,19 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "finlytic_analyzer")}_{Guid.NewGuid():N}" }; - _logger.LogInformation("[{Channel}] Starting Unified Analyzer MQTT Client. Host: {Host}, ClientId: {ClientId}", "AnalyzerChannel", config.Host, config.ClientId); + _logger.LogInformation("Starting Unified Analyzer MQTT Client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId); await ConnectAsync(config); } public async Task StopAsync(CancellationToken cancellationToken) { - _logger.LogInformation("[{Channel}] Stopping Unified Analyzer MQTT Client.", "AnalyzerChannel"); + _logger.LogInformation("Stopping Unified Analyzer MQTT Client."); await DisconnectAsync(); } protected override async Task OnConnectedAsync() { - _logger.LogInformation("[{Channel}] Analyzer MQTT Client connected. Subscribing to topics and RPC response channels...", "AnalyzerChannel"); + _logger.LogInformation("Analyzer MQTT Client connected. Subscribing to topics and RPC response channels..."); // Incoming Event Topics await SubscribeAsync("services/news/#"); @@ -85,16 +87,29 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService await SubscribeAsync("services/config/updated/#"); await SubscribeAsync("services/request/health_Ping/#"); await SubscribeAsync("services/request/analyzer_TriggerManual/#"); + await SubscribeAsync("services/request/analyzer_settings_GetAll/#"); + await SubscribeAsync("services/request/analyzer_settings_Update/#"); await SubscribeAsync("finlytic/trades/closed/#"); // RPC Response Channels await SubscribeAsync("services/response/ta_GetAnalysis/#"); await SubscribeAsync("services/response/fundamentals_Get/#"); await SubscribeAsync("services/response/sentiment_GetIsin/#"); + await SubscribeAsync("services/response/sentiment_Analyze/#"); await SubscribeAsync("services/response/trades_Get/#"); await SubscribeAsync("services/response/tr_GetLivePrice/#"); + await SubscribeAsync("services/response/events_GetByMonth/#"); + await SubscribeAsync("services/response/events_GetAll/#"); - _logger.LogInformation("[{Channel}] Successfully subscribed to all event and RPC channels.", "AnalyzerChannel"); + FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) => + { + if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase)) + { + await PublishAsync("finlytic/logs/FinlyticAnalyzer", logDto); + } + }; + + _logger.LogInformation("Successfully subscribed to all event and RPC channels."); } protected override async Task OnMessageReceivedAsync(string topic, string payloadStr) @@ -114,10 +129,9 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService string respTopic = $"services/response/health_Ping/{correlationId}"; var healthResp = new ServiceHealthResponse("FinlyticAnalyzer", "Online", DateTime.UtcNow, "Connected"); await PublishAsync(respTopic, healthResp); - if (LogCategoryFilter.IsEnabled(LogCategory.MqttHealthPing)) - { - _logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", "AnalyzerChannel", correlationId); - } + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "[AnalyzerMqttClient] Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId); } return; } @@ -126,21 +140,22 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService { if (topic.EndsWith("FinlyticAnalyzer", StringComparison.OrdinalIgnoreCase)) { - _logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Received config update event for FinlyticAnalyzer.", "AnalyzerChannel"); try { var configUpdate = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ServiceConfigUpdatePayload); if (configUpdate?.Settings != null && configUpdate.Settings.Count > 0) { using var scope = _scopeFactory.CreateScope(); - var settingsDb = scope.ServiceProvider.GetRequiredService(); - await settingsDb.UpdateSettingsFromDictionaryAsync(configUpdate.Settings); - _logger.LogInformation("[{Channel}] [AnalyzerMqttClient] Persisted {Count} updated settings to FinlyticAnalyzer database.", "AnalyzerChannel", configUpdate.Settings.Count); + var settings = scope.ServiceProvider.GetRequiredService(); + var dict = configUpdate.Settings.ToDictionary(k => k.Key, v => (object?)v.Value); + await settings.UpdateSettingsAsync(dict); } } catch (Exception ex) { - _logger.LogError(ex, "[{Channel}] [AnalyzerMqttClient] Error processing MQTT config update event.", "AnalyzerChannel"); + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Error processing MQTT config update event."); } } return; @@ -160,6 +175,16 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService var correlationId = topic.Split('/').Last(); await HandleManualTriggerAsync(correlationId, payloadStr, CancellationToken.None); } + else if (topic.StartsWith("services/request/analyzer_settings_GetAll", StringComparison.OrdinalIgnoreCase)) + { + var correlationId = topic.Split('/').Last(); + await HandleSettingsGetAllAsync(correlationId); + } + else if (topic.StartsWith("services/request/analyzer_settings_Update", StringComparison.OrdinalIgnoreCase)) + { + var correlationId = topic.Split('/').Last(); + await HandleSettingsUpdateAsync(payloadStr, correlationId); + } else if (topic.StartsWith("finlytic/trades/closed/")) { await HandleClosedTradeFeedbackAsync(payloadStr); @@ -167,12 +192,80 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService } catch (Exception ex) { - _logger.LogError(ex, "[{Channel}] Error processing incoming MQTT message on topic {Topic}", "AnalyzerChannel", topic); + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Error processing incoming MQTT message on topic {Topic}", topic); + } + } + + private async Task HandleSettingsGetAllAsync(string correlationId) + { + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + var settingsService = scope.ServiceProvider.GetRequiredService(); + + await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId); + try + { + var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); + var responseTopic = $"services/response/analyzer_settings_GetAll/{correlationId}"; + + await PublishAsync(responseTopic, settings); + await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic); + } + catch (Exception ex) + { + await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticAnalyzer] [Settings_GetAll] Failed to retrieve settings."); + } + } + + private async Task HandleSettingsUpdateAsync(string payload, string correlationId) + { + if (string.IsNullOrWhiteSpace(payload)) return; + + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + var settingsService = scope.ServiceProvider.GetRequiredService(); + + await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId); + try + { + Dictionary? updates = null; + try + { + updates = JsonSerializer.Deserialize>(payload); + } + catch + { + var list = JsonSerializer.Deserialize>(payload); + if (list != null) + { + updates = new Dictionary(); + foreach (var item in list) updates[item.Key] = item.Value; + } + } + + if (updates != null && updates.Count > 0) + { + await settingsService.UpdateSettingsAsync(updates); + await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticAnalyzer] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count); + } + + var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) }); + var responseTopic = $"services/response/analyzer_settings_Update/{correlationId}"; + await PublishAsync(responseTopic, currentSettings); + } + catch (Exception ex) + { + await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticAnalyzer] [Settings_Update] Failed to update settings."); } } private async Task HandleClosedTradeFeedbackAsync(string payloadStr) { + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + try { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; @@ -209,32 +302,31 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService string filePath = System.IO.Path.Combine(feedbackDir, $"{closedDto.TradeId}.json"); await System.IO.File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(new[] { feedback }, options)); - _logger.LogInformation("[{Channel}] Processed closed trade feedback for {TradeId}. Saved to {FilePath}", "AnalyzerChannel", closedDto.TradeId, filePath); + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AnalyzerMqttClient] Processed closed trade feedback for {TradeId}. Saved to {FilePath}", closedDto.TradeId, filePath); } } catch (Exception ex) { - _logger.LogError(ex, "[{Channel}] Error processing closed trade feedback.", "AnalyzerChannel"); + await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Error processing closed trade feedback."); } } private async Task HandleManualTriggerAsync(string correlationId, string payloadStr, CancellationToken cancellationToken) { + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + try { var manualReq = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.ManualAnalysisRpcRequest); if (manualReq == null || string.IsNullOrWhiteSpace(manualReq.Isin)) { - _logger.LogWarning("[{Channel}] Manual trigger received without valid request or ISIN.", "AnalyzerChannel"); + await finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, "[AnalyzerMqttClient] Manual trigger received without valid request or ISIN."); return; } - if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerManual)) - { - _logger.LogInformation("[{Channel}] [ManualAnalyzer] [TRIGGERED] Processing rich manual trigger for ISIN '{Isin}' (Symbol: {Symbol}). CorrelationId: {CorrelationId}", "AnalyzerChannel", manualReq.Isin, manualReq.Symbol, correlationId); - } + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalyzer] [TRIGGERED] Processing rich manual trigger for ISIN '{Isin}' (Symbol: {Symbol}). CorrelationId: {CorrelationId}", manualReq.Isin, manualReq.Symbol, correlationId); - using var scope = _scopeFactory.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var regime = _vixTracker.GetCurrentRegime(); @@ -325,9 +417,8 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken); - var settingsService = scope.ServiceProvider.GetRequiredService(); - var settings = await settingsService.GetSettingsAsync(); - double minSignalScore = settings.MinSignalScore; + var settingsService = scope.ServiceProvider.GetRequiredService(); + double minSignalScore = await settingsService.GetSettingAsync(SettingKeys.MinWinRateThreshold, cancellationToken); double dynamicWinRate = _winRateCalculator.CalculateDynamicWinRate( manualReq.Sector, @@ -422,12 +513,12 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService { string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(manualReq.Sector) ? "general" : manualReq.Sector.ToLowerInvariant())}/{manualReq.Symbol.ToLowerInvariant()}"; await PublishAsync(propTopic, proposalDto); - _logger.LogInformation("[{Channel}] [ManualAnalyzer] [DISPATCHED] Dispatched Manual Trade Proposal {AnalysisId} to topic {Topic}", "AnalyzerChannel", analysisId, propTopic); + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[ManualAnalyzer] [DISPATCHED] Dispatched Manual Trade Proposal {AnalysisId} to topic {Topic}", analysisId, propTopic); } } catch (Exception ex) { - _logger.LogError(ex, "[{Channel}] Failed to handle manual trigger for correlation {CorrelationId}.", "AnalyzerChannel", correlationId); + await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Failed to handle manual trigger for correlation {CorrelationId}.", correlationId); try { var errorResponse = new ManualAnalysisResponseDto @@ -439,7 +530,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService } catch (Exception pubEx) { - _logger.LogError(pubEx, "[{Channel}] Failed to publish error response for correlation {CorrelationId}.", "AnalyzerChannel", correlationId); + await finlyticLogger.LogErrorAsync(SettingKeys.AnalyzerChannel, pubEx, "[AnalyzerMqttClient] Failed to publish error response for correlation {CorrelationId}.", correlationId); } } } @@ -458,13 +549,16 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService } catch (Exception ex) { - _logger.LogWarning(ex, "[{Channel}] Failed to parse VIX tick message.", "AnalyzerChannel"); + _logger.LogWarning(ex, "Failed to parse VIX tick message."); } } } private async Task ProcessNewsMessageAsync(string payloadStr, CancellationToken cancellationToken) { + using var scope = _scopeFactory.CreateScope(); + var finlyticLogger = scope.ServiceProvider.GetRequiredService>(); + var newsArticle = JsonSerializer.Deserialize(payloadStr, FinlyticJsonSerializerContext.Default.NewsArticleDto); if (newsArticle == null) return; @@ -474,17 +568,11 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService var filterResult = _filterEngine.EvaluateNews(newsArticle, regime); if (!filterResult.Passed) { - if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto)) - { - _logger.LogInformation("[{Channel}] [AutoScreener] [SKIPPED] News message skipped for ISIN '{Isin}'. Reason: {Reason}", "AnalyzerChannel", filterResult.Isin, filterResult.RejectReason); - } + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [SKIPPED] News message skipped for ISIN '{Isin}'. Reason: {Reason}", filterResult.Isin, filterResult.RejectReason); return; } - if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto)) - { - _logger.LogInformation("[{Channel}] [AutoScreener] [TRIGGERED] Screening market opportunity for ISIN '{Isin}'...", "AnalyzerChannel", filterResult.Isin); - } + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [TRIGGERED] Screening market opportunity for ISIN '{Isin}'...", filterResult.Isin); string analysisId = Guid.NewGuid().ToString("N"); string eventId = newsArticle.Id != Guid.Empty ? newsArticle.Id.ToString() : analysisId; @@ -543,7 +631,6 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService { var isinReq = new IsinRequest(filterResult.Isin); - // Parallel RPC calls (was sequential — up to 12s latency reduced to ~3s) var livePriceTask = SendRpcRequestAsync( "tr_GetLivePrice", isinReq, TimeSpan.FromSeconds(5)); var taTask = SendRpcRequestAsync( @@ -606,7 +693,6 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService if (sentResp != null) { double compound = sentResp.CurrentSummary?.CompoundScore ?? 0.0; - // FinBERT compound score is in range [-1.0, +1.0]. Normalize to [0.0, 1.0] for AI prompt context double normalizedScore = Math.Clamp((compound + 1.0) / 2.0, 0.0, 1.0); sentInfo = new SentimentContextInfo @@ -620,7 +706,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService } catch (Exception ex) { - _logger.LogWarning(ex, "[{Channel}] Failed to fetch context data for auto screener analysis.", "AnalyzerChannel"); + await finlyticLogger.LogWarningAsync(SettingKeys.AnalyzerChannel, ex, "[AnalyzerMqttClient] Failed to fetch context data for auto screener analysis."); } var n8nRequest = new N8nAnalysisRequestDto @@ -670,13 +756,8 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService var n8nResponse = await _n8nService.EvaluateAssetAsync(n8nRequest, cancellationToken); - double minSignalScore = 75.0; - using (var scope = _scopeFactory.CreateScope()) - { - var settingsService = scope.ServiceProvider.GetRequiredService(); - var settings = await settingsService.GetSettingsAsync(); - minSignalScore = settings.MinSignalScore; - } + var settingsService = scope.ServiceProvider.GetRequiredService(); + double minSignalScore = await settingsService.GetSettingAsync(SettingKeys.MinWinRateThreshold, cancellationToken); double confidenceScore = n8nResponse?.EvalScore > 0 ? n8nResponse.EvalScore : 0.75; bool isHighConviction = n8nResponse != null && @@ -752,47 +833,44 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService sentimentScore: sentResp?.CurrentSummary?.CompoundScore, signalType: n8nResponse?.SuggestedDirection ?? "BUY"); - using (var scope = _scopeFactory.CreateScope()) + var dbContext = scope.ServiceProvider.GetRequiredService(); + + bool hasRecentProposal = await dbContext.Analyses.AnyAsync(a => + a.Isin == filterResult.Isin && + a.IsTradeProposed && + a.CreatedAt >= DateTime.UtcNow.AddHours(-4), + cancellationToken); + + if (hasRecentProposal && isHighConviction) { - var dbContext = scope.ServiceProvider.GetRequiredService(); - - bool hasRecentProposal = await dbContext.Analyses.AnyAsync(a => - a.Isin == filterResult.Isin && - a.IsTradeProposed && - a.CreatedAt >= DateTime.UtcNow.AddHours(-4), - cancellationToken); - - if (hasRecentProposal && isHighConviction) - { - _logger.LogInformation("[{Channel}] [AutoScreener] Asset {Symbol} ({Isin}) already has an active trade proposal in the last 4 hours. Skipping duplicate trade proposal generation.", - "AnalyzerChannel", finalSymbol, filterResult.Isin); - isHighConviction = false; - } - - var analysisEntity = new AnalysisEntity - { - AnalysisId = analysisId, - EventId = eventId, - Sector = filterResult.Sector, - Symbol = finalSymbol, - Isin = filterResult.Isin, - VixRegime = regime, - VixValue = currentVix, - ImpactScore = filterResult.ImpactScore, - WinRate = dynamicWinRate, - RawDataJson = payloadStr, - AiOutputJson = JsonSerializer.Serialize(recommendation), - N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}", - N8nEvalScore = n8nResponse?.EvalScore ?? 0, - N8nDecision = n8nResponse?.AiDecision ?? "None", - IsTradeProposed = isHighConviction, - CreatedAt = DateTime.UtcNow - }; - - dbContext.Analyses.Add(analysisEntity); - await dbContext.SaveChangesAsync(cancellationToken); + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] Asset {Symbol} ({Isin}) already has an active trade proposal in the last 4 hours. Skipping duplicate trade proposal generation.", + finalSymbol, filterResult.Isin); + isHighConviction = false; } + var analysisEntity = new AnalysisEntity + { + AnalysisId = analysisId, + EventId = eventId, + Sector = filterResult.Sector, + Symbol = finalSymbol, + Isin = filterResult.Isin, + VixRegime = regime, + VixValue = currentVix, + ImpactScore = filterResult.ImpactScore, + WinRate = dynamicWinRate, + RawDataJson = payloadStr, + AiOutputJson = JsonSerializer.Serialize(recommendation), + N8nResponseJson = n8nResponse != null ? JsonSerializer.Serialize(n8nResponse) : "{}", + N8nEvalScore = n8nResponse?.EvalScore ?? 0, + N8nDecision = n8nResponse?.AiDecision ?? "None", + IsTradeProposed = isHighConviction, + CreatedAt = DateTime.UtcNow + }; + + dbContext.Analyses.Add(analysisEntity); + await dbContext.SaveChangesAsync(cancellationToken); + if (isHighConviction && n8nResponse != null) { var autoProposalDto = new TradeProposalDto @@ -830,7 +908,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService string propTopic = $"finlytic/trades/proposed/{(string.IsNullOrWhiteSpace(filterResult.Sector) ? "general" : filterResult.Sector.ToLowerInvariant())}/{finalSymbol.ToLowerInvariant()}"; await PublishAsync(propTopic, autoProposalDto); - _logger.LogInformation("[{Channel}] [AutoScreener] Dispatched High-Conviction Proposal {TradeId} to topic {Topic}", "AnalyzerChannel", autoProposalDto.TradeId, propTopic); + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] Dispatched High-Conviction Proposal {TradeId} to topic {Topic}", autoProposalDto.TradeId, propTopic); } if (isHighConviction) @@ -839,19 +917,13 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService await PublishAsync(recTopic, recommendation); await PublishAsync("finlytic/recommendations/auto", recommendation); - if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto)) - { - _logger.LogInformation("[{Channel}] [AutoScreener] [RECOMMENDED] High-Conviction Opportunity found for {Symbol} (Bias: {Bias}, Confidence: {Score:F2}). Published to {Topic}", - "AnalyzerChannel", finalSymbol, recommendation.RecommendedAsset.Bias, recommendation.RecommendedAsset.ConfidenceScore, recTopic); - } + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [RECOMMENDED] High-Conviction Opportunity found for {Symbol} (Bias: {Bias}, Confidence: {Score:F2}). Published to {Topic}", + finalSymbol, recommendation.RecommendedAsset.Bias, recommendation.RecommendedAsset.ConfidenceScore, recTopic); } else { - if (LogCategoryFilter.IsEnabled(LogCategory.AnalyzerAuto)) - { - _logger.LogInformation("[{Channel}] [AutoScreener] [DROPPED] Low-conviction signal for {Symbol} dropped (Confidence: {Score:F2}, Action: NO_ACTION)", - "AnalyzerChannel", finalSymbol, recommendation.RecommendedAsset.ConfidenceScore); - } + await finlyticLogger.LogInfoAsync(SettingKeys.AnalyzerChannel, "[AutoScreener] [DROPPED] Low-conviction signal for {Symbol} dropped (Confidence: {Score:F2}, Action: NO_ACTION)", + finalSymbol, recommendation.RecommendedAsset.ConfidenceScore); } } diff --git a/FinlyticAnalyzer/Util/SettingKeys.cs b/FinlyticAnalyzer/Util/SettingKeys.cs new file mode 100644 index 0000000..a1f4eac --- /dev/null +++ b/FinlyticAnalyzer/Util/SettingKeys.cs @@ -0,0 +1,30 @@ +using FinlyticCore.Models.Settings; + +namespace FinlyticAnalyzer.Util; + +public static class SettingKeys +{ + // --- Logging-Kanäle --- + public static readonly SettingKey AnalyzerChannel = new("Logging.Channel.Analyzer", true); + public static readonly SettingKey MqttChannel = new("Logging.Channel.MQTT", true); + public static readonly SettingKey HealthPingChannel = new("Logging.Channel.Health", true); + + // --- Makro & VIX Schwellenwerte --- + public static readonly SettingKey VixPanicThreshold = new("Macro.VixPanicThreshold", 28.0); + public static readonly SettingKey VixElevatedThreshold = new("Macro.VixElevatedThreshold", 20.0); + public static readonly SettingKey VixPollIntervalSeconds = new("Macro.VixPollIntervalSeconds", 60); + + // --- Filter & Winrate-Logik --- + public static readonly SettingKey MinWinRateThreshold = new("Filter.MinWinRateThreshold", 60.0); + public static readonly SettingKey WeightMacro = new("Filter.WeightMacro", 0.30); + public static readonly SettingKey WeightFundamental = new("Filter.WeightFundamental", 0.30); + public static readonly SettingKey WeightSentiment = new("Filter.WeightSentiment", 0.20); + public static readonly SettingKey WeightTechnical = new("Filter.WeightTechnical", 0.20); + + // --- Trade & Risiko-Parameter --- + public static readonly SettingKey DefaultTakeProfitPercent = new("Trade.DefaultTakeProfitPercent", 15.0); + public static readonly SettingKey DefaultStopLossPercent = new("Trade.DefaultStopLossPercent", 5.0); + public static readonly SettingKey MaxAllowedLeverage = new("Trade.MaxAllowedLeverage", 10); + public static readonly SettingKey MaxRiskPerTradePercent = new("Trade.MaxRiskPerTradePercent", 2.0); + public static readonly SettingKey ProposalValidityHours = new("Trade.ProposalValidityHours", 24); +}