From c496651dd1148c06f22ed676b3dda9ac855bd90a Mon Sep 17 00:00:00 2001 From: Kleidukos Date: Fri, 14 Aug 2026 23:55:52 +0200 Subject: [PATCH] feat(analyzer): update MQTT handler, active trade monitoring, and dynamic settings --- .../Database/AnalyzerDbContext.cs | 8 + ...813202556_CheckPendingAnalyzer.Designer.cs | 277 ++++++++++++++ .../20260813202556_CheckPendingAnalyzer.cs | 351 ++++++++++++++++++ .../AnalyzerDbContextModelSnapshot.cs | 91 ++++- FinlyticAnalyzer/Program.cs | 2 +- .../Services/ActiveTradeMonitorWorker.cs | 4 +- .../Services/N8nEvaluationService.cs | 12 +- FinlyticAnalyzer/Util/AnalyzerMqttClient.cs | 78 ++-- 8 files changed, 778 insertions(+), 45 deletions(-) create mode 100644 FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.Designer.cs create mode 100644 FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.cs diff --git a/FinlyticAnalyzer/Database/AnalyzerDbContext.cs b/FinlyticAnalyzer/Database/AnalyzerDbContext.cs index befdba3..81811c8 100644 --- a/FinlyticAnalyzer/Database/AnalyzerDbContext.cs +++ b/FinlyticAnalyzer/Database/AnalyzerDbContext.cs @@ -1,4 +1,5 @@ using FinlyticAnalyzer.Entities; +using FinlyticCore.Entities.Settings; using Microsoft.EntityFrameworkCore; namespace FinlyticAnalyzer.Database; @@ -7,6 +8,7 @@ public class AnalyzerDbContext : DbContext { public AnalyzerDbContext(DbContextOptions options) : base(options) { } + public DbSet DynamicSettings => Set(); public DbSet Analyses => Set(); public DbSet Settings => Set(); public DbSet TradeProposals => Set(); @@ -15,6 +17,12 @@ public class AnalyzerDbContext : DbContext { base.OnModelCreating(modelBuilder); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Key); + }); + modelBuilder.Entity(entity => { entity.HasIndex(e => e.AnalysisId).IsUnique(); diff --git a/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.Designer.cs b/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.Designer.cs new file mode 100644 index 0000000..b2e9f0d --- /dev/null +++ b/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.Designer.cs @@ -0,0 +1,277 @@ +// +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("20260813202556_CheckPendingAnalyzer")] + partial class CheckPendingAnalyzer + { + /// + 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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.cs b/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.cs new file mode 100644 index 0000000..2fc3b74 --- /dev/null +++ b/FinlyticAnalyzer/Migrations/20260813202556_CheckPendingAnalyzer.cs @@ -0,0 +1,351 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FinlyticAnalyzer.Migrations +{ + /// + public partial class CheckPendingAnalyzer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_TradeProposals", + table: "TradeProposals"); + + migrationBuilder.RenameTable( + name: "TradeProposals", + newName: "trade_proposals"); + + migrationBuilder.RenameIndex( + name: "IX_TradeProposals_Isin", + table: "trade_proposals", + newName: "IX_trade_proposals_Isin"); + + migrationBuilder.RenameIndex( + name: "IX_TradeProposals_ExpiresAt", + table: "trade_proposals", + newName: "IX_trade_proposals_ExpiresAt"); + + migrationBuilder.AlterColumn( + name: "ProposedAction", + table: "trade_proposals", + type: "character varying(20)", + maxLength: 20, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "trade_proposals", + type: "character varying(150)", + maxLength: 150, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Isin", + table: "trade_proposals", + type: "character varying(30)", + maxLength: 30, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "AnalysisId", + table: "trade_proposals", + type: "character varying(100)", + maxLength: 100, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "EntryPrice", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "EntryZoneMax", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: true); + + migrationBuilder.AddColumn( + name: "EntryZoneMin", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: true); + + migrationBuilder.AddColumn( + name: "EventId", + table: "trade_proposals", + type: "character varying(100)", + maxLength: 100, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "FundamentalRationale", + table: "trade_proposals", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "InstrumentType", + table: "trade_proposals", + type: "character varying(30)", + maxLength: 30, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "MaxLeverage", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: true); + + migrationBuilder.AddColumn( + name: "RiskRewardRatio", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: true); + + migrationBuilder.AddColumn( + name: "RiskTolerance", + table: "trade_proposals", + type: "character varying(30)", + maxLength: 30, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "RiskWarning", + table: "trade_proposals", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Sector", + table: "trade_proposals", + type: "character varying(50)", + maxLength: 50, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "StopLoss", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "Symbol", + table: "trade_proposals", + type: "character varying(30)", + maxLength: 30, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "TakeProfit", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "TakeProfitTargets", + table: "trade_proposals", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "TechnicalRationale", + table: "trade_proposals", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Timeframe", + table: "trade_proposals", + type: "character varying(20)", + maxLength: 20, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "VixRegime", + table: "trade_proposals", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "VixValue", + table: "trade_proposals", + type: "numeric(18,4)", + nullable: false, + defaultValue: 0m); + + migrationBuilder.AddColumn( + name: "WinRate", + table: "trade_proposals", + type: "double precision", + nullable: false, + defaultValue: 0.0); + + migrationBuilder.AddPrimaryKey( + name: "PK_trade_proposals", + table: "trade_proposals", + column: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_trade_proposals", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "AnalysisId", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "EntryPrice", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "EntryZoneMax", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "EntryZoneMin", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "EventId", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "FundamentalRationale", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "InstrumentType", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "MaxLeverage", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "RiskRewardRatio", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "RiskTolerance", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "RiskWarning", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "Sector", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "StopLoss", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "Symbol", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "TakeProfit", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "TakeProfitTargets", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "TechnicalRationale", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "Timeframe", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "VixRegime", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "VixValue", + table: "trade_proposals"); + + migrationBuilder.DropColumn( + name: "WinRate", + table: "trade_proposals"); + + migrationBuilder.RenameTable( + name: "trade_proposals", + newName: "TradeProposals"); + + migrationBuilder.RenameIndex( + name: "IX_trade_proposals_Isin", + table: "TradeProposals", + newName: "IX_TradeProposals_Isin"); + + migrationBuilder.RenameIndex( + name: "IX_trade_proposals_ExpiresAt", + table: "TradeProposals", + newName: "IX_TradeProposals_ExpiresAt"); + + migrationBuilder.AlterColumn( + name: "ProposedAction", + table: "TradeProposals", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(20)", + oldMaxLength: 20); + + migrationBuilder.AlterColumn( + name: "Name", + table: "TradeProposals", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(150)", + oldMaxLength: 150); + + migrationBuilder.AlterColumn( + name: "Isin", + table: "TradeProposals", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(30)", + oldMaxLength: 30); + + migrationBuilder.AddPrimaryKey( + name: "PK_TradeProposals", + table: "TradeProposals", + column: "Id"); + } + } +} diff --git a/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs b/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs index 009ae98..6820609 100644 --- a/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs +++ b/FinlyticAnalyzer/Migrations/AnalyzerDbContextModelSnapshot.cs @@ -149,41 +149,124 @@ namespace FinlyticAnalyzer.Migrations .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("Isin") + 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() - .HasColumnType("text"); + .HasMaxLength(150) + .HasColumnType("character varying(150)"); b.Property("ProposedAction") .IsRequired() - .HasColumnType("text"); + .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("TradeProposals"); + b.ToTable("trade_proposals"); }); #pragma warning restore 612, 618 } diff --git a/FinlyticAnalyzer/Program.cs b/FinlyticAnalyzer/Program.cs index a8b0a28..a1154b3 100644 --- a/FinlyticAnalyzer/Program.cs +++ b/FinlyticAnalyzer/Program.cs @@ -19,7 +19,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddSingleton(); // Unified MQTT Client (Handles both Events and RPC) builder.Services.AddSingleton(); diff --git a/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs b/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs index bbc62e2..2780345 100644 --- a/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs +++ b/FinlyticAnalyzer/Services/ActiveTradeMonitorWorker.cs @@ -211,8 +211,8 @@ public class ActiveTradeMonitorWorker : BackgroundService }, MarketContext = new MarketContextInfo { - Vix = (double)vixService.CurrentVix, - MarketRegime = vixService.CurrentRegime.ToString() + Vix = vixService.GetCurrentVix(), + MarketRegime = vixService.GetCurrentRegime().ToString() }, UserPreferences = new UserPreferencesInfo { diff --git a/FinlyticAnalyzer/Services/N8nEvaluationService.cs b/FinlyticAnalyzer/Services/N8nEvaluationService.cs index 43cdb96..842ada9 100644 --- a/FinlyticAnalyzer/Services/N8nEvaluationService.cs +++ b/FinlyticAnalyzer/Services/N8nEvaluationService.cs @@ -14,7 +14,11 @@ public class N8nEvaluationService : IN8nEvaluationService { _httpClient = httpClient; _logger = logger; - _webhookUrl = configuration["N8N:WebhookUrl"] ?? configuration["N8N__WebhookUrl"] ?? "https://n8n.kleidukos.me/webhook/gemini/analysis/auto"; + _webhookUrl = configuration["N8N:WebhookUrl"] ?? configuration["N8N__WebhookUrl"] ?? string.Empty; + if (string.IsNullOrWhiteSpace(_webhookUrl)) + { + _logger.LogWarning("[{Channel}] N8N:WebhookUrl configuration is missing or empty.", "AnalyzerChannel"); + } // Timeout auf 45 Sekunden erhöht für komplexere LLM/Gemini Chains in n8n _httpClient.Timeout = TimeSpan.FromSeconds(45); @@ -25,6 +29,12 @@ public class N8nEvaluationService : IN8nEvaluationService /// public async Task EvaluateAssetAsync(N8nAnalysisRequestDto request, CancellationToken cancellationToken = default) { + if (string.IsNullOrWhiteSpace(_webhookUrl)) + { + _logger.LogError("[{Channel}] Cannot execute AI evaluation for {Symbol}: N8N:WebhookUrl is not configured.", "AnalyzerChannel", request.TargetAsset.Symbol); + return null; + } + try { _logger.LogInformation("[{Channel}] Sending n8n AI Evaluation request {RequestId} for Asset {Symbol} (ISIN: {Isin}) to {Url}...", diff --git a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs b/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs index 962c973..5ef10d1 100644 --- a/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs +++ b/FinlyticAnalyzer/Util/AnalyzerMqttClient.cs @@ -180,8 +180,8 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService if (closedDto != null && !string.IsNullOrWhiteSpace(closedDto.TradeId)) { - bool isWin = closedDto.Status.Contains("Profit", StringComparison.OrdinalIgnoreCase) || - closedDto.Status.Contains("Win", StringComparison.OrdinalIgnoreCase); + bool isWin = closedDto.Status?.Contains("Profit", StringComparison.OrdinalIgnoreCase) == true || + closedDto.Status?.Contains("Win", StringComparison.OrdinalIgnoreCase) == true; var feedback = new TradeFeedbackRecord { @@ -252,8 +252,8 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService TriggerType = "Manual", TargetAsset = new TargetAssetInfo { - Symbol = manualReq.FundamentalsData?.Ticker ?? manualReq.Symbol.ToUpperInvariant(), - Name = manualReq.FundamentalsData?.CompanyName ?? manualReq.Isin.ToUpperInvariant(), + Symbol = manualReq.FundamentalsData?.Fundamentals?.Ticker?.Ticker ?? manualReq.FundamentalsData?.Asset?.PrimaryTicker?.Ticker ?? manualReq.Symbol.ToUpperInvariant(), + Name = !string.IsNullOrWhiteSpace(manualReq.FundamentalsData?.Asset?.Name) ? manualReq.FundamentalsData.Asset.Name : manualReq.Isin.ToUpperInvariant(), Isin = manualReq.Isin.ToUpperInvariant(), Sector = manualReq.Sector }, @@ -308,18 +308,18 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService }, FundamentalContext = new FundamentalContextInfo { - PeRatio = (double?)manualReq.FundamentalsData?.PeRatioTrailing, - ForwardPeRatio = (double?)manualReq.FundamentalsData?.PeRatioForward, - PegRatio = (double?)manualReq.FundamentalsData?.PegRatio, - MarketCap = (double?)manualReq.FundamentalsData?.MarketCapitalization, - DebtToEquity = (double?)manualReq.FundamentalsData?.DebtToEquity, - GrossMargin = (double?)manualReq.FundamentalsData?.GrossMargin, - NetProfitMargin = (double?)manualReq.FundamentalsData?.NetProfitMargin, - ReturnOnEquity = (double?)manualReq.FundamentalsData?.ReturnOnEquity, - DividendYield = (double?)manualReq.FundamentalsData?.DividendYield, - ShortPercentOfFloat = (double?)manualReq.FundamentalsData?.ShortPercentOfFloat, - AnalystTargetMedian = (double?)manualReq.FundamentalsData?.PriceTargetMedian, - EvToEbitda = (double?)manualReq.FundamentalsData?.EvToEbitda + PeRatio = (double?)manualReq.FundamentalsData?.Fundamentals?.TrailingPe, + ForwardPeRatio = (double?)manualReq.FundamentalsData?.Fundamentals?.ForwardPe, + PegRatio = (double?)manualReq.FundamentalsData?.Fundamentals?.PegRatio, + MarketCap = (double?)manualReq.FundamentalsData?.Fundamentals?.MarketCap, + DebtToEquity = (double?)manualReq.FundamentalsData?.Fundamentals?.DebtToEquity, + GrossMargin = (double?)manualReq.FundamentalsData?.Fundamentals?.GrossProfit, + NetProfitMargin = (double?)manualReq.FundamentalsData?.Fundamentals?.NetIncome, + ReturnOnEquity = (double?)manualReq.FundamentalsData?.Fundamentals?.ReturnOnEquity, + DividendYield = (double?)manualReq.FundamentalsData?.Fundamentals?.ForwardDividendYield, + ShortPercentOfFloat = null, + AnalystTargetMedian = null, + EvToEbitda = (double?)manualReq.FundamentalsData?.Fundamentals?.EvToEbitda } }; @@ -346,7 +346,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService Sector = manualReq.Sector, Symbol = manualReq.Symbol.ToUpperInvariant(), Isin = manualReq.Isin.ToUpperInvariant(), - CompanyName = manualReq.FundamentalsData?.CompanyName ?? manualReq.Symbol, + CompanyName = !string.IsNullOrWhiteSpace(manualReq.FundamentalsData?.Asset?.Name) ? manualReq.FundamentalsData.Asset.Name : manualReq.Symbol, EntryPrice = manualReq.CurrentPrice, SignalType = string.Equals(n8nResponse.SuggestedDirection, "Short", StringComparison.OrdinalIgnoreCase) ? "SELL" : "BUY", Status = shouldProceed ? "Proposed" : "Rejected", @@ -427,8 +427,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService Status = "ERROR", Message = $"Analysis failed: {ex.Message}" }; - await PublishAsync($"services/response/analyzer_TriggerManual/{correlationId}", - JsonSerializer.Serialize(errorResponse, FinlyticJsonSerializerContext.Default.ManualAnalysisResponseDto)); + await PublishAsync($"services/response/analyzer_TriggerManual/{correlationId}", errorResponse); } catch (Exception pubEx) { @@ -574,33 +573,38 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService if (fundResp != null) { - resolvedSymbol = !string.IsNullOrWhiteSpace(fundResp.Ticker) ? fundResp.Ticker : resolvedSymbol; - resolvedName = !string.IsNullOrWhiteSpace(fundResp.CompanyName) ? fundResp.CompanyName : resolvedName; + string? fundTicker = fundResp.Fundamentals?.Ticker?.Ticker ?? fundResp.Asset?.PrimaryTicker?.Ticker; + resolvedSymbol = !string.IsNullOrWhiteSpace(fundTicker) ? fundTicker : resolvedSymbol; + resolvedName = !string.IsNullOrWhiteSpace(fundResp.Asset?.Name) ? fundResp.Asset.Name : resolvedName; fundInfo = new FundamentalContextInfo { - PeRatio = (double?)fundResp.PeRatioTrailing, - ForwardPeRatio = (double?)fundResp.PeRatioForward, - PegRatio = (double?)fundResp.PegRatio, - MarketCap = (double?)fundResp.MarketCapitalization, - DebtToEquity = (double?)fundResp.DebtToEquity, - GrossMargin = (double?)fundResp.GrossMargin, - NetProfitMargin = (double?)fundResp.NetProfitMargin, - ReturnOnEquity = (double?)fundResp.ReturnOnEquity, - DividendYield = (double?)fundResp.DividendYield, - ShortPercentOfFloat = (double?)fundResp.ShortPercentOfFloat, - AnalystTargetMedian = (double?)fundResp.PriceTargetMedian, - EvToEbitda = (double?)fundResp.EvToEbitda + PeRatio = (double?)fundResp.Fundamentals?.TrailingPe, + ForwardPeRatio = (double?)fundResp.Fundamentals?.ForwardPe, + PegRatio = (double?)fundResp.Fundamentals?.PegRatio, + MarketCap = (double?)fundResp.Fundamentals?.MarketCap, + DebtToEquity = (double?)fundResp.Fundamentals?.DebtToEquity, + GrossMargin = (double?)fundResp.Fundamentals?.GrossProfit, + NetProfitMargin = (double?)fundResp.Fundamentals?.NetIncome, + ReturnOnEquity = (double?)fundResp.Fundamentals?.ReturnOnEquity, + DividendYield = (double?)fundResp.Fundamentals?.ForwardDividendYield, + ShortPercentOfFloat = null, + AnalystTargetMedian = null, + EvToEbitda = (double?)fundResp.Fundamentals?.EvToEbitda }; } 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 { - AssetSentimentScore = sentResp.CurrentSummary?.CompoundScore ?? 0.0, - SectorSentimentScore = 0.5, - NewsSentimentSummary = sentResp.CurrentSummary?.SentimentLabel ?? "Neutral" + AssetSentimentScore = Math.Round(normalizedScore, 2), + SectorSentimentScore = Math.Round(normalizedScore, 2), + NewsSentimentSummary = string.IsNullOrWhiteSpace(sentResp.CurrentSummary?.SentimentLabel) ? "Neutral" : sentResp.CurrentSummary.SentimentLabel }; } } @@ -684,7 +688,7 @@ public class AnalyzerMqttClient : ManagedMqttClient, IHostedService var supportLevels = new List(); var resistanceLevels = new List(); - double currentPrice = (double)(livePriceResp?.CurrentPrice > 0 ? livePriceResp.CurrentPrice : (fundResp?.CurrentPrice > 0 ? fundResp.CurrentPrice : 0.0m)); + double currentPrice = (double)(livePriceResp?.CurrentPrice > 0 ? livePriceResp.CurrentPrice : 0.0m); if (currentPrice > 0) { supportLevels.Add(Math.Round(currentPrice * 0.98, 2));